diff --git a/src/all/kavita/AndroidManifest.xml b/src/all/kavita/AndroidManifest.xml
deleted file mode 100644
index 8072ee00d..000000000
--- a/src/all/kavita/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/src/all/kavita/CHANGELOG.md b/src/all/kavita/CHANGELOG.md
deleted file mode 100644
index 2843891e9..000000000
--- a/src/all/kavita/CHANGELOG.md
+++ /dev/null
@@ -1,93 +0,0 @@
-## 1.3.13
-
-### Fixed
-
- * Fixed 'null cannot be cast to non-null type' exception
-
-## 1.3.12
-
-### Features
-
-* Migrate filters to v2
-* Implemented smartFilters
-* Added localization support
-
-### Fixed
-
-* Fixed publication status not showing
-
-## 1.3.10
-
-### Features
-
-* API Change for Kavita v0.7.2
-
-## 1.3.9
-
-### Features
-
-* Added pdf support
-
-## 1.3.8
-
-### Fix
-
-* Fixed `Expected URL scheme 'http' or 'https` when downloading
-
-## 1.3.7
-
-### Features
-
-* New Sort filter: Time to read
-* New Filter: Year release filter
-
-### Fix
-
-* Filters can now be used together with search
-* Epub and pdfs no longer show in format filter (currently not supported)
-
-## 1.3.6
-
-### Fix
-
-* Fixed "lateinit property title not initialized"
-
-## 1.3.5
-
-### Features
-
-* Ignore DOH
-* Added sort option `Item Added`
-* Latest button now shows latest `Item Added`
-
-## 1.3.4
-
-### Features
-
-* Exclude from bulk update warnings
-
-## 1.2.3
-
-### Fix
-
-* Fixed Rating filter
-* Fixed Chapter list not sorting correctly
-* Fixed search
-* Fixed manga details not showing correctly
-* Fixed filters not populating if account was not admin
-
-### Features
-* The extension is now ready to implement tracking.
-* Min required version for the extension to work properly: `v0.5.1.1`
-
-## 1.2.2
-
-### Features
-
-* Add `CHANGELOG.md` & `README.md`
-
-## 1.2.1
-
-### Features
-
-* first version
diff --git a/src/all/kavita/README.md b/src/all/kavita/README.md
deleted file mode 100644
index 79f467edd..000000000
--- a/src/all/kavita/README.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# Kavita
-
-Table of Content
-- [FAQ](#FAQ)
- - [Why do I see no manga?](#why-do-i-see-no-manga)
- - [Where can I get more information about Kavita?](#where-can-i-get-more-information-about-kavita)
- - [The Kavita extension stopped working?](#the-kavita-extension-stopped-working)
- - [Can I add more than one Kavita server or user?](#can-i-add-more-than-one-kavita-server-or-user)
- - [Can I test the Kavita extension before setting up my own server?](#can-i-test-the-kavita-extension-before-setting-up-my-own-server)
-- [Guides](#Guides)
- - [How do I add my Kavita server to Tachiyomi?](#how-do-i-add-my-kavita-server-to-tachiyomi)
-
-Don't find the question you are look for go check out our general FAQs and Guides over at [Extension FAQ](https://tachiyomi.org/help/faq/#extensions) or [Getting Started](https://tachiyomi.org/help/guides/getting-started/#installation)
-
-Kavita also has a documentation about the Tachiyomi Kavita extension at the [Kavita wiki](https://wiki.kavitareader.com/en/guides/misc/tachiyomi).
-
-## FAQ
-
-### Why do I see no manga?
-Kavita is a self-hosted comic/manga media server.
-
-### Where can I get more information about Kavita?
-You can visit the [Kavita](https://www.kavitareader.com/) website for for more information.
-
-### The Kavita extension stopped working?
-Make sure that your Kavita server and extension are on the newest version.
-
-### Can I add more than one Kavita server or user?
-Yes, currently you can add up to 3 different Kavita instances to Tachiyomi.
-
-### Can I test the Kavita extension before setting up my own server?
-Yes, you can try it out with the DEMO servers OPDS url `https://demo.kavitareader.com/api/opds/aca1c50d-7e08-4f37-b356-aecd6bf69b72`.
-
-## Guides
-
-### How do I add my Kavita server to Tachiyomi?
-Go into the settings of the Kavita extension from the Extension tab in Browse and fill in your OPDS url.
diff --git a/src/all/kavita/assets/i18n/messages_en.properties b/src/all/kavita/assets/i18n/messages_en.properties
deleted file mode 100644
index 52f341d65..000000000
--- a/src/all/kavita/assets/i18n/messages_en.properties
+++ /dev/null
@@ -1,18 +0,0 @@
-login_errors_failed_login=Login failed. Something went wrong
-login_errors_header_token_empty="Error: The JSON Web Token is empty.\nTry opening the extension first."
-login_errors_invalid_url=Invalid URL:
-login_errors_parse_tokendto=There was an error parsing the auth token
-pref_customsource_title=Displayed name for source
-pref_edit_customsource_summary=Here you can change this source name.\nYou can write a descriptive name to identify this OPDS URL.
-pref_filters_summary=Show these filters in the filter list
-pref_filters_title=Default filters shown
-pref_opds_badformed_url=Incorrect OPDS address. Please copy it from User settings \u2192 3rd party apps \u2192 OPDS URL
-pref_opds_duplicated_source_url=The URL is configured in a different source ->
-pref_opds_must_setup_address=You must set up the address to communicate with Kavita
-pref_opds_summary=The OPDS URL copied from User Settings. This should include address and end with the API key.
-restartapp_settings=Restart Tachiyomi to apply new setting.
-version_exceptions_chapters_parse=Unhandled exception parsing chapters. Send your logs to the Kavita devs.
-check_version=Ensure you have the newest version of the extension and Kavita. (0.7.8 or newer.)\nIf the issue persists, report it to the Kavita developers with the accompanying logs.
-version_exceptions_smart_filter=Could not decode SmartFilter. Ensure you are using Kavita version 0.7.11 or later.
-http_errors_500=Something went wrong
-http_errors_401=There was an error logging in. Try again or reload the app
diff --git a/src/all/kavita/assets/i18n/messages_es_es.properties b/src/all/kavita/assets/i18n/messages_es_es.properties
deleted file mode 100644
index 8f6d3058c..000000000
--- a/src/all/kavita/assets/i18n/messages_es_es.properties
+++ /dev/null
@@ -1,18 +0,0 @@
-pref_customsource_title=Nombre de la instancia
-pref_edit_customsource_summary=Aqui puedes cambiar el nombre de la instancia.\nPuedes escribir un nombre descriptivo que identifique esta url/instancia
-restartapp_settings=Reinicia la aplicación para aplicar los cambios
-version_exceptions_chapters_parse=Algo ha ido mal al procesar los capitulos. Envia los registros de fallo a los desarrolladores de Kavita
-check_version=Comprueba que tienes tanto Kavita como la extension actualizada. (Version minima: 0.7.8)\nSi el problema persiste, reportalo a los desarrolladores de Kavita aportando los registros de fallo.
-version_exceptions_smart_filter=Fallo al decodificar los filtros inteligentes. Aseg\u00FArate que estas al menos en la version 0.7.11 de Kavita
-http_errors_500=Algo ha ido mal
-http_errors_401=Ha habido un error al iniciar sesi\u00F3n. Prueba otra vez o reinicia la aplicaci\u00F3n
-pref_opds_summary=La url del OPDS copiada de la configuraci\u00F3n del usuario. Debe incluir la direcci\u00F3n y la clave api al final.
-pref_filters_summary=Mostrar estos filtros en la lista de filtros
-pref_filters_title=Filtros por defecto
-pref_opds_badformed_url=La direcci\u00F3n OPDS no es correcta. Por favor, c\u00F3piela desde la Configuraci\u00F3n de usuario-> aplicaciones de terceros -> url de OPDS
-login_errors_parse_tokendto=Se ha producido un error al procesar el token de autenticaci\u00F3n
-pref_opds_duplicated_source_url=Url est\u00E1 configurado en una fuente diferente ->
-pref_opds_must_setup_address=Debe configurar la direcci\u00F3n para comunicarse con Kavita
-login_errors_failed_login=Error en el inicio de sesi\u00F3n. Algo ha ido mal
-login_errors_header_token_empty="Error: el token jwt est\u00E1 vac\u00EDo.\nIntente abrir primero la extensi\u00F3n"
-login_errors_invalid_url=URL no v\u00E1lida:
diff --git a/src/all/kavita/assets/i18n/messages_fr_fr.properties b/src/all/kavita/assets/i18n/messages_fr_fr.properties
deleted file mode 100644
index bd29b962a..000000000
--- a/src/all/kavita/assets/i18n/messages_fr_fr.properties
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-version_exceptions_chapters_parse=Exception non trait\u00E9e durant l'analyse des chapitres. Envoyez les journaux aux d\u00E9velopeurs de Kavita
-pref_customsource_title=Nom d'affichage pour la source
-version_exceptions_smart_filter=\u00C9chec du d\u00E9codage de SmartFilter. Assurez-vous que vous utilisez au moins Kavita version 0.7.11
-pref_opds_summary=L'URL OPDS a \u00E9t\u00E9 copi\u00E9e \u00E0 partir des param\u00E8tres de l'utilisateur. Ceci devrait inclure l'adresse et la cl\u00E9 API.
-pref_filters_summary=Afficher ces filtres dans la liste des filtres
-check_version=Assurez-vous que vous avez l'extension et Kavita mises \u00E0 jour. (version Mini\u202F: 0.7.8)\nSi le probl\u00E8me persiste, signalez-le aux d\u00E9veloppeurs de Kavita en fournissant des journaux
-pref_filters_title=Filtres par d\u00E9faut affich\u00E9s
-pref_edit_customsource_summary=Ici vous pouvez changer ce nom source.\nVous pouvez \u00E9crire un nom descriptif pour identifier cette URL opds
-pref_opds_badformed_url=L'adresse OPDS n'est pas correcte. Veuillez la copiez \u00E0 partir des param\u00E8tres de l'utilisateur - > Applis tierces -> URL OPDS
-login_errors_parse_tokendto=Il y a eu une erreur pendant l'analyse du jeton d'authentification
-restartapp_settings=Red\u00E9marrez Tachiyomi pour appliquer le nouveau r\u00E9glage.
-pref_opds_duplicated_source_url=L'URL est configur\u00E9e dans une autre source ->
-pref_opds_must_setup_address=Vous devez configurer l'adresse pour communiquer avec Kavita
-login_errors_failed_login=\u00C9chec de la connexion. Quelque chose s'est mal pass\u00E9
-http_errors_500=Quelque chose s'est mal pass\u00E9
-login_errors_header_token_empty=\u00AB\u00A0Erreur\u202F: le jeton jwt est vide.\nEssayez d'abord d'ouvrir l'extension\u00A0\u00BB
-login_errors_invalid_url=URL invalide\u202F:
-http_errors_401=Il y a eu une erreur. Essayez de nouveau ou rechargez l'application
diff --git a/src/all/kavita/assets/i18n/messages_nb_no.properties b/src/all/kavita/assets/i18n/messages_nb_no.properties
deleted file mode 100644
index c2e0a530d..000000000
--- a/src/all/kavita/assets/i18n/messages_nb_no.properties
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-pref_customsource_title=Vist kildenavn
-pref_edit_customsource_summary=Her kan du endre dette kildenavnet.\nDu kan skrive et beskrivende navn for \u00E5 identifisere denne OPDS-nettadressen.
-restartapp_settings=Ny innstilling trer i kraft n\u00E5r du starter Tachiyomi p\u00E5 ny.
-duplicated_source_url=Nettadressen er satt opp i en annen Kavita-instans
-pref_filters_summary=Vis disse filterne i filterlisten
-pref_filters_title=Forvalgte filtre valgt
-login_errors_parse_tokendto=Kunne ikke tolke identifiseringssymbolet
-login_errors_failed_login=Innlogging mislyktes. Noe gikk galt.
-http_errors_500=Noe gikk galt
-login_errors_header_token_empty="Feil: JSON-nettsymbol er tomt.\nPr\u00F8v \u00E5 \u00E5pne utvidelsen f\u00F8rst."
-login_errors_invalid_url=Ugyldig nettadresse:
-version_exceptions_chapters_parse=Uh\u00E5ndtert unntak i tolking av kapitler. Send loggene dine til Kavita-utviklerne.
-version_exceptions_smart_filter=Kunne ikke dekode smartfilter. Forsikre deg om at du bruker Kavita versjon 0.7.11 eller nyere.
-pref_opds_summary=OPDS-nettadressen kopiert fra brukerinnstillingene. Denne skal inkludere med adressen og slutte med API-n\u00F8kkelen.
-check_version=Forsikre deg om at b\u00E5de utvidelsen og Kavita er av nyeste versjon. (Ihvertfall 0.7.8)\nHvis problemet vedvarer kan du rapportere det til Kavita-utviklerne med tilh\u00F8rende loggf\u00F8ring.
-pref_opds_badformed_url=OPDS-adressen er ikke riktig. Kopier den fra brukerinnstillinger -> tredjepartsprogrammer -> OPDS-nettadresse
-pref_opds_duplicated_source_url=Nettadressen er satt opp i en annen instans ->
-pref_opds_must_setup_address=Du m\u00E5 sette opp adressen som skal kommunisere med Kavita
-http_errors_401=Feil med innlogging. Pr\u00F8v \u00E5 laste inn p\u00E5 ny, eller start programmet p\u00E5 ny.
diff --git a/src/all/kavita/build.gradle b/src/all/kavita/build.gradle
deleted file mode 100644
index bb2376766..000000000
--- a/src/all/kavita/build.gradle
+++ /dev/null
@@ -1,12 +0,0 @@
-ext {
- extName = 'Kavita'
- extClass = '.KavitaFactory'
- extVersionCode = 13
-}
-
-dependencies {
- implementation 'info.debatty:java-string-similarity:2.0.0'
- implementation(project(':lib:i18n'))
-}
-
-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
deleted file mode 100644
index 16cee6c3e..000000000
Binary files a/src/all/kavita/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/kavita/res/mipmap-mdpi/ic_launcher.png b/src/all/kavita/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index ebcbc75a2..000000000
Binary files a/src/all/kavita/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/kavita/res/mipmap-xhdpi/ic_launcher.png b/src/all/kavita/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 39ca4ccaa..000000000
Binary files a/src/all/kavita/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/kavita/res/mipmap-xxhdpi/ic_launcher.png b/src/all/kavita/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index 98f3ea1ff..000000000
Binary files a/src/all/kavita/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/kavita/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/kavita/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index dc7d8341f..000000000
Binary files a/src/all/kavita/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/Filters.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/Filters.kt
deleted file mode 100644
index 80d0e4075..000000000
--- a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/Filters.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-package eu.kanade.tachiyomi.extension.all.kavita
-
-import eu.kanade.tachiyomi.extension.all.kavita.KavitaConstants.noSmartFilterSelected
-import eu.kanade.tachiyomi.source.model.Filter
-
-class UserRating :
- Filter.Select(
- "Minimum Rating",
- arrayOf(
- "Any",
- "1 star",
- "2 stars",
- "3 stars",
- "4 stars",
- "5 stars",
- ),
- )
-class SmartFiltersFilter(smartFilters: Array) :
- Filter.Select("Smart Filters", arrayOf(noSmartFilterSelected) + smartFilters)
-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),
- Pair("Item added", 4),
- Pair("Time to Read", 5),
- Pair("Release year", 6),
-)
-
-class StatusFilter(name: String) : Filter.CheckBox(name, false)
-class StatusFilterGroup(filters: List) :
- Filter.Group("Status", filters)
-
-class ReleaseYearRange(name: String) : Filter.Text(name)
-class ReleaseYearRangeGroup(filters: List) :
- Filter.Group("Release Year", filters)
-class GenreFilter(name: String) : Filter.TriState(name)
-class GenreFilterGroup(genres: List) :
- Filter.Group("Genres", genres)
-
-class TagFilter(name: String) : Filter.TriState(name)
-class TagFilterGroup(tags: List) : Filter.Group("Tags", tags)
-
-class AgeRatingFilter(name: String) : Filter.TriState(name)
-class AgeRatingFilterGroup(ageRatings: List) :
- Filter.Group("Age Rating", ageRatings)
-
-class FormatFilter(name: String) : Filter.CheckBox(name, false)
-class FormatsFilterGroup(formats: List) :
- Filter.Group("Formats", formats)
-
-class CollectionFilter(name: String) : Filter.TriState(name)
-class CollectionFilterGroup(collections: List) :
- Filter.Group("Collection", collections)
-
-class LanguageFilter(name: String) : Filter.TriState(name)
-class LanguageFilterGroup(languages: List) :
- Filter.Group("Language", languages)
-
-class LibraryFilter(library: String) : Filter.TriState(library)
-class LibrariesFilterGroup(libraries: List) :
- Filter.Group("Libraries", libraries)
-
-class PubStatusFilter(name: String) : Filter.CheckBox(name, false)
-class PubStatusFilterGroup(status: List) :
- Filter.Group("Publication Status", status)
-
-class PeopleHeaderFilter(name: String) :
- Filter.Header(name)
-class PeopleSeparatorFilter :
- Filter.Separator()
-
-class WriterPeopleFilter(name: String) : Filter.CheckBox(name, false)
-class WriterPeopleFilterGroup(peoples: List) :
- Filter.Group("Writer", peoples)
-
-class PencillerPeopleFilter(name: String) : Filter.CheckBox(name, false)
-class PencillerPeopleFilterGroup(peoples: List) :
- Filter.Group("Penciller", peoples)
-
-class InkerPeopleFilter(name: String) : Filter.CheckBox(name, false)
-class InkerPeopleFilterGroup(peoples: List) :
- Filter.Group("Inker", peoples)
-
-class ColoristPeopleFilter(name: String) : Filter.CheckBox(name, false)
-class ColoristPeopleFilterGroup(peoples: List) :
- Filter.Group("Colorist", peoples)
-
-class LettererPeopleFilter(name: String) : Filter.CheckBox(name, false)
-class LettererPeopleFilterGroup(peoples: List) :
- Filter.Group("Letterer", peoples)
-
-class CoverArtistPeopleFilter(name: String) : Filter.CheckBox(name, false)
-class CoverArtistPeopleFilterGroup(peoples: List) :
- Filter.Group("Cover Artist", peoples)
-
-class EditorPeopleFilter(name: String) : Filter.CheckBox(name, false)
-class EditorPeopleFilterGroup(peoples: List) :
- Filter.Group("Editor", peoples)
-
-class PublisherPeopleFilter(name: String) : Filter.CheckBox(name, false)
-class PublisherPeopleFilterGroup(peoples: List) :
- Filter.Group("Publisher", peoples)
-
-class CharacterPeopleFilter(name: String) : Filter.CheckBox(name, false)
-class CharacterPeopleFilterGroup(peoples: List) :
- Filter.Group("Character", peoples)
-
-class TranslatorPeopleFilter(name: String) : Filter.CheckBox(name, false)
-class TranslatorPeopleFilterGroup(peoples: List) :
- Filter.Group("Translator", peoples)
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
deleted file mode 100644
index 079e457a9..000000000
--- a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/Kavita.kt
+++ /dev/null
@@ -1,1264 +0,0 @@
-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.AppInfo
-import eu.kanade.tachiyomi.extension.all.kavita.dto.AuthenticationDto
-import eu.kanade.tachiyomi.extension.all.kavita.dto.FilterComparison
-import eu.kanade.tachiyomi.extension.all.kavita.dto.FilterField
-import eu.kanade.tachiyomi.extension.all.kavita.dto.FilterStatementDto
-import eu.kanade.tachiyomi.extension.all.kavita.dto.FilterV2Dto
-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.ServerInfoDto
-import eu.kanade.tachiyomi.extension.all.kavita.dto.SmartFilter
-import eu.kanade.tachiyomi.extension.all.kavita.dto.SortFieldEnum
-import eu.kanade.tachiyomi.extension.all.kavita.dto.SortOptions
-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.UnmeteredSource
-import eu.kanade.tachiyomi.source.model.Filter
-import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_EXCLUDE
-import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_INCLUDE
-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.buildJsonObject
-import kotlinx.serialization.json.encodeToJsonElement
-import kotlinx.serialization.json.put
-import okhttp3.Dns
-import okhttp3.Headers
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
-import okhttp3.MediaType.Companion.toMediaTypeOrNull
-import okhttp3.OkHttpClient
-import okhttp3.Request
-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.net.ConnectException
-import java.security.MessageDigest
-import java.util.Locale
-
-class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSource, HttpSource() {
- private val helper = KavitaHelper()
- override val client: OkHttpClient =
- network.client.newBuilder()
- .dns(Dns.SYSTEM)
- .build()
- 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 = "${KavitaInt.KAVITA_NAME} (${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 val apiKey by lazy { getPrefApiKey() }
- private var jwtToken = "" // * JWT Token for authentication with the server. Stored in memory.
- private val LOG_TAG = """Kavita_${"[$suffix]_" + preferences.getString(KavitaConstants.customSourceNamePref, "[$suffix]")!!.replace(' ', '_')}"""
- private var isLogged = false // Used to know if login was correct and not send login requests anymore
- private val json: Json by injectLazy()
-
- private var series = emptyList() // Acts as a cache
-
- private inline fun Response.parseAs(): T =
- use {
- if (it.code == 401) {
- Log.e(LOG_TAG, "Http error 401 - Not authorized: ${it.request.url}")
- Throwable("Http error 401 - Not authorized: ${it.request.url}")
- }
-
- if (it.peekBody(Long.MAX_VALUE).string().isEmpty()) {
- Log.e(LOG_TAG, "Empty body String for request url: ${it.request.url}")
- throw EmptyRequestBody(
- "Body of the response is empty. RequestUrl=${it.request.url}\nPlease check your kavita instance is up to date",
- Throwable("Error. Request body is empty"),
- )
- }
- json.decodeFromString(it.body.string())
- }
-
- /**
- * Custom implementation for fetch popular, latest and search
- * Handles and logs errors to provide a more detailed exception to the users.
- */
- private fun fetch(request: Request): Observable {
- return client.newCall(request)
- .asObservableSuccess()
- .onErrorResumeNext { throwable ->
- // Get Http code
- val field = throwable.javaClass.getDeclaredField("code")
- field.isAccessible = true // Make the field accessible
- try {
- var code = field.get(throwable) // Get the value of the code property
- Log.e(LOG_TAG, "Error fetching manga: ${throwable.message}", throwable)
- if (code as Int !in intArrayOf(401, 201, 500)) {
- code = 500
- }
- return@onErrorResumeNext Observable.error(IOException("Http Error: $code\n ${helper.intl["http_errors_$code"]}\n${helper.intl["check_version"]}"))
- } catch (e: Exception) {
- Log.e(LOG_TAG, e.toString(), e)
- return@onErrorResumeNext Observable.error(e)
- }
- }
- .map { response ->
- popularMangaParse(response)
- }
- }
-
- override fun fetchPopularManga(page: Int) =
- fetch(popularMangaRequest(page))
-
- override fun fetchLatestUpdates(page: Int) =
- fetch(latestUpdatesRequest(page))
-
- override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable =
- fetch(searchMangaRequest(page, query, filters))
-
- override fun popularMangaParse(response: Response): MangasPage {
- try {
- val result = response.parseAs>()
- series = result
- val mangaList = result.map { item -> helper.createSeriesDto(item, apiUrl, apiKey) }
- return MangasPage(mangaList, helper.hasNextPage(response))
- } catch (e: Exception) {
- Log.e(LOG_TAG, "Unhandled exception", e)
- throw IOException(helper.intl["check_version"])
- }
- }
-
- override fun popularMangaRequest(page: Int): Request {
- if (!isLogged) {
- doLogin()
- }
- val payload = buildFilterBody(currentFilter)
- return POST(
- "$apiUrl/series/all-v2?pageNumber=$page&pageSize=20",
- headersBuilder().build(),
- payload.toRequestBody(JSON_MEDIA_TYPE),
- )
- }
-
- override fun latestUpdatesRequest(page: Int): Request {
- if (!isLogged) {
- doLogin()
- }
- // Hardcode exclude epubs
- val filter = FilterV2Dto(sortOptions = SortOptions(SortFieldEnum.LastChapterAdded.type, false))
- filter.statements.add(FilterStatementDto(FilterComparison.NotContains.type, FilterField.Formats.type, "3"))
- val payload = json.encodeToJsonElement(filter).toString()
- return POST(
- "$apiUrl/series/all-v2?pageNumber=$page&pageSize=20",
- headersBuilder().build(),
- payload.toRequestBody(JSON_MEDIA_TYPE),
- )
- }
-
- override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
- val newFilter = MetadataPayload() // need to reset it or will double
- val smartFilterFilter = filters.find { it is SmartFiltersFilter }
- // If a SmartFilter selected, apply its filter and return that
- if (smartFilterFilter?.state != 0 && smartFilterFilter != null) {
- val index = try {
- smartFilterFilter?.state as Int - 1
- } catch (e: Exception) {
- Log.e(LOG_TAG, e.toString(), e)
- 0
- }
-
- val filter: SmartFilter = smartFilters[index]
- val payload = buildJsonObject {
- put("EncodedFilter", filter.filter)
- }
- // Decode selected filters
- val request = POST(
- "$apiUrl/filter/decode",
- headersBuilder().build(),
- payload.toString().toRequestBody(JSON_MEDIA_TYPE),
- )
- client.newCall(request).execute().use {
- if (it.code == 200) {
- // Hardcode exclude epub
- val decoded_filter = json.decodeFromString(it.body.string())
- decoded_filter.statements.add(FilterStatementDto(FilterComparison.NotContains.type, FilterField.Formats.type, "3"))
-
- // Make request with selected filters
- return POST(
- "$apiUrl/series/all-v2?pageNumber=$page&pageSize=20",
- headersBuilder().build(),
- json.encodeToJsonElement(decoded_filter).toString().toRequestBody(JSON_MEDIA_TYPE),
- )
- } else {
- Log.e(LOG_TAG, "Failed to decode SmartFilter: ${it.code}\n" + it.message)
- throw IOException(helper.intl["version_exceptions_smart_filter"])
- }
- }
- }
- // Else apply user filters
-
- filters.forEach { filter ->
- when (filter) {
- is SortFilter -> {
- if (filter.state != null) {
- newFilter.sorting = filter.state!!.index + 1
- newFilter.sorting_asc = filter.state!!.ascending
- }
- }
-
- is StatusFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state) {
- newFilter.readStatus.add(content.name)
- }
- }
- }
-
- is ReleaseYearRangeGroup -> {
- filter.state.forEach { content ->
- if (content.state.isNotEmpty()) {
- if (content.name == "Min") {
- newFilter.releaseYearRangeMin = content.state.toInt()
- }
- if (content.name == "Max") {
- newFilter.releaseYearRangeMax = content.state.toInt()
- }
- }
- }
- }
-
- is GenreFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state == STATE_INCLUDE) {
- newFilter.genres_i.add(genresListMeta.find { it.title == content.name }!!.id)
- } else if (content.state == STATE_EXCLUDE) {
- newFilter.genres_e.add(genresListMeta.find { it.title == content.name }!!.id)
- }
- }
- }
-
- is UserRating -> {
- newFilter.userRating = filter.state
- }
-
- is TagFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state == STATE_INCLUDE) {
- newFilter.tags_i.add(tagsListMeta.find { it.title == content.name }!!.id)
- } else if (content.state == STATE_EXCLUDE) {
- newFilter.tags_e.add(tagsListMeta.find { it.title == content.name }!!.id)
- }
- }
- }
-
- is AgeRatingFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state == STATE_INCLUDE) {
- newFilter.ageRating_i.add(ageRatingsListMeta.find { it.title == content.name }!!.value)
- } else if (content.state == STATE_EXCLUDE) {
- newFilter.ageRating_e.add(ageRatingsListMeta.find { it.title == content.name }!!.value)
- }
- }
- }
-
- is FormatsFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state) {
- newFilter.formats.add(MangaFormat.valueOf(content.name).ordinal)
- }
- }
- }
-
- is CollectionFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state == STATE_INCLUDE) {
- newFilter.collections_i.add(collectionsListMeta.find { it.title == content.name }!!.id)
- } else if (content.state == STATE_EXCLUDE) {
- newFilter.collections_e.add(collectionsListMeta.find { it.title == content.name }!!.id)
- }
- }
- }
-
- is LanguageFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state == STATE_INCLUDE) {
- newFilter.language_i.add(languagesListMeta.find { it.title == content.name }!!.isoCode)
- } else if (content.state == STATE_EXCLUDE) {
- newFilter.language_e.add(languagesListMeta.find { it.title == content.name }!!.isoCode)
- }
- }
- }
-
- is LibrariesFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state == STATE_INCLUDE) {
- newFilter.libraries_i.add(libraryListMeta.find { it.name == content.name }!!.id)
- } else if (content.state == STATE_EXCLUDE) {
- newFilter.libraries_e.add(libraryListMeta.find { it.name == content.name }!!.id)
- }
- }
- }
-
- is PubStatusFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state) {
- newFilter.pubStatus.add(pubStatusListMeta.find { it.title == content.name }!!.value)
- }
- }
- }
-
- is WriterPeopleFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state) {
- newFilter.peopleWriters.add(peopleListMeta.find { it.name == content.name }!!.id)
- }
- }
- }
-
- is PencillerPeopleFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state) {
- newFilter.peoplePenciller.add(peopleListMeta.find { it.name == content.name }!!.id)
- }
- }
- }
-
- is InkerPeopleFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state) {
- newFilter.peopleInker.add(peopleListMeta.find { it.name == content.name }!!.id)
- }
- }
- }
-
- is ColoristPeopleFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state) {
- newFilter.peoplePeoplecolorist.add(peopleListMeta.find { it.name == content.name }!!.id)
- }
- }
- }
-
- is LettererPeopleFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state) {
- newFilter.peopleLetterer.add(peopleListMeta.find { it.name == content.name }!!.id)
- }
- }
- }
-
- is CoverArtistPeopleFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state) {
- newFilter.peopleCoverArtist.add(peopleListMeta.find { it.name == content.name }!!.id)
- }
- }
- }
-
- is EditorPeopleFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state) {
- newFilter.peopleEditor.add(peopleListMeta.find { it.name == content.name }!!.id)
- }
- }
- }
-
- is PublisherPeopleFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state) {
- newFilter.peoplePublisher.add(peopleListMeta.find { it.name == content.name }!!.id)
- }
- }
- }
-
- is CharacterPeopleFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state) {
- newFilter.peopleCharacter.add(peopleListMeta.find { it.name == content.name }!!.id)
- }
- }
- }
-
- is TranslatorPeopleFilterGroup -> {
- filter.state.forEach { content ->
- if (content.state) {
- newFilter.peopleTranslator.add(peopleListMeta.find { it.name == content.name }!!.id)
- }
- }
- }
-
- else -> {}
- }
- }
- newFilter.seriesNameQuery = query
- currentFilter = newFilter
- return popularMangaRequest(page)
- }
-
- /*
- * 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 ->
- 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, apiKey)
- manga.url = "$apiUrl/Series/${result.seriesId}"
- 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 }
- manga.thumbnail_url = "$apiUrl/image/series-cover?seriesId=${result.seriesId}&apiKey=$apiKey"
-
- return manga
- }
- val serieDto = client.newCall(GET("$apiUrl/Series/${result.seriesId}", headersBuilder().build()))
- .execute()
- .parseAs()
-
- return SManga.create().apply {
- url = "$apiUrl/Series/${result.seriesId}"
- artist = result.coverArtists.joinToString { it.name }
- description = result.summary
- author = result.writers.joinToString { it.name }
- genre = result.genres.joinToString { it.title }
- title = serieDto.name
- thumbnail_url = "$apiUrl/image/series-cover?seriesId=${result.seriesId}&apiKey=$apiKey"
- status = when (result.publicationStatus) {
- 4 -> SManga.PUBLISHING_FINISHED
- 2 -> SManga.COMPLETED
- 0 -> SManga.ONGOING
- 3 -> SManga.CANCELLED
- 1 -> SManga.ON_HIATUS
- else -> SManga.UNKNOWN
- }
- }
- }
-
- /*
- * CHAPTER LIST
- * **/
- override fun chapterListRequest(manga: SManga): Request {
- val url = "$apiUrl/Series/volumes?seriesId=${helper.getIdFromUrl(manga.url)}"
- return GET(url, headersBuilder().build())
- }
-
- 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(helper.chapterFromObject(it))
- }
- } else {
- // Volume chapter
- volume.chapters.map {
- allChapterList.add(helper.chapterFromVolume(it, volume))
- }
- }
- }
- }
-
- allChapterList.sortWith(KavitaHelper.CompareChapters)
- return allChapterList
- } catch (e: Exception) {
- Log.e(LOG_TAG, "Unhandled exception parsing chapters. Send logs to kavita devs", e)
- throw IOException(helper.intl["version_exceptions_chapters_parse"])
- }
- }
-
- /**
- * Fetches the "url" of each page from the chapter
- * **/
- override fun pageListRequest(chapter: SChapter): Request {
- return GET("$apiUrl/${chapter.url}", headersBuilder().build())
- }
-
- override fun fetchPageList(chapter: SChapter): Observable> {
- val chapterId = chapter.url
- val numPages = chapter.scanlator?.replace(" pages", "")?.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&extractPdf=true&apiKey=$apiKey",
- ),
- )
- }
- return Observable.just(pages)
- }
-
- override fun latestUpdatesParse(response: Response): MangasPage =
- throw UnsupportedOperationException("Not used")
-
- override fun pageListParse(response: Response): List =
- throw UnsupportedOperationException("Not used")
-
- override fun searchMangaParse(response: Response): MangasPage =
- throw UnsupportedOperationException("Not used")
-
- override fun imageUrlParse(response: Response): String = ""
-
- /*
- * FILTERING
- **/
-
- private var currentFilter: MetadataPayload = MetadataPayload()
-
- /** 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 var smartFilters = emptyList()
- private val personRoles = listOf(
- "Writer",
- "Penciller",
- "Inker",
- "Colorist",
- "Letterer",
- "CoverArtist",
- "Editor",
- "Publisher",
- "Character",
- "Translator",
- )
-
- /**
- * Loads the enabled filters if they are not empty so tachiyomi can show them to the user
- */
- override fun getFilterList(): FilterList {
- val toggledFilters = getToggledFilters()
-
- val filters = try {
- val peopleInRoles = mutableListOf>()
- personRoles.map { role ->
- val peoplesWithRole = mutableListOf()
- peopleListMeta.map {
- if (it.role == helper.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 (smartFilters.isNotEmpty()) {
- filtersLoaded.add(
- SmartFiltersFilter(smartFilters.map { it.name }.toTypedArray()),
-
- )
- }
- }
- if (toggledFilters.contains("Read Status")) {
- filtersLoaded.add(
- StatusFilterGroup(
- listOf(
- "notRead",
- "inProgress",
- "read",
- ).map { StatusFilter(it) },
- ),
- )
- }
- if (toggledFilters.contains("ReleaseYearRange")) {
- filtersLoaded.add(
- ReleaseYearRangeGroup(
- listOf("Min", "Max").map { ReleaseYearRange(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",
- "Pdf",
- "Unknown",
- ).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)
- }
-
- /**
- * Returns a FilterV2Dto encoded as a json string with values taken from filter
- */
- private fun buildFilterBody(filter: MetadataPayload): String {
- val filter_dto = FilterV2Dto()
- filter_dto.sortOptions.sortField = filter.sorting
- filter_dto.sortOptions.isAscending = filter.sorting_asc
-
- // Fields that support contains and not contains statements
- val containsAndNotTriplets = listOf(
- Triple(FilterField.Libraries, filter.libraries_i, filter.libraries_e),
- Triple(FilterField.Tags, filter.tags_i, filter.tags_e),
- Triple(FilterField.Languages, filter.language_i, filter.genres_e),
- Triple(FilterField.AgeRating, filter.ageRating_i, filter.ageRating_e),
- Triple(FilterField.Genres, filter.genres_i, filter.genres_e),
- Triple(FilterField.CollectionTags, filter.collections_i, filter.collections_e),
- )
- filter_dto.addContainsNotTriple(containsAndNotTriplets)
- // Fields that have must contains statements
- val peoplePairs = listOf(
-
- Pair(FilterField.Writers, filter.peopleWriters),
- Pair(FilterField.Penciller, filter.peoplePenciller),
- Pair(FilterField.Inker, filter.peopleInker),
- Pair(FilterField.Colorist, filter.peopleCharacter),
- Pair(FilterField.Letterer, filter.peopleLetterer),
- Pair(FilterField.CoverArtist, filter.peopleCoverArtist),
- Pair(FilterField.Editor, filter.peopleEditor),
- Pair(FilterField.Publisher, filter.peoplePublisher),
- Pair(FilterField.Characters, filter.peopleCharacter),
- Pair(FilterField.Translators, filter.peopleTranslator),
-
- Pair(FilterField.PublicationStatus, filter.pubStatus),
- )
- filter_dto.addPeople(peoplePairs)
-
- // Customized statements
- filter_dto.addStatement(FilterComparison.Contains, FilterField.Formats, filter.formats)
- filter_dto.addStatement(FilterComparison.Matches, FilterField.SeriesName, filter.seriesNameQuery)
- // Hardcoded statement to filter out epubs:
- filter_dto.addStatement(FilterComparison.NotContains, FilterField.Formats, "3")
- if (filter.readStatus.isNotEmpty()) {
- filter.readStatus.forEach {
- if (it == "notRead") {
- filter_dto.addStatement(FilterComparison.Equal, FilterField.ReadProgress, "0")
- } else if (it == "inProgress") {
- filter_dto.addStatement(FilterComparison.GreaterThan, FilterField.ReadProgress, "0")
- filter_dto.addStatement(FilterComparison.LessThan, FilterField.ReadProgress, "100")
- } else if (it == "read") {
- filter_dto.addStatement(FilterComparison.Equal, FilterField.ReadProgress, "100")
- }
- }
- }
- // todo: check statement
- // filter_dto.addStatement(FilterComparison.GreaterThanEqual, FilterField.UserRating, filter.userRating.toString())
- if (filter.releaseYearRangeMin != 0) {
- filter_dto.addStatement(FilterComparison.GreaterThan, FilterField.ReleaseYear, filter.releaseYearRangeMin.toString())
- }
-
- if (filter.releaseYearRangeMax != 0) {
- filter_dto.addStatement(FilterComparison.LessThan, FilterField.ReleaseYear, filter.releaseYearRangeMax.toString())
- }
- return json.encodeToJsonElement(filter_dto).toString()
- }
-
- class LoginErrorException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) {
- constructor(cause: Throwable) : this(null, cause)
- }
-
- class OpdsurlExistsInPref(message: String? = null, cause: Throwable? = null) : Exception(message, cause) {
- constructor(cause: Throwable) : this(null, cause)
- }
-
- class EmptyRequestBody(message: String? = null, cause: Throwable? = null) : Exception(message, cause) {
- constructor(cause: Throwable) : this(null, cause)
- }
-
- class LoadingFilterFailed(message: String? = null, cause: Throwable? = null) : Exception(message, cause) {
- constructor(cause: Throwable) : this(null, cause)
- }
-
- override fun headersBuilder(): Headers.Builder {
- if (jwtToken.isEmpty()) {
- doLogin()
- if (jwtToken.isEmpty()) throw LoginErrorException(helper.intl["login_errors_header_token_empty"])
- }
- return Headers.Builder()
- .add("User-Agent", "Tachiyomi Kavita v${AppInfo.getVersionName()}")
- .add("Content-Type", "application/json")
- .add("Authorization", "Bearer $jwtToken")
- }
-
- private fun setupLoginHeaders(): Headers.Builder {
- return Headers.Builder()
- .add("User-Agent", "Tachiyomi Kavita v${AppInfo.getVersionName()}")
- .add("Content-Type", "application/json")
- .add("Authorization", "Bearer $jwtToken")
- }
-
- override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
- val opdsAddressPref = screen.editTextPreference(
- ADDRESS_TITLE,
- "OPDS url",
- "",
- helper.intl["pref_opds_summary"],
- )
- val enabledFiltersPref = MultiSelectListPreference(screen.context).apply {
- key = KavitaConstants.toggledFiltersPref
- title = helper.intl["pref_filters_title"]
- summary = helper.intl["pref_filters_summary"]
- entries = KavitaConstants.filterPrefEntries
- entryValues = KavitaConstants.filterPrefEntriesValue
- setDefaultValue(KavitaConstants.defaultFilterPrefEntries)
- setOnPreferenceChangeListener { _, newValue ->
- @Suppress("UNCHECKED_CAST")
- val checkValue = newValue as Set
- preferences.edit()
- .putStringSet(KavitaConstants.toggledFiltersPref, checkValue)
- .commit()
- }
- }
- val customSourceNamePref = EditTextPreference(screen.context).apply {
- key = KavitaConstants.customSourceNamePref
- title = helper.intl["pref_customsource_title"]
- summary = helper.intl["pref_edit_customsource_summary"]
- setOnPreferenceChangeListener { _, newValue ->
- val res = preferences.edit()
- .putString(KavitaConstants.customSourceNamePref, newValue.toString())
- .commit()
- Toast.makeText(
- screen.context,
- helper.intl["restartapp_settings"],
- 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 opdsUrlInPref = opdsUrlInPreferences(newValue.toString()) // We don't allow hot have multiple sources with same ip or domain
- if (opdsUrlInPref.isNotEmpty()) {
- // TODO("Add option to allow multiple sources with same url at the cost of tracking")
- preferences.edit().putString(title, "").apply()
-
- Toast.makeText(
- context,
- helper.intl["pref_opds_duplicated_source_url"] + ": " + opdsUrlInPref,
- Toast.LENGTH_LONG,
- ).show()
- throw OpdsurlExistsInPref(helper.intl["pref_opds_duplicated_source_url"] + opdsUrlInPref)
- }
-
- val res = preferences.edit().putString(title, newValue as String).commit()
- Toast.makeText(
- context,
- helper.intl["restartapp_settings"],
- Toast.LENGTH_LONG,
- ).show()
- setupLogin(newValue)
- Log.v(LOG_TAG, "[Preferences] Successfully modified OPDS URL")
- res
- } catch (e: OpdsurlExistsInPref) {
- Log.e(LOG_TAG, "Url exists in a different sourcce")
- false
- } catch (e: Exception) {
- Log.e(LOG_TAG, "Unrecognised error", e)
- false
- }
- }
- }
- }
-
- private fun getPrefBaseUrl(): String = preferences.getString("BASEURL", "")!!
- private fun getPrefApiUrl(): String = preferences.getString("APIURL", "")!!
- private fun getPrefKey(): String = preferences.getString("APIKEY", "")!!
- 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
- }
-
- private fun getPrefApiKey(): String {
- // http(s)://host:(port)/api/opds/api-key
- val existingKey = preferences.getString("APIKEY", "")
- return existingKey!!.ifEmpty { preferences.getString(ADDRESS_TITLE, "")!!.split("/opds/")[1] }
- }
-
- companion object {
- private const val ADDRESS_TITLE = "Address"
- private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
- }
-
- /*
- * LOGIN
- **/
- /**
- * Used to check if a url is configured already in any of the sources
- * This is a limitation needed for tracking.
- * **/
- private fun opdsUrlInPreferences(url: String): String {
- fun getCleanedApiUrl(url: String): String = "${url.split("/api/").first()}/api"
-
- for (sourceId in 1..3) { // There's 3 sources so 3 preferences to check
- val sourceSuffixID by lazy {
- val key = "${"kavita_$sourceId"}/all/1" // Hardcoded versionID to 1
- 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
- }
- val preferences: SharedPreferences by lazy {
- Injekt.get().getSharedPreferences("source_$sourceSuffixID", 0x0000)
- }
- val prefApiUrl = preferences.getString("APIURL", "")!!
-
- if (prefApiUrl.isNotEmpty()) {
- if (prefApiUrl == getCleanedApiUrl(url)) {
- if (sourceId.toString() != suffix) {
- return preferences.getString(KavitaConstants.customSourceNamePref, sourceId.toString())!!
- }
- }
- }
- }
- return ""
- }
-
- private fun setupLogin(addressFromPreference: String = "") {
- Log.v(LOG_TAG, "[Setup Login] Starting setup")
- val validAddress = address.ifEmpty { addressFromPreference }
- val tokens = validAddress.split("/api/opds/")
- val apiKey = tokens[1]
- val baseUrlSetup = tokens[0].replace("\n", "\\n")
-
- if (baseUrlSetup.toHttpUrlOrNull() == null) {
- Log.e(LOG_TAG, "Invalid URL $baseUrlSetup")
- throw Exception("""${helper.intl["login_errors_invalid_url"]}: $baseUrlSetup""")
- }
- preferences.edit().putString("BASEURL", baseUrlSetup).apply()
- preferences.edit().putString("APIKEY", apiKey).apply()
- preferences.edit().putString("APIURL", "$baseUrlSetup/api").apply()
- 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(helper.intl["pref_opds_must_setup_address"])
- }
- if (address.split("/opds/").size != 2) {
- throw IOException(helper.intl["pref_opds_badformed_url"])
- }
- if (jwtToken.isEmpty()) setupLogin()
- Log.v(LOG_TAG, "[Login] Starting login")
- val request = POST(
- "$apiUrl/Plugin/authenticate?apiKey=${getPrefKey()}&pluginName=Tachiyomi-Kavita",
- setupLoginHeaders().build(),
- "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()),
- )
- client.newCall(request).execute().use {
- val peekbody = it.peekBody(Long.MAX_VALUE).toString()
-
- if (it.code == 200) {
- try {
- jwtToken = it.parseAs().token
- isLogged = true
- } catch (e: Exception) {
- Log.e(LOG_TAG, "Possible outdated kavita", e)
- throw IOException(helper.intl["login_errors_parse_tokendto"])
- }
- } else {
- if (it.code == 500) {
- Log.e(LOG_TAG, "[LOGIN] login failed. There was some error -> Code: ${it.code}.Response message: ${it.message} Response body: $peekbody.")
- throw LoginErrorException(helper.intl["login_errors_failed_login"])
- } else {
- Log.e(LOG_TAG, "[LOGIN] login failed. Authentication was not successful -> Code: ${it.code}.Response message: ${it.message} Response body: $peekbody.")
- throw LoginErrorException(helper.intl["login_errors_failed_login"])
- }
- }
- }
- Log.v(LOG_TAG, "[Login] Login successful")
- }
-
- init {
- if (apiUrl.isNotBlank()) {
- Single.fromCallable {
- // Login
- doLogin()
- try { // Get current version
- val requestUrl = "$apiUrl/Server/server-info"
- val serverInfoDto = client.newCall(GET(requestUrl, headersBuilder().build()))
- .execute()
- .parseAs()
- Log.e(
- LOG_TAG,
- "Extension version: code=${AppInfo.getVersionCode()} name=${AppInfo.getVersionName()}" +
- " - - Kavita version: ${serverInfoDto.kavitaVersion} - - Lang:${Locale.getDefault()}",
- ) // this is not a real error. Using this so it gets printed in dump logs if there's any error
- } catch (e: EmptyRequestBody) {
- Log.e(LOG_TAG, "Extension version: code=${AppInfo.getVersionCode()} - name=${AppInfo.getVersionName()}")
- } catch (e: Exception) {
- Log.e(LOG_TAG, "Tachiyomi version: code=${AppInfo.getVersionCode()} - name=${AppInfo.getVersionName()}", e)
- }
- try { // Load Filters
- // Genres
- Log.v(LOG_TAG, "[Filter] Fetching filters ")
- client.newCall(GET("$apiUrl/Metadata/genres", headersBuilder().build()))
- .execute().use { response ->
-
- genresListMeta = try {
- response.body.use { json.decodeFromString(it.string()) }
- } catch (e: Exception) {
- Log.e(LOG_TAG, "[Filter] Error decoding JSON for genres filter -> ${response.body}", e)
- emptyList()
- }
- }
- // tagsListMeta
- client.newCall(GET("$apiUrl/Metadata/tags", headersBuilder().build()))
- .execute().use { response ->
- tagsListMeta = try {
- response.body.use { json.decodeFromString(it.string()) }
- } catch (e: Exception) {
- Log.e(LOG_TAG, "[Filter] Error decoding JSON for tagsList filter", e)
- emptyList()
- }
- }
- // age-ratings
- client.newCall(GET("$apiUrl/Metadata/age-ratings", headersBuilder().build()))
- .execute().use { response ->
- ageRatingsListMeta = try {
- response.body.use { json.decodeFromString(it.string()) }
- } catch (e: Exception) {
- Log.e(
- LOG_TAG,
- "[Filter] Error decoding JSON for age-ratings filter",
- e,
- )
- emptyList()
- }
- }
- // collectionsListMeta
- client.newCall(GET("$apiUrl/Collection", headersBuilder().build()))
- .execute().use { response ->
- collectionsListMeta = try {
- response.body.use { json.decodeFromString(it.string()) }
- } catch (e: Exception) {
- Log.e(
- LOG_TAG,
- "[Filter] Error decoding JSON for collectionsListMeta filter",
- e,
- )
- emptyList()
- }
- }
- // languagesListMeta
- client.newCall(GET("$apiUrl/Metadata/languages", headersBuilder().build()))
- .execute().use { response ->
- languagesListMeta = try {
- response.body.use { json.decodeFromString(it.string()) }
- } catch (e: Exception) {
- Log.e(
- LOG_TAG,
- "[Filter] Error decoding JSON for languagesListMeta filter",
- e,
- )
- emptyList()
- }
- }
- // libraries
- client.newCall(GET("$apiUrl/Library", headersBuilder().build()))
- .execute().use { response ->
- libraryListMeta = try {
- response.body.use { json.decodeFromString(it.string()) }
- } catch (e: Exception) {
- Log.e(
- LOG_TAG,
- "[Filter] Error decoding JSON for libraries filter",
- e,
- )
- emptyList()
- }
- }
- // peopleListMeta
- client.newCall(GET("$apiUrl/Metadata/people", headersBuilder().build()))
- .execute().use { response ->
- peopleListMeta = try {
- response.body.use { json.decodeFromString(it.string()) }
- } catch (e: Exception) {
- Log.e(
- LOG_TAG,
- "error while decoding JSON for peopleListMeta filter",
- e,
- )
- emptyList()
- }
- }
- client.newCall(GET("$apiUrl/Metadata/publication-status", headersBuilder().build()))
- .execute().use { response ->
- pubStatusListMeta = try {
- response.body.use { json.decodeFromString(it.string()) }
- } catch (e: Exception) {
- Log.e(
- LOG_TAG,
- "error while decoding JSON for publicationStatusListMeta filter",
- e,
- )
- emptyList()
- }
- }
- client.newCall(GET("$apiUrl/filter", headersBuilder().build()))
- .execute().use { response ->
- smartFilters = try {
- response.body.use { json.decodeFromString(it.string()) }
- } catch (e: Exception) {
- Log.e(
- LOG_TAG,
- "error while decoding JSON for smartfilters",
- e,
- )
- emptyList()
- }
- }
- Log.v(LOG_TAG, "[Filter] Successfully loaded metadata tags from server")
- } catch (e: Exception) {
- throw LoadingFilterFailed("Failed Loading Filters", e.cause)
- }
- }
- .subscribeOn(Schedulers.io())
- .observeOn(Schedulers.io())
- .subscribe(
- {},
- { tr ->
- // Avoid polluting logs with traces of exception
- if (tr is EmptyRequestBody || tr is LoginErrorException) {
- Log.e(LOG_TAG, "error while doing initial calls\n${tr.cause}")
- return@subscribe
- }
- if (tr is ConnectException) { // avoid polluting logs with traces of exception
- Log.e(LOG_TAG, "Error while doing initial calls\n${tr.cause}")
- return@subscribe
- }
- 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
deleted file mode 100644
index 24f284410..000000000
--- a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaConstants.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-package eu.kanade.tachiyomi.extension.all.kavita
-
-object KavitaConstants {
- // toggle 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",
- "ReleaseYearRange",
- )
- 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",
- "ReleaseYearRange",
- )
- 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",
- "ReleaseYearRange",
- )
-
- const val customSourceNamePref = "customSourceName"
- const val noSmartFilterSelected = "No smart filter loaded"
-}
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
deleted file mode 100644
index 7639b6e53..000000000
--- a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaFactory.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-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