Optimizations/Upgrades from the old code

This commit is contained in:
Mio.
2025-07-15 05:36:49 +02:00
parent 67704dce6d
commit 00d1762ff0
5 changed files with 577 additions and 574 deletions

View File

@@ -1,32 +1,107 @@
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." # Authentication #
login_errors_invalid_url=Invalid URL: # ================ #
login_errors_parse_tokendto=There was an error parsing the auth token login_errors_failed_login=Authentication failed.\nPlease check your credentials
pref_customsource_title=Displayed name for source login_errors_header_token_empty=Authorization token missing\nPlease open the extension first to generate a token
pref_group_tags_title=Group Tags (fork must support grouping) login_errors_invalid_url=Invalid server URL:
pref_group_tags_summary_on=Will prefix tags with their type login_errors_parse_tokendto=Failed to process authentication token
pref_group_tags_summary_off=List all tags together http_errors_401=Authentication expired\nPlease log in again
pref_last_volume_cover_title=Keep cover updated http_errors_500=Server error occurred
pref_last_volume_cover_summary_off=Keep current cover
# ============== #
# Preferences #
# ============== #
pref_customsource_title=Source Display Name
pref_edit_customsource_summary=Customize how this Kavita instance appears in your library\nExample: "Home Server" or "Cloud Instance"
pref_group_tags_title=Tag Grouping (fork must support grouping)
pref_group_tags_summary_on=Show tags with category prefixes
pref_group_tags_summary_off=Display tags as flat list
pref_last_volume_cover_title=Keep Cover Updated
pref_last_volume_cover_summary_off=Keep series cover
pref_last_volume_cover_summary_on=Always show current Volume/Issue cover on refresh pref_last_volume_cover_summary_on=Always show current Volume/Issue cover on refresh
pref_show_epub_title=Show EPUB (EPUB can't be read in Mihon)
pref_show_epub_summary_off=Hide EPUB titles in browsing pref_show_epub_title=EPUB Visibility
pref_show_epub_summary_on=Show EPUB titles in browsing pref_show_epub_summary_off=Hide EPUB entries
pref_show_epub_summary_on=Show EPUB entries (unreadable in Mihon)
pref_rd_date_title=Reading List: Display Release Date pref_rd_date_title=Reading List: Display Release Date
pref_rd_date_summary_off=Do not display the release date pref_rd_date_summary_off=Hide publication dates
pref_rd_date_summary_on=Display the release date | !! Once you toggle it on, if you toggle it off again, it will stays as N/A !! pref_rd_date_summary_on=Show publication dates\nNote: Once you toggle it on, if you toggle it off again, it will stays as N/A.
pref_reset_title=Reset All Settings
pref_reset_summary=Clear all preferences for this Kavita instance (Only affect the selected instance) pref_reset_title=Reset Settings
pref_edit_customsource_summary=Here you can change this source name.\nYou can write a descriptive name to identify this OPDS URL. pref_reset_summary=Clear all preferences for this Kavita instance
pref_filters_summary=Show these filters in the filter list
pref_filters_title=Default filters shown pref_filters_title=Default Filters
pref_opds_badformed_url=Incorrect OPDS address. Please copy it from User settings \u2192 3rd party apps \u2192 OPDS URL pref_filters_summary=Select which filters appear by default
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. # OPDS Setup #
restartapp_settings=Restart Mihon to apply new setting. # ============== #
version_exceptions_chapters_parse=Unhandled exception parsing chapters. Send your logs to the Kavita devs. pref_opds_must_setup_address=OPDS URL required for connection
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. pref_opds_badformed_url=Invalid OPDS address\nCopy from: User Settings > API Key / OPDS > OPDS URL (settings#clients)
version_exceptions_smart_filter=Could not decode SmartFilter. Ensure you are using Kavita version 0.7.11 or later. pref_opds_duplicated_source_url=URL already used in another source:
http_errors_500=Something went wrong pref_opds_summary=Complete OPDS URL including API key\nFormat: http://address:port/opds?apiKey=XXX
http_errors_401=There was an error logging in. Try again or reload the app
# ============== #
# System Messages #
# ============== #
restartapp_settings=Restart Mihon to apply changes
check_version=Requires Kavita v0.8.7+\nIf issues persist, contact support with logs
version_exceptions_chapters_parse=Chapter parsing error\nPlease report with logs
version_exceptions_smart_filter=Smart Filter error\nRequires Kavita v0.7.11+
# ============== #
# Filters #
# ============== #
# Filter headers
filters_special=These filters can't be combined with other filters
filters_people=People Filters
filters_special_list=Special Lists
filters_smart_filters=Smart Filters
# @todo
# Basic filters
filters_user_rating=User Rating
filters_status=Status
filters_sort=Sort By
filters_release_year=Release Year
filters_genres=Genres
filters_tags=Tags
filters_age_ratings=Age Ratings
filters_collections=Collections
filters_languages=Languages
filters_libraries=Libraries
filters_pub_status=Publication Status
# People filters
filters_writers=Writers
filters_pencillers=Pencillers
filters_inkers=Inkers
filters_colorists=Colorists
filters_letterers=Letterers
filters_cover_artists=Cover Artists
filters_editors=Editors
filters_publishers=Publishers
filters_characters=Characters
filters_translators=Translators
# Status options
filters_status_any=Any
filters_status_unread=Unread
filters_status_in_progress=In Progress
filters_status_read=Read
# Sort options
filters_sort_title=Title
filters_sort_rating=Rating
filters_sort_date_added=Date Added
filters_sort_release_date=Release Date
filters_sort_read_progress=Read Progress
# Error messages
filters_error_loading=Failed to load filters
filters_empty_list=No filters available

View File

@@ -1,18 +1,107 @@
version_exceptions_chapters_parse=Exception non trait\u00E9e durant l'analyse des chapitres. Envoyez les logs aux d\u00E9velopeurs de Kavita. # ================ #
pref_customsource_title=Nom d'affichage pour la source # Authentification #
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. login_errors_failed_login=Échec de l'authentification. Vérifiez vos identifiants
pref_filters_summary=Afficher ces filtres dans la liste des filtres login_errors_header_token_empty=Token d'autorisation manquant\nOuvrez d'abord l'extension pour générer un token
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 login_errors_invalid_url=URL du serveur invalide :
pref_filters_title=Filtres par d\u00E9faut affich\u00E9s login_errors_parse_tokendto=Échec du traitement du token d'authentification
pref_edit_customsource_summary=Ici vous pouvez changer ce nom source.\nVous pouvez \u00E9crire un nom descriptif pour identifier cette URL opds http_errors_401=Authentification expirée. Veuillez vous reconnecter
pref_opds_badformed_url=L'adresse OPDS n'est pas correcte. Veuillez la copier \u00E0 partir des param\u00E8tres de l'utilisateur - > Applis tierces -> URL OPDS http_errors_500=Erreur du serveur
login_errors_parse_tokendto=Il y a eu une erreur pendant l'analyse du jeton d'authentification
restartapp_settings=Red\u00E9marrez Mihon pour appliquer le nouveau r\u00E9glage. # ============== #
pref_opds_duplicated_source_url=L'URL est configur\u00E9e dans une autre source -> # Préférences #
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 pref_customsource_title=Nom d'affichage de la source
http_errors_500=Quelque chose s'est mal pass\u00E9 pref_edit_customsource_summary=Personnalisez l'affichage de cette instance Kavita\nExemple : "Serveur Maison" ou "Instance Cloud"
login_errors_header_token_empty=\u00AB\u00A0Erreur\u202F: le token jwt est vide.\nEssayez d'abord d'ouvrir l'extension\u00A0\u00BB
login_errors_invalid_url=URL invalide\u202F: pref_group_tags_title=Groupement des tags (fork doit supporter le groupement)
http_errors_401=Il y a eu une erreur. Essayez de nouveau ou relancez l'application pref_group_tags_summary_on=Afficher les tags avec préfixes
pref_group_tags_summary_off=Afficher les tags en liste plate
pref_last_volume_cover_title=Garder la couverture à jour
pref_last_volume_cover_summary_off=Conserver la couverture de la série
pref_last_volume_cover_summary_on=Toujours afficher la dernière couverture
pref_show_epub_title=Visibilité EPUB
pref_show_epub_summary_off=Masquer les EPUB
pref_show_epub_summary_on=Afficher les EPUB (illisibles dans Mihon)
pref_rd_date_title=Listes de Lecture: Affichage date de publication
pref_rd_date_summary_off=Masquer les dates de publication
pref_rd_date_summary_on=Afficher les dates de publication\nNote : Désactiver affichera toujours "N/A" après activation
pref_reset_title=Réinitialisation
pref_reset_summary=Effacer toutes les préférences pour cette instance Kavita
pref_filters_title=Filtres par défaut
pref_filters_summary=Sélectionnez les filtres visibles par défaut
# ============== #
# Configuration OPDS #
# ============== #
pref_opds_must_setup_address=URL OPDS requise pour la connexion
pref_opds_badformed_url=URL OPDS invalide\nCopiez depuis : Paramètres > Clé API / OPDS > URL OPDS
pref_opds_duplicated_source_url=URL déjà utilisée dans une autre source :
pref_opds_summary=URL OPDS complète incluant la clé API\nFormat : http://adresse:port/opds?apiKey=XXX
# ============== #
# Messages système #
# ============== #
restartapp_settings=Redémarrez Mihon pour appliquer les changements
check_version=Nécessite Kavita v0.8.7+\nSi problème persiste, contactez le support avec les logs
version_exceptions_chapters_parse=Erreur d'analyse des chapitres\nMerci de rapporter avec les logs
version_exceptions_smart_filter=Erreur Smart Filter\nNécessite Kavita v0.7.11+
# ============== #
# Filtres #
# ============== #
# Filter headers
filters_special=Ces filtres ne peuvent pas être combinés avec d'autres filtres
filters_people=Filtres Personnalités
# Special filters
filters_special_list=Listes Spéciales
filters_smart_filters=Filtres Intelligents
# Basic filters
filters_user_rating=Note Utilisateur
filters_status=Statut
filters_sort=Trier Par
filters_release_year=Année de Sortie
filters_genres=Genres
filters_tags=Étiquettes
filters_age_ratings=Classifications d'Âge
filters_collections=Collections
filters_languages=Langues
filters_libraries=Bibliothèques
filters_pub_status=Statut de Publication
# People filters
filters_writers=Auteur
filters_pencillers=Dessinateurs
filters_inkers=Encreurs
filters_colorists=Coloristes
filters_letterers=Lettrists
filters_cover_artists=Artistes de Couverture
filters_editors=Éditeurs
filters_publishers=Éditeurs
filters_characters=Personnalités
filters_translators=Traducteurs
# Status options
filters_status_any=Tous
filters_status_unread=Non Lu
filters_status_in_progress=En Cours
filters_status_read=Lu
# Sort options
filters_sort_title=Titre
filters_sort_rating=Note
filters_sort_date_added=Date d'Ajout
filters_sort_release_date=Date de Sortie
filters_sort_read_progress=Progrès de Lecture
# Error messages
filters_error_loading=Échec du chargement des filtres
filters_empty_list=Aucun filtre disponible

View File

@@ -3,25 +3,6 @@ package eu.kanade.tachiyomi.extension.all.kavita
import eu.kanade.tachiyomi.extension.all.kavita.KavitaConstants.noSmartFilterSelected import eu.kanade.tachiyomi.extension.all.kavita.KavitaConstants.noSmartFilterSelected
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
class UserRating : Filter.Select<Int>(
"Minimum Rating",
arrayOf(
"Any",
"1 star",
"2 stars",
"3 stars",
"4 stars",
"5 stars",
).mapIndexed { index, _ -> index }.toTypedArray(),
) {
override fun toString(): String {
return when (state) {
0 -> "Any"
else -> "$state star${if (state > 1) "s" else ""}"
}
}
}
class SmartFiltersFilter(smartFilters: Array<String>) : class SmartFiltersFilter(smartFilters: Array<String>) :
Filter.Select<String>("Smart Filters", arrayOf(noSmartFilterSelected) + smartFilters) Filter.Select<String>("Smart Filters", arrayOf(noSmartFilterSelected) + smartFilters)
class SortFilter(sortables: Array<String>) : Filter.Sort("Sort by", sortables, Selection(0, true)) class SortFilter(sortables: Array<String>) : Filter.Sort("Sort by", sortables, Selection(0, true))
@@ -38,8 +19,8 @@ val sortableList = listOf(
Pair("Random", 9), Pair("Random", 9),
) )
class SpecialListFilter : Filter.Select<String>( class SpecialListFilter(name: String = "Special Lists") : Filter.Select<String>(
"Special Lists", name,
arrayOf("None", "Want to Read", "Reading Lists"), arrayOf("None", "Want to Read", "Reading Lists"),
) )
@@ -47,9 +28,11 @@ class StatusFilter : Filter.Select<String>(
"Status", "Status",
arrayOf("Any", "Unread", "In Progress", "Read"), arrayOf("Any", "Unread", "In Progress", "Read"),
) )
class StatusSeparator :
Filter.Separator()
class StatusFilterGroup : Filter.Group<StatusFilter>( class StatusFilterGroup(name: String = "Status") : Filter.Group<StatusFilter>(
"Status", name,
listOf(StatusFilter()), listOf(StatusFilter()),
) )
@@ -64,7 +47,8 @@ class GenreFilterGroup(genres: List<GenreFilter>) :
Filter.Group<GenreFilter>("Genres", genres) Filter.Group<GenreFilter>("Genres", genres)
class TagFilter(name: String) : Filter.TriState(name) class TagFilter(name: String) : Filter.TriState(name)
class TagFilterGroup(tags: List<TagFilter>) : Filter.Group<TagFilter>("Tags", tags) class TagFilterGroup(tags: List<TagFilter>) :
Filter.Group<TagFilter>("Tags", tags)
class AgeRatingFilter(name: String) : Filter.TriState(name) class AgeRatingFilter(name: String) : Filter.TriState(name)
class AgeRatingFilterGroup(ageRatings: List<AgeRatingFilter>) : class AgeRatingFilterGroup(ageRatings: List<AgeRatingFilter>) :
@@ -90,8 +74,29 @@ class PubStatusFilter(name: String) : Filter.CheckBox(name, false)
class PubStatusFilterGroup(status: List<PubStatusFilter>) : class PubStatusFilterGroup(status: List<PubStatusFilter>) :
Filter.Group<PubStatusFilter>("Publication Status", status) Filter.Group<PubStatusFilter>("Publication Status", status)
class UserRating : Filter.Select<Int>(
"Minimum Rating",
arrayOf(
"Any",
"1 star",
"2 stars",
"3 stars",
"4 stars",
"5 stars",
).mapIndexed { index, _ -> index }.toTypedArray(),
) {
override fun toString(): String {
return when (state) {
0 -> "Any"
else -> "$state star${if (state > 1) "s" else ""}"
}
}
}
class UserRatingSeparator :
Filter.Separator()
class PeopleHeaderFilter(name: String) : class PeopleHeaderFilter(name: String) :
Filter.Header(name) Filter.Header(name.toString())
class PeopleSeparatorFilter : class PeopleSeparatorFilter :
Filter.Separator() Filter.Separator()

View File

@@ -43,7 +43,6 @@ import eu.kanade.tachiyomi.extension.all.kavita.dto.SortOptions
import eu.kanade.tachiyomi.extension.all.kavita.dto.VolumeDto import eu.kanade.tachiyomi.extension.all.kavita.dto.VolumeDto
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
@@ -55,6 +54,14 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
@@ -68,16 +75,11 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import rx.Observable
import rx.Single
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
import java.net.ConnectException
import java.security.MessageDigest import java.security.MessageDigest
import java.util.Locale
class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSource, HttpSource() { class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSource, HttpSource() {
private val helper = KavitaHelper() private val helper = KavitaHelper()
@@ -193,65 +195,40 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
* Custom implementation for fetch popular, latest and search * Custom implementation for fetch popular, latest and search
* Handles and logs errors to provide a more detailed exception to the users. * Handles and logs errors to provide a more detailed exception to the users.
*/ */
private fun fetch(request: Request): Observable<MangasPage> { private suspend fun fetch(request: Request): MangasPage = withContext(Dispatchers.IO) {
return client.newCall(request)
.asObservableSuccess()
.onErrorResumeNext { throwable ->
val field = throwable.javaClass.getDeclaredField("code")
field.isAccessible = true
try { try {
var code = field.get(throwable) val response = client.newCall(request).execute()
Log.e(LOG_TAG, "Error fetching manga: ${throwable.message}", throwable) if (!response.isSuccessful) {
if (code as Int !in intArrayOf(401, 201, 500)) { val code = response.code
code = 500 throw IOException("Http Error: $code\n ${helper.intl["http_errors_$code"]}\n${helper.intl["check_version"]}")
} }
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) popularMangaParse(response)
} catch (e: Exception) {
Log.e(LOG_TAG, "Error fetching manga", e)
throw e
} }
} }
@Deprecated( override suspend fun getPopularManga(page: Int): MangasPage = withContext(Dispatchers.IO) {
"Use the non-RxJava API instead", try {
replaceWith = ReplaceWith("getPopularManga(page)"), val response = client.newCall(popularMangaRequest(page)).execute()
)
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newCall(popularMangaRequest(page))
.asObservableSuccess()
.map { response ->
if (!response.isSuccessful) { if (!response.isSuccessful) {
throw IOException("HTTP error ${response.code}") throw IOException("HTTP error ${response.code}")
} }
popularMangaParse(response) popularMangaParse(response)
} } catch (e: Exception) {
.onErrorReturn { error -> Log.e(LOG_TAG, "Error fetching popular manga", e)
Log.e(LOG_TAG, "Error fetching popular manga", error)
MangasPage(emptyList(), false) MangasPage(emptyList(), false)
} }
} }
@Deprecated( override suspend fun getLatestUpdates(page: Int): MangasPage = fetch(latestUpdatesRequest(page))
"Use the non-RxJava API instead",
replaceWith = ReplaceWith("getLatestUpdates(page)"),
)
override fun fetchLatestUpdates(page: Int) =
fetch(latestUpdatesRequest(page))
@Deprecated( override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage {
"Use the non-RxJava API instead",
replaceWith = ReplaceWith("getSearchManga(page, query, filters)"),
)
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
val specialListFilter = filters.find { it is SpecialListFilter } as? SpecialListFilter val specialListFilter = filters.find { it is SpecialListFilter } as? SpecialListFilter
return when (specialListFilter?.state) { return when (specialListFilter?.state) {
1 -> { 1 -> { // Want to Read selected
// Want to Read selected
val payload = """{ val payload = """{
"id": 0, "id": 0,
"name": "", "name": "",
@@ -268,14 +245,12 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
headersBuilder().build(), headersBuilder().build(),
payload.toRequestBody(JSON_MEDIA_TYPE), payload.toRequestBody(JSON_MEDIA_TYPE),
) )
return client.newCall(request) val response = client.newCall(request).execute()
.asObservableSuccess() popularMangaParse(response)
.map { popularMangaParse(it) }
} }
2 -> { // Reading Lists 2 -> { // Reading Lists
client.newCall(readingListRequest()) val response = client.newCall(readingListRequest()).execute()
.asObservableSuccess() readingListParse(response)
.map { readingListParse(it) }
} }
else -> { // Regular searches else -> { // Regular searches
fetch(searchMangaRequest(page, query, filters)) fetch(searchMangaRequest(page, query, filters))
@@ -283,15 +258,11 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
} }
} }
@Deprecated( override suspend fun getChapterList(manga: SManga): List<SChapter> {
"Use the non-RxJava API instead",
replaceWith = ReplaceWith("getChapterList(manga)"),
)
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return if (manga.url.contains("/ReadingList/")) { return if (manga.url.contains("/ReadingList/")) {
fetchReadingListItems(manga) fetchReadingListItems(manga)
} else { } else {
super.fetchChapterList(manga) super.getChapterList(manga)
} }
} }
@@ -299,7 +270,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
try { try {
val result = response.parseAs<List<SeriesDto>>() val result = response.parseAs<List<SeriesDto>>()
series = result series = result
val mangaList = result.map { item -> helper.createSeriesDto(item, apiUrl, baseUrl, apiKey) } val mangaList = result.map { item -> helper.createSeriesDto(item, apiUrl, baseUrl) }
return MangasPage(mangaList, helper.hasNextPage(response)) return MangasPage(mangaList, helper.hasNextPage(response))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(LOG_TAG, "Unhandled exception", e) Log.e(LOG_TAG, "Unhandled exception", e)
@@ -431,7 +402,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
is Filter.Group<*> -> { is Filter.Group<*> -> {
when (filter) { when (filter) {
is StatusFilterGroup -> { is StatusFilterGroup -> {
val statusFilter = filter.state.firstOrNull() as? StatusFilter val statusFilter = filter.state.firstOrNull()
statusFilter?.state?.let { selectedIndex -> statusFilter?.state?.let { selectedIndex ->
when (selectedIndex) { when (selectedIndex) {
1 -> { // Unread (0%) 1 -> { // Unread (0%)
@@ -505,24 +476,32 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
} }
is TagFilterGroup -> { is TagFilterGroup -> {
filter.state.forEach { tagFilter -> val included = filter.state
val tag = tagsListMeta.find { it.title == (tagFilter as Filter.TriState).name } .filter { it.state == STATE_INCLUDE }
tag?.let { .mapNotNull { tagFilter ->
when (tagFilter.state) { tagsListMeta.find { it.title == tagFilter.name }?.id?.toString()
STATE_INCLUDE -> filterV2.addStatement( }
val excluded = filter.state
.filter { it.state == STATE_EXCLUDE }
.mapNotNull { tagFilter ->
tagsListMeta.find { it.title == tagFilter.name }?.id?.toString()
}
if (included.isNotEmpty()) {
filterV2.addStatement(
FilterComparison.Contains, FilterComparison.Contains,
FilterField.Tags, FilterField.Tags,
tag.id.toString(), included.joinToString(","),
) )
STATE_EXCLUDE -> filterV2.addStatement( }
if (excluded.isNotEmpty()) {
filterV2.addStatement(
FilterComparison.NotContains, FilterComparison.NotContains,
FilterField.Tags, FilterField.Tags,
tag.id.toString(), excluded.joinToString(","),
) )
} }
} }
}
}
is AgeRatingFilterGroup -> { is AgeRatingFilterGroup -> {
val included = filter.state val included = filter.state
.filter { it.state == STATE_INCLUDE } .filter { it.state == STATE_INCLUDE }
@@ -802,14 +781,12 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
"Use the non-RxJava API instead", "Use the non-RxJava API instead",
replaceWith = ReplaceWith("getMangaDetails(manga)"), replaceWith = ReplaceWith("getMangaDetails(manga)"),
) )
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override suspend fun getMangaDetails(manga: SManga): SManga {
val serieId = helper.getIdFromUrl(manga.url) val serieId = helper.getIdFromUrl(manga.url)
return if (manga.url.contains("source=readinglist")) { return if (manga.url.contains("source=readinglist")) {
val readingListId = manga.url.substringAfter("readingListId=").substringBefore("&").toIntOrNull() val readingListId = manga.url.substringAfter("readingListId=").substringBefore("&").toIntOrNull()
val now = java.util.Calendar.getInstance() val now = java.util.Calendar.getInstance()
Observable.fromCallable {
val readingList = cachedReadingLists.find { it.id == readingListId } val readingList = cachedReadingLists.find { it.id == readingListId }
if (readingList != null) { if (readingList != null) {
val groupTags = preferences.groupTags val groupTags = preferences.groupTags
@@ -881,25 +858,13 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
this.status = status this.status = status
} }
} else { } else {
manga.apply { manga.apply { initialized = true }
initialized = true
}
}
}.onErrorReturn { error ->
Log.e(LOG_TAG, "Error fetching reading list details", error)
manga.apply {
description = helper.intl["error_loading_reading_list"]
initialized = true
}
} }
} else { } else {
client.newCall(GET("$apiUrl/series/metadata?seriesId=$serieId", headersBuilder().build())) val response = client.newCall(GET("$apiUrl/series/metadata?seriesId=$serieId", headersBuilder().build())).execute()
.asObservableSuccess()
.map { response ->
mangaDetailsParse(response).apply { initialized = true } mangaDetailsParse(response).apply { initialized = true }
} }
} }
}
override fun mangaDetailsRequest(manga: SManga): Request { override fun mangaDetailsRequest(manga: SManga): Request {
val serieId = helper.getIdFromUrl(manga.url) val serieId = helper.getIdFromUrl(manga.url)
@@ -965,9 +930,9 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
.filterNot { it.equals(foundDemographic, ignoreCase = true) } .filterNot { it.equals(foundDemographic, ignoreCase = true) }
.filterNot { tag -> filteredGenres.any { genre -> genre.equals(tag, ignoreCase = true) } } .filterNot { tag -> filteredGenres.any { genre -> genre.equals(tag, ignoreCase = true) } }
val manga = helper.createSeriesDto(existingSeries, apiUrl, baseUrl, apiKey) val manga = helper.createSeriesDto(existingSeries, apiUrl, baseUrl)
manga.title = existingSeries.name manga.title = existingSeries.name
manga.url = "$baseUrl/library/${existingSeries.libraryId}/series/${result.seriesId}" // "$apiUrl/Series/${result.seriesId}" manga.url = "$baseUrl/Series/${result.seriesId}" // "$baseUrl/library/${existingSeries.libraryId}/series/${result.seriesId}"
manga.artist = result.coverArtists.joinToString { it.name } manga.artist = result.coverArtists.joinToString { it.name }
manga.description = listOfNotNull( manga.description = listOfNotNull(
ratingLine.takeIf { it.isNotBlank() }, ratingLine.takeIf { it.isNotBlank() },
@@ -1092,7 +1057,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
.filterNot { it.equals(foundDemographic, ignoreCase = true) } .filterNot { it.equals(foundDemographic, ignoreCase = true) }
.filterNot { tag -> filteredGenres.any { genre -> genre.equals(tag, ignoreCase = true) } } .filterNot { tag -> filteredGenres.any { genre -> genre.equals(tag, ignoreCase = true) } }
url = "$baseUrl/library/${result.getLibraryId(serieDto)}/series/${result.seriesId}" // "$apiUrl/Series/${result.seriesId}" url = "$baseUrl/Series/${result.seriesId}" // "$baseUrl/library/${result.getLibraryId(serieDto)}/series/${result.seriesId}"
artist = result.coverArtists.joinToString { it.name } artist = result.coverArtists.joinToString { it.name }
description = listOfNotNull( description = listOfNotNull(
ratingLine.takeIf { it.isNotBlank() }, ratingLine.takeIf { it.isNotBlank() },
@@ -1173,13 +1138,11 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
} }
} }
private fun fetchReadingListItems(manga: SManga): Observable<List<SChapter>> { private fun fetchReadingListItems(manga: SManga): List<SChapter> {
val readingListId = manga.url.substringAfter("readingListId=").substringBefore("&") val readingListId = manga.url.substringAfter("readingListId=").substringBefore("&")
val request = GET("$apiUrl/ReadingList/items?readingListId=$readingListId", headersBuilder().build()) val request = GET("$apiUrl/ReadingList/items?readingListId=$readingListId", headersBuilder().build())
val response = client.newCall(request).execute()
return client.newCall(request) return parseReadingListItems(response)
.asObservableSuccess()
.map { parseReadingListItems(it) }
} }
private fun parseReadingListItems(response: Response): List<SChapter> { private fun parseReadingListItems(response: Response): List<SChapter> {
@@ -1321,7 +1284,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
try { try {
val seriesDto = client.newCall(GET("$apiUrl/Series/${item.seriesId}", headersBuilder().build())) val seriesDto = client.newCall(GET("$apiUrl/Series/${item.seriesId}", headersBuilder().build()))
.execute().parseAs<SeriesDto>() .execute().parseAs<SeriesDto>()
helper.createSeriesDto(seriesDto, apiUrl, baseUrl, apiKey) helper.createSeriesDto(seriesDto, apiUrl, baseUrl)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(LOG_TAG, "Error fetching series for related reading list", e) Log.e(LOG_TAG, "Error fetching series for related reading list", e)
null null
@@ -1531,11 +1494,10 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
return GET("$apiUrl/$chapterId", headersBuilder().build()) return GET("$apiUrl/$chapterId", headersBuilder().build())
} }
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList(chapter)")) override suspend fun getPageList(chapter: SChapter): List<Page> = withContext(Dispatchers.IO) {
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
// Check if this is a reading list item (has readingListId in URL) // Check if this is a reading list item (has readingListId in URL)
if (chapter.url.contains("readingListId=")) { if (chapter.url.contains("readingListId=")) {
return Observable.fromCallable { try {
// Extract seriesId from the chapter URL // Extract seriesId from the chapter URL
val seriesId = chapter.url.substringAfter("seriesId=").substringBefore("&").toIntOrNull() val seriesId = chapter.url.substringAfter("seriesId=").substringBefore("&").toIntOrNull()
val readingListId = chapter.url.substringAfter("readingListId=").substringBefore("&").toIntOrNull() val readingListId = chapter.url.substringAfter("readingListId=").substringBefore("&").toIntOrNull()
@@ -1560,32 +1522,39 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
} ?: throw IOException("Item not found in reading list") } ?: throw IOException("Item not found in reading list")
// Get item details based on type // Get item details based on type
val chapterDetails = if (listItem.chapterId != null) { val chapterDetails = when {
listItem.chapterId != null -> {
// Standard chapter
val request = GET("$apiUrl/Chapter?chapterId=${listItem.chapterId}", headersBuilder().build()) val request = GET("$apiUrl/Chapter?chapterId=${listItem.chapterId}", headersBuilder().build())
client.newCall(request).execute().parseAs<ChapterDto>() client.newCall(request).execute().parseAs<ChapterDto>()
} else if (listItem.volumeId != null) { }
// For volumes, get all chapters and let user choose listItem.volumeId != null -> {
// Volume - get first chapter
val volumeRequest = GET("$apiUrl/Volume/${listItem.volumeId}", headersBuilder().build()) val volumeRequest = GET("$apiUrl/Volume/${listItem.volumeId}", headersBuilder().build())
val volume = client.newCall(volumeRequest).execute().parseAs<VolumeDto>() val volume = client.newCall(volumeRequest).execute().parseAs<VolumeDto>()
volume.chapters.firstOrNull() ?: throw IOException(helper.intl["error_no_chapters_found"]) volume.chapters.firstOrNull()?.let {
} else { val chapRequest = GET("$apiUrl/Chapter?chapterId=${it.id}", headersBuilder().build())
throw IOException(helper.intl["error_invalid_reading_list_item"]) client.newCall(chapRequest).execute().parseAs<ChapterDto>()
} ?: throw IOException(helper.intl["error_no_chapters_found"])
}
else -> throw IOException(helper.intl["error_invalid_reading_list_item"])
} }
(0 until chapterDetails.pages).map { i -> // Generate pages with consistent URL format
return@withContext (0 until chapterDetails.pages).map { i ->
Page( Page(
index = i, index = i,
imageUrl = "$apiUrl/Reader/image?chapterId=${chapterDetails.id}&page=$i&extractPdf=true&apiKey=$apiKey", imageUrl = "$apiUrl/Reader/image?chapterId=${chapterDetails.id}&page=$i&extractPdf=true&apiKey=$apiKey",
) )
} }
}.onErrorResumeNext { error -> } catch (e: Exception) {
Log.e(LOG_TAG, "Error fetching reading list chapter pages", error) Log.e(LOG_TAG, "Error processing reading list item", e)
Observable.error(error) throw e
} }
} }
// For normal titles // For normal titles
return Observable.fromCallable { try {
// Check if this is a volume URL // Check if this is a volume URL
if (chapter.url.contains("/Volume/") || chapter.url.startsWith("volume_")) { if (chapter.url.contains("/Volume/") || chapter.url.startsWith("volume_")) {
val volumeId = when { val volumeId = when {
@@ -1621,7 +1590,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
} }
} }
(initialPages + remainingPages).toList() return@withContext (initialPages + remainingPages).toList()
} else { } else {
// Original chapter handling // Original chapter handling
val chapterId = when { val chapterId = when {
@@ -1643,23 +1612,23 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
throw IOException("${helper.intl["error_failed_parse_chapter"]}: ${e.message}") throw IOException("${helper.intl["error_failed_parse_chapter"]}: ${e.message}")
} }
(0 until chapterDetails.pages).map { i -> return@withContext (0 until chapterDetails.pages).map { i ->
Page( Page(
index = i, index = i,
imageUrl = "$apiUrl/Reader/image?chapterId=$chapterId&page=$i&extractPdf=true&apiKey=$apiKey", imageUrl = "$apiUrl/Reader/image?chapterId=$chapterId&page=$i&extractPdf=true&apiKey=$apiKey",
) )
} }
} }
}.onErrorResumeNext { error -> } catch (e: Exception) {
// Fallback to using the scanlator field if we can't get chapter details // Fallback to using the scanlator field if we can't get chapter details
Log.e(LOG_TAG, "Error fetching chapter details, using fallback", error) Log.e(LOG_TAG, "Error fetching chapter details, using fallback", e)
val fallbackPageCount = chapter.scanlator?.replace(" pages", "")?.toIntOrNull() ?: 1 val fallbackPageCount = chapter.scanlator?.replace(" pages", "")?.toIntOrNull() ?: 1
(0 until fallbackPageCount).map { i -> return@withContext (0 until fallbackPageCount).map { i ->
Page( Page(
index = i, index = i,
imageUrl = "$apiUrl/Reader/image?chapterId=${chapter.url.substringBefore("_")}&page=$i&extractPdf=true&apiKey=$apiKey", imageUrl = "$apiUrl/Reader/image?chapterId=${chapter.url.substringBefore("_")}&page=$i&extractPdf=true&apiKey=$apiKey",
) )
}.let { Observable.just(it) } }
} }
} }
@@ -1672,7 +1641,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
return try { return try {
val result = response.parseAs<List<SeriesDto>>() val result = response.parseAs<List<SeriesDto>>()
val mangaList = result.map { helper.createSeriesDto(it, apiUrl, baseUrl, apiKey) } val mangaList = result.map { helper.createSeriesDto(it, apiUrl, baseUrl) }
MangasPage(mangaList, false) MangasPage(mangaList, false)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(LOG_TAG, "Error parsing search results", e) Log.e(LOG_TAG, "Error parsing search results", e)
@@ -1701,101 +1670,77 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
private var cachedReadingLists: List<ReadingListDto> = emptyList() private var cachedReadingLists: List<ReadingListDto> = emptyList()
/** /**
* Loads the enabled filters if they are not empty so Mihon can show them to the user * Loads the filters if they are not empty so Mihon can show them to the user
*/ */
override fun getFilterList(): FilterList { private fun getFilterList(
val toggledFilters = getToggledFilters() genres: List<String>,
tags: List<String>,
ageRatings: List<String>,
collections: List<String>,
languages: List<String>,
libraries: List<String>,
people: List<String>,
pubStatus: List<String>,
smartFilters: Array<String>,
): FilterList {
return FilterList(
SortFilter(sortableList.map { it.first }.toTypedArray()),
val filters = try { StatusSeparator(),
val filtersLoaded = mutableListOf<Filter<*>>() StatusFilterGroup(),
filtersLoaded.add(SortFilter(sortableList.map { it.first }.toTypedArray())) // Release year filter
if (smartFilters.isNotEmpty()) {
filtersLoaded.add(SmartFiltersFilter(smartFilters.map { it.name }.toTypedArray()))
}
filtersLoaded.add(SpecialListFilter())
if (toggledFilters.contains("Read Status")) {
filtersLoaded.add(StatusFilterGroup())
}
if (toggledFilters.contains("ReleaseYearRange")) {
filtersLoaded.add(
ReleaseYearRangeGroup( ReleaseYearRangeGroup(
listOf("Min", "Max").map { ReleaseYearRange(it) }, listOf(
ReleaseYearRange("Min"),
ReleaseYearRange("Max"),
),
), ),
)
}
if (genresListMeta.isNotEmpty() and toggledFilters.contains("Genres")) { GenreFilterGroup(genres.map { GenreFilter(it) }),
filtersLoaded.add( TagFilterGroup(tags.map { TagFilter(it) }),
GenreFilterGroup(genresListMeta.map { GenreFilter(it.title) }), CollectionFilterGroup(collections.map { CollectionFilter(it) }),
) LibrariesFilterGroup(libraries.map { LibraryFilter(it) }),
} LanguageFilterGroup(languages.map { LanguageFilter(it) }),
if (tagsListMeta.isNotEmpty() and toggledFilters.contains("Tags")) { PubStatusFilterGroup(pubStatus.map { PubStatusFilter(it) }),
filtersLoaded.add( AgeRatingFilterGroup(ageRatings.map { AgeRatingFilter(it) }),
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",
"Epub",
"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(
listOf(
PubStatusFilter("Ongoing"),
PubStatusFilter("Hiatus"),
PubStatusFilter("Completed"),
PubStatusFilter("Cancelled"),
PubStatusFilter("Ended"),
),
),
)
}
if (userRatingsListMeta.isNotEmpty() and toggledFilters.contains("Rating")) {
filtersLoaded.add(
UserRating(), UserRating(),
UserRatingSeparator(),
// Special filters section
Filter.Header(helper.intl["filters_special"]),
SpecialListFilter(helper.intl["filters_special_list"]),
SmartFiltersFilter(smartFilters),
// @todo People filters (but not as filters, as a text search)
// PeopleHeaderFilter(helper.intl["filters_people"]),
// PeopleSeparatorFilter(),
// WriterPeopleFilterGroup(people.map { WriterPeopleFilter(it) }),
// PencillerPeopleFilterGroup(people.map { PencillerPeopleFilter(it) }),
// InkerPeopleFilterGroup(people.map { InkerPeopleFilter(it) }),
// ColoristPeopleFilterGroup(people.map { ColoristPeopleFilter(it) }),
// LettererPeopleFilterGroup(people.map { LettererPeopleFilter(it) }),
// CoverArtistPeopleFilterGroup(people.map { CoverArtistPeopleFilter(it) }),
// EditorPeopleFilterGroup(people.map { EditorPeopleFilter(it) }),
// PublisherPeopleFilterGroup(people.map { PublisherPeopleFilter(it) }),
// CharacterPeopleFilterGroup(people.map { CharacterPeopleFilter(it) }),
// TranslatorPeopleFilterGroup(people.map { TranslatorPeopleFilter(it) }),
) )
} }
filtersLoaded override fun getFilterList(): FilterList {
} catch (e: Exception) { return getFilterList(
Log.e(LOG_TAG, "[FILTERS] Error while creating filter list", e) genres = genresListMeta.map { it.title },
FilterList(emptyList()) tags = tagsListMeta.map { it.title },
} ageRatings = ageRatingsListMeta.map { it.title },
return FilterList(filters) collections = collectionsListMeta.map { it.title },
languages = languagesListMeta.map { it.title },
libraries = libraryListMeta.map { it.name },
people = peopleListMeta.map { it.name },
pubStatus = pubStatusListMeta.map { it.title },
smartFilters = smartFilters.map { it.name }.toTypedArray(),
)
} }
class LoginErrorException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) { class LoginErrorException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) {
@@ -2179,165 +2124,88 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
Log.v(LOG_TAG, "[Login] Login successful") Log.v(LOG_TAG, "[Login] Login successful")
} }
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
init { init {
if (apiUrl.isNotBlank()) { if (apiUrl.isNotBlank()) {
Single.fromCallable { scope.launch {
doLogin()
try { // Get current version
val requestUrl = "$apiUrl/Server/server-info-slim"
val serverInfoDto = client.newCall(GET(requestUrl, headersBuilder().build()))
.execute()
.parseAs<ServerInfoDto>()
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/libraries", 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 {
val jsonString = response.body.string()
// Log.d(LOG_TAG, "Raw people metadata: $jsonString") // Debug log
json.decodeFromString<List<MetadataPeople>>(jsonString)
.filter { it.role != null } // Filter out entries without role
} catch (e: Exception) {
Log.e(LOG_TAG, "Error decoding people metadata", e)
emptyList()
}
}
client.newCall(GET("$apiUrl/Metadata/publication-status", headersBuilder().build()))
.execute().use { response ->
if (!response.isSuccessful) {
Log.e(LOG_TAG, "Failed to fetch publication status: ${response.code}")
pubStatusListMeta = emptyList()
return@use
}
try { try {
pubStatusListMeta = response.body.use { json.decodeFromString(it.string()) } doLogin()
} catch (e: Exception) {
Log.e(LOG_TAG, "Error decoding publication status JSON", e) // Load all filters in parallel
pubStatusListMeta = emptyList() val deferredFilters = listOf(
} async {
} genresListMeta = loadMetadata("$apiUrl/Metadata/genres")
client.newCall(GET("$apiUrl/filter", headersBuilder().build())) Log.d(LOG_TAG, "Loaded ${genresListMeta.size} genres")
.execute().use { response -> },
smartFilters = try { async {
response.body.use { json.decodeFromString(it.string()) } tagsListMeta = loadMetadata("$apiUrl/Metadata/tags")
} catch (e: Exception) { Log.d(LOG_TAG, "Loaded ${tagsListMeta.size} tags")
Log.e( },
LOG_TAG, async {
"error while decoding JSON for smartfilters", ageRatingsListMeta = loadMetadata("$apiUrl/Metadata/age-ratings")
e, Log.d(LOG_TAG, "Loaded ${ageRatingsListMeta.size} age ratings")
) },
emptyList() async {
} collectionsListMeta = loadMetadata("$apiUrl/Collection")
} Log.d(LOG_TAG, "Loaded ${collectionsListMeta.size} collections")
} catch (e: Exception) { },
throw LoadingFilterFailed("Failed Loading Filters", e.cause) async {
} languagesListMeta = loadMetadata("$apiUrl/Metadata/languages")
} Log.d(LOG_TAG, "Loaded ${languagesListMeta.size} languages")
.subscribeOn(Schedulers.io()) },
.observeOn(Schedulers.io()) async {
.subscribe( libraryListMeta = loadMetadata("$apiUrl/Library/libraries")
{}, Log.d(LOG_TAG, "Loaded ${libraryListMeta.size} libraries")
{ tr -> },
// Avoid polluting logs with traces of exception async {
if (tr is EmptyRequestBody || tr is LoginErrorException) { peopleListMeta = loadMetadata("$apiUrl/Metadata/people")
Log.e(LOG_TAG, "error while doing initial calls\n${tr.cause}") Log.d(LOG_TAG, "Loaded ${peopleListMeta.size} people")
return@subscribe },
} async {
if (tr is ConnectException) { // avoid polluting logs with traces of exception pubStatusListMeta = loadMetadata("$apiUrl/Metadata/publication-status")
Log.e(LOG_TAG, "Error while doing initial calls\n${tr.cause}") Log.d(LOG_TAG, "Loaded ${pubStatusListMeta.size} publication statuses")
return@subscribe },
} async {
Log.e(LOG_TAG, "error while doing initial calls", tr) smartFilters = loadMetadata("$apiUrl/filter")
Log.d(LOG_TAG, "Loaded ${smartFilters.size} smart filters")
}, },
) )
deferredFilters.awaitAll()
// Get server info
val serverInfoDto = client.newCall(GET("$apiUrl/Server/server-info-slim", headersBuilder().build()))
.execute()
.parseAs<ServerInfoDto>()
Log.d(LOG_TAG, "Kavita version: ${serverInfoDto.kavitaVersion}")
} catch (e: Exception) {
Log.e(LOG_TAG, "Initialization error", e)
} }
} }
} }
}
// Add cleanup for the coroutine scope
fun cleanup() {
scope.cancel()
// Clear any cached data if needed
genresListMeta = emptyList()
tagsListMeta = emptyList()
ageRatingsListMeta = emptyList()
peopleListMeta = emptyList()
pubStatusListMeta = emptyList()
languagesListMeta = emptyList()
libraryListMeta = emptyList()
collectionsListMeta = emptyList()
smartFilters = emptyList()
cachedReadingLists = emptyList()
Log.d(LOG_TAG, "Cleanup completed")
}
private inline fun <reified T> loadMetadata(url: String): T {
return client.newCall(GET(url, headersBuilder().build()))
.execute()
.parseAs()
}
}

View File

@@ -47,18 +47,17 @@ class KavitaHelper {
} }
} }
// Rating Providers from Series-Details-Plus // @todo Rating Providers from Series-Details-Plus
private fun getProviderName(provider: Int): String = when (provider) { // private fun getProviderName(provider: Int): String = when (provider) {
0 -> "User" // 0 -> "User"
1 -> "AniList" // 1 -> "AniList"
2 -> "MyAnimeList" // 2 -> "MyAnimeList"
3 -> "MangaUpdates" // 3 -> "MangaUpdates"
else -> "Rating" // else -> "Rating"
} // }
fun createSeriesDto(obj: SeriesDto, baseUrl: String, apiUrl: String, apiKey: String): SManga = fun createSeriesDto(obj: SeriesDto, baseUrl: String, apiKey: String): SManga =
SManga.create().apply { SManga.create().apply {
// url = "$baseUrl/library/${obj.libraryId}/series/${obj.id}"
url = "$baseUrl/Series/${obj.id}" url = "$baseUrl/Series/${obj.id}"
title = obj.name title = obj.name
thumbnail_url = "$baseUrl/image/series-cover?seriesId=${obj.id}&apiKey=$apiKey" thumbnail_url = "$baseUrl/image/series-cover?seriesId=${obj.id}&apiKey=$apiKey"
@@ -102,16 +101,24 @@ class KavitaHelper {
} }
ChapterType.SingleFileVolume -> { ChapterType.SingleFileVolume -> {
cleanChapterTitle( val volumeNumber = formatVolumeNumber(volume)
val cleanVolumeName = volume.name.trim()
// Always use the API's volume number, never parse from title
when { when {
volume.name.any { it.isLetter() } -> "v${formatVolumeNumber(volume)} - ${volume.name}" // If name is empty or just numbers (Kavita default)
else -> "Volume ${formatVolumeNumber(volume)}" cleanVolumeName.isEmpty() || cleanVolumeName.none { it.isLetter() } -> {
}, cleanChapterTitle("Volume $volumeNumber")
ChapterTitleContext( }
mangaTitle = mangaTitle, // If name already contains volume info (Vol. 3, V3, etc.)
volumeName = volume.name, cleanVolumeName.contains(Regex("(?i)(vol|volume|v)[.\\s]*\\d+")) -> {
), cleanChapterTitle(cleanVolumeName)
) }
// Normal case - prefix with volume number
else -> {
cleanChapterTitle("$volumeNumber - $cleanVolumeName")
}
}
} }
ChapterType.Special -> { ChapterType.Special -> {
@@ -163,47 +170,6 @@ class KavitaHelper {
} }
} }
// @todo Keeping this in case we're switching the logic here this after the Mihon PR
// chapter_number = when {
// type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->
// singleFileVolumeNumber.toFloat()
//
// type == ChapterType.SingleFileVolume -> {
// // For standalone volumes, use positive numbers
// volume.minNumber.toFloat()
// }
//
// // For regular chapters
// type != ChapterType.SingleFileVolume && type != ChapterType.Special -> {
// // Handle decimal chapter numbers
// if (chapter.minNumber % 1 != 0.0) {
// chapter.minNumber.toFloat()
// } else {
// chapter.minNumber.toInt().toFloat()
// }
// }
//
// // For volumes/specials in mixed content, place them below chapter 0
// else -> {
// val volumeNum = try {
// if (volume.minNumber % 1 != 0.0) {
// volume.minNumber.toFloat()
// } else {
// volume.minNumber.toInt().toFloat()
// }
// } catch (e: NumberFormatException) {
// 0f
// }
// -volumeNum - KavitaConstants.VOLUME_NUMBER_OFFSET
// }
// }
//
// url = when {
// type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->
// "volume_${volume.id}"
// else -> "chapter_${chapter.id}"
// }
// Handle decimal chapter numbers properly // Handle decimal chapter numbers properly
chapter_number = when { chapter_number = when {
type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null -> type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->