diff --git a/src/all/kavita/assets/i18n/messages_en.properties b/src/all/kavita/assets/i18n/messages_en.properties index ec7fbd8b..893d4664 100644 --- a/src/all/kavita/assets/i18n/messages_en.properties +++ b/src/all/kavita/assets/i18n/messages_en.properties @@ -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." -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_group_tags_title=Group Tags (fork must support grouping) -pref_group_tags_summary_on=Will prefix tags with their type -pref_group_tags_summary_off=List all tags together -pref_last_volume_cover_title=Keep cover updated -pref_last_volume_cover_summary_off=Keep current cover +# ================ # +# Authentication # +# ================ # +login_errors_failed_login=Authentication failed.\nPlease check your credentials +login_errors_header_token_empty=Authorization token missing\nPlease open the extension first to generate a token +login_errors_invalid_url=Invalid server URL: +login_errors_parse_tokendto=Failed to process authentication token +http_errors_401=Authentication expired\nPlease log in again +http_errors_500=Server error occurred + +# ============== # +# 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_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_summary_on=Show EPUB titles in browsing + +pref_show_epub_title=EPUB Visibility +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_summary_off=Do not display the release date -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_reset_title=Reset All Settings -pref_reset_summary=Clear all preferences for this Kavita instance (Only affect the selected instance) -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 Mihon 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 +pref_rd_date_summary_off=Hide publication dates +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 Settings +pref_reset_summary=Clear all preferences for this Kavita instance + +pref_filters_title=Default Filters +pref_filters_summary=Select which filters appear by default + +# ============== # +# OPDS Setup # +# ============== # +pref_opds_must_setup_address=OPDS URL required for connection +pref_opds_badformed_url=Invalid OPDS address\nCopy from: User Settings > API Key / OPDS > OPDS URL (settings#clients) +pref_opds_duplicated_source_url=URL already used in another source: +pref_opds_summary=Complete OPDS URL including API key\nFormat: http://address:port/opds?apiKey=XXX + +# ============== # +# 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 + diff --git a/src/all/kavita/assets/i18n/messages_fr_fr.properties b/src/all/kavita/assets/i18n/messages_fr_fr.properties index 866b7d69..fc6a3880 100644 --- a/src/all/kavita/assets/i18n/messages_fr_fr.properties +++ b/src/all/kavita/assets/i18n/messages_fr_fr.properties @@ -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 -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 copier \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 Mihon 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 token 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 relancez l'application +# ================ # +# Authentification # +# ================ # +login_errors_failed_login=Échec de l'authentification. Vérifiez vos identifiants +login_errors_header_token_empty=Token d'autorisation manquant\nOuvrez d'abord l'extension pour générer un token +login_errors_invalid_url=URL du serveur invalide : +login_errors_parse_tokendto=Échec du traitement du token d'authentification +http_errors_401=Authentification expirée. Veuillez vous reconnecter +http_errors_500=Erreur du serveur + +# ============== # +# Préférences # +# ============== # +pref_customsource_title=Nom d'affichage de la source +pref_edit_customsource_summary=Personnalisez l'affichage de cette instance Kavita\nExemple : "Serveur Maison" ou "Instance Cloud" + +pref_group_tags_title=Groupement des tags (fork doit supporter le groupement) +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 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 index 70f6c945..b54304d1 100644 --- 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 @@ -3,25 +3,6 @@ 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", - ).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) : Filter.Select("Smart Filters", arrayOf(noSmartFilterSelected) + smartFilters) class SortFilter(sortables: Array) : Filter.Sort("Sort by", sortables, Selection(0, true)) @@ -38,8 +19,8 @@ val sortableList = listOf( Pair("Random", 9), ) -class SpecialListFilter : Filter.Select( - "Special Lists", +class SpecialListFilter(name: String = "Special Lists") : Filter.Select( + name, arrayOf("None", "Want to Read", "Reading Lists"), ) @@ -47,9 +28,11 @@ class StatusFilter : Filter.Select( "Status", arrayOf("Any", "Unread", "In Progress", "Read"), ) +class StatusSeparator : + Filter.Separator() -class StatusFilterGroup : Filter.Group( - "Status", +class StatusFilterGroup(name: String = "Status") : Filter.Group( + name, listOf(StatusFilter()), ) @@ -64,7 +47,8 @@ class GenreFilterGroup(genres: List) : Filter.Group("Genres", genres) class TagFilter(name: String) : Filter.TriState(name) -class TagFilterGroup(tags: List) : Filter.Group("Tags", tags) +class TagFilterGroup(tags: List) : + Filter.Group("Tags", tags) class AgeRatingFilter(name: String) : Filter.TriState(name) class AgeRatingFilterGroup(ageRatings: List) : @@ -90,8 +74,29 @@ class PubStatusFilter(name: String) : Filter.CheckBox(name, false) class PubStatusFilterGroup(status: List) : Filter.Group("Publication Status", status) +class UserRating : Filter.Select( + "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) : - Filter.Header(name) + Filter.Header(name.toString()) class PeopleSeparatorFilter : Filter.Separator() 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 index ad890790..dd7beafd 100644 --- 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 @@ -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.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 @@ -55,6 +54,14 @@ 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.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.json.Json import kotlinx.serialization.json.buildJsonObject @@ -68,16 +75,11 @@ 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() @@ -193,65 +195,40 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou * 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 -> - val field = throwable.javaClass.getDeclaredField("code") - field.isAccessible = true - try { - var code = field.get(throwable) - 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) + private suspend fun fetch(request: Request): MangasPage = withContext(Dispatchers.IO) { + try { + val response = client.newCall(request).execute() + if (!response.isSuccessful) { + val code = response.code + throw IOException("Http Error: $code\n ${helper.intl["http_errors_$code"]}\n${helper.intl["check_version"]}") } + popularMangaParse(response) + } catch (e: Exception) { + Log.e(LOG_TAG, "Error fetching manga", e) + throw e + } } - @Deprecated( - "Use the non-RxJava API instead", - replaceWith = ReplaceWith("getPopularManga(page)"), - ) - override fun fetchPopularManga(page: Int): Observable { - return client.newCall(popularMangaRequest(page)) - .asObservableSuccess() - .map { response -> - if (!response.isSuccessful) { - throw IOException("HTTP error ${response.code}") - } - popularMangaParse(response) - } - .onErrorReturn { error -> - Log.e(LOG_TAG, "Error fetching popular manga", error) - MangasPage(emptyList(), false) + override suspend fun getPopularManga(page: Int): MangasPage = withContext(Dispatchers.IO) { + try { + val response = client.newCall(popularMangaRequest(page)).execute() + if (!response.isSuccessful) { + throw IOException("HTTP error ${response.code}") } + popularMangaParse(response) + } catch (e: Exception) { + Log.e(LOG_TAG, "Error fetching popular manga", e) + MangasPage(emptyList(), false) + } } - @Deprecated( - "Use the non-RxJava API instead", - replaceWith = ReplaceWith("getLatestUpdates(page)"), - ) - override fun fetchLatestUpdates(page: Int) = - fetch(latestUpdatesRequest(page)) + override suspend fun getLatestUpdates(page: Int): MangasPage = fetch(latestUpdatesRequest(page)) - @Deprecated( - "Use the non-RxJava API instead", - replaceWith = ReplaceWith("getSearchManga(page, query, filters)"), - ) - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { val specialListFilter = filters.find { it is SpecialListFilter } as? SpecialListFilter return when (specialListFilter?.state) { - 1 -> { - // Want to Read selected + 1 -> { // Want to Read selected val payload = """{ "id": 0, "name": "", @@ -268,14 +245,12 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou headersBuilder().build(), payload.toRequestBody(JSON_MEDIA_TYPE), ) - return client.newCall(request) - .asObservableSuccess() - .map { popularMangaParse(it) } + val response = client.newCall(request).execute() + popularMangaParse(response) } 2 -> { // Reading Lists - client.newCall(readingListRequest()) - .asObservableSuccess() - .map { readingListParse(it) } + val response = client.newCall(readingListRequest()).execute() + readingListParse(response) } else -> { // Regular searches fetch(searchMangaRequest(page, query, filters)) @@ -283,15 +258,11 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou } } - @Deprecated( - "Use the non-RxJava API instead", - replaceWith = ReplaceWith("getChapterList(manga)"), - ) - override fun fetchChapterList(manga: SManga): Observable> { + override suspend fun getChapterList(manga: SManga): List { return if (manga.url.contains("/ReadingList/")) { fetchReadingListItems(manga) } else { - super.fetchChapterList(manga) + super.getChapterList(manga) } } @@ -299,7 +270,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou try { val result = response.parseAs>() 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)) } catch (e: Exception) { Log.e(LOG_TAG, "Unhandled exception", e) @@ -431,7 +402,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou is Filter.Group<*> -> { when (filter) { is StatusFilterGroup -> { - val statusFilter = filter.state.firstOrNull() as? StatusFilter + val statusFilter = filter.state.firstOrNull() statusFilter?.state?.let { selectedIndex -> when (selectedIndex) { 1 -> { // Unread (0%) @@ -505,22 +476,30 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou } is TagFilterGroup -> { - filter.state.forEach { tagFilter -> - val tag = tagsListMeta.find { it.title == (tagFilter as Filter.TriState).name } - tag?.let { - when (tagFilter.state) { - STATE_INCLUDE -> filterV2.addStatement( - FilterComparison.Contains, - FilterField.Tags, - tag.id.toString(), - ) - STATE_EXCLUDE -> filterV2.addStatement( - FilterComparison.NotContains, - FilterField.Tags, - tag.id.toString(), - ) - } + val included = filter.state + .filter { it.state == STATE_INCLUDE } + .mapNotNull { tagFilter -> + tagsListMeta.find { it.title == tagFilter.name }?.id?.toString() } + 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, + FilterField.Tags, + included.joinToString(","), + ) + } + if (excluded.isNotEmpty()) { + filterV2.addStatement( + FilterComparison.NotContains, + FilterField.Tags, + excluded.joinToString(","), + ) } } is AgeRatingFilterGroup -> { @@ -802,102 +781,88 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou "Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails(manga)"), ) - override fun fetchMangaDetails(manga: SManga): Observable { + override suspend fun getMangaDetails(manga: SManga): SManga { val serieId = helper.getIdFromUrl(manga.url) return if (manga.url.contains("source=readinglist")) { val readingListId = manga.url.substringAfter("readingListId=").substringBefore("&").toIntOrNull() val now = java.util.Calendar.getInstance() + val readingList = cachedReadingLists.find { it.id == readingListId } + if (readingList != null) { + val groupTags = preferences.groupTags + val starting = readingList.startingYear.takeIf { it > 0 }?.let { + "${readingList.startingYear}-${readingList.startingMonth.toString().padStart(2, '0')}" + } + val ending = readingList.endingYear.takeIf { it > 0 }?.let { + "${readingList.endingYear}-${readingList.endingMonth.toString().padStart(2, '0')}" + } - Observable.fromCallable { - val readingList = cachedReadingLists.find { it.id == readingListId } - if (readingList != null) { - val groupTags = preferences.groupTags - val starting = readingList.startingYear.takeIf { it > 0 }?.let { - "${readingList.startingYear}-${readingList.startingMonth.toString().padStart(2, '0')}" - } - val ending = readingList.endingYear.takeIf { it > 0 }?.let { - "${readingList.endingYear}-${readingList.endingMonth.toString().padStart(2, '0')}" - } + val itemsRequest = GET("$apiUrl/ReadingList/items?readingListId=$readingListId", headersBuilder().build()) + val items = try { + client.newCall(itemsRequest).execute().parseAs>() + } catch (e: Exception) { + Log.e(LOG_TAG, "Error parsing reading list items", e) + emptyList() + } - val itemsRequest = GET("$apiUrl/ReadingList/items?readingListId=$readingListId", headersBuilder().build()) - val items = try { - client.newCall(itemsRequest).execute().parseAs>() + val allGenres = items.flatMap { item -> + try { + val seriesMetaRequest = GET("$apiUrl/series/metadata?seriesId=${item.seriesId}", headersBuilder().build()) + client.newCall(seriesMetaRequest).execute() + .parseAs() + .genres.map { it.title } } catch (e: Exception) { - Log.e(LOG_TAG, "Error parsing reading list items", e) + Log.e(LOG_TAG, "Error fetching series metadata for item ${item.seriesId}", e) emptyList() } + }.distinct() - val allGenres = items.flatMap { item -> - try { - val seriesMetaRequest = GET("$apiUrl/series/metadata?seriesId=${item.seriesId}", headersBuilder().build()) - client.newCall(seriesMetaRequest).execute() - .parseAs() - .genres.map { it.title } - } catch (e: Exception) { - Log.e(LOG_TAG, "Error fetching series metadata for item ${item.seriesId}", e) - emptyList() - } - }.distinct() - - val genreString = if (groupTags) { - buildList { - add("Type: Reading List") - starting?.let { add("Starting: $it") } - ending?.let { add("Ending: $it") } - if (allGenres.isNotEmpty()) addAll(allGenres.map { "Genres: $it" }) - }.joinToString(", ") - } else { - buildList { - starting?.let { add("Starting: $it") } - ending?.let { add("Ending: $it") } - add("Reading List") - if (allGenres.isNotEmpty()) addAll(allGenres) - }.joinToString(", ") - } - - val hasDates = readingList.startingYear > 0 && readingList.endingYear > 0 - val status = if (hasDates) { - val endCal = java.util.Calendar.getInstance().apply { - set(readingList.endingYear, readingList.endingMonth - 1, 1) - } - if (now.after(endCal)) SManga.PUBLISHING_FINISHED else SManga.ONGOING - } else { - SManga.COMPLETED - } - - manga.apply { - title = (if (readingList.promoted) "🔺 " else "") + readingList.title - artist = "${readingList.itemCount} items" - thumbnail_url = if (!readingList.coverImage.isNullOrBlank()) { - "$apiUrl/image/${readingList.coverImage}?apiKey=$apiKey" - } else { - "$apiUrl/image/readinglist-cover?readingListId=$readingListId&apiKey=$apiKey" - } - description = readingList.summary ?: "Reading List" - genre = genreString - url = manga.url - initialized = true - this.status = status - } + val genreString = if (groupTags) { + buildList { + add("Type: Reading List") + starting?.let { add("Starting: $it") } + ending?.let { add("Ending: $it") } + if (allGenres.isNotEmpty()) addAll(allGenres.map { "Genres: $it" }) + }.joinToString(", ") } else { - manga.apply { - initialized = true + buildList { + starting?.let { add("Starting: $it") } + ending?.let { add("Ending: $it") } + add("Reading List") + if (allGenres.isNotEmpty()) addAll(allGenres) + }.joinToString(", ") + } + + val hasDates = readingList.startingYear > 0 && readingList.endingYear > 0 + val status = if (hasDates) { + val endCal = java.util.Calendar.getInstance().apply { + set(readingList.endingYear, readingList.endingMonth - 1, 1) } + if (now.after(endCal)) SManga.PUBLISHING_FINISHED else SManga.ONGOING + } else { + SManga.COMPLETED } - }.onErrorReturn { error -> - Log.e(LOG_TAG, "Error fetching reading list details", error) + manga.apply { - description = helper.intl["error_loading_reading_list"] + title = (if (readingList.promoted) "🔺 " else "") + readingList.title + artist = "${readingList.itemCount} items" + thumbnail_url = if (!readingList.coverImage.isNullOrBlank()) { + "$apiUrl/image/${readingList.coverImage}?apiKey=$apiKey" + } else { + "$apiUrl/image/readinglist-cover?readingListId=$readingListId&apiKey=$apiKey" + } + description = readingList.summary ?: "Reading List" + genre = genreString + url = manga.url initialized = true + this.status = status } + } else { + manga.apply { initialized = true } } } else { - client.newCall(GET("$apiUrl/series/metadata?seriesId=$serieId", headersBuilder().build())) - .asObservableSuccess() - .map { response -> - mangaDetailsParse(response).apply { initialized = true } - } + val response = client.newCall(GET("$apiUrl/series/metadata?seriesId=$serieId", headersBuilder().build())).execute() + mangaDetailsParse(response).apply { initialized = true } } } @@ -965,9 +930,9 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou .filterNot { it.equals(foundDemographic, 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.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.description = listOfNotNull( ratingLine.takeIf { it.isNotBlank() }, @@ -1092,7 +1057,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou .filterNot { it.equals(foundDemographic, 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 } description = listOfNotNull( ratingLine.takeIf { it.isNotBlank() }, @@ -1173,13 +1138,11 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou } } - private fun fetchReadingListItems(manga: SManga): Observable> { + private fun fetchReadingListItems(manga: SManga): List { val readingListId = manga.url.substringAfter("readingListId=").substringBefore("&") val request = GET("$apiUrl/ReadingList/items?readingListId=$readingListId", headersBuilder().build()) - - return client.newCall(request) - .asObservableSuccess() - .map { parseReadingListItems(it) } + val response = client.newCall(request).execute() + return parseReadingListItems(response) } private fun parseReadingListItems(response: Response): List { @@ -1321,7 +1284,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou try { val seriesDto = client.newCall(GET("$apiUrl/Series/${item.seriesId}", headersBuilder().build())) .execute().parseAs() - helper.createSeriesDto(seriesDto, apiUrl, baseUrl, apiKey) + helper.createSeriesDto(seriesDto, apiUrl, baseUrl) } catch (e: Exception) { Log.e(LOG_TAG, "Error fetching series for related reading list", e) null @@ -1531,11 +1494,10 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou return GET("$apiUrl/$chapterId", headersBuilder().build()) } - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList(chapter)")) - override fun fetchPageList(chapter: SChapter): Observable> { + override suspend fun getPageList(chapter: SChapter): List = withContext(Dispatchers.IO) { // Check if this is a reading list item (has readingListId in URL) if (chapter.url.contains("readingListId=")) { - return Observable.fromCallable { + try { // Extract seriesId from the chapter URL val seriesId = chapter.url.substringAfter("seriesId=").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") // Get item details based on type - val chapterDetails = if (listItem.chapterId != null) { - val request = GET("$apiUrl/Chapter?chapterId=${listItem.chapterId}", headersBuilder().build()) - client.newCall(request).execute().parseAs() - } else if (listItem.volumeId != null) { - // For volumes, get all chapters and let user choose - val volumeRequest = GET("$apiUrl/Volume/${listItem.volumeId}", headersBuilder().build()) - val volume = client.newCall(volumeRequest).execute().parseAs() - volume.chapters.firstOrNull() ?: throw IOException(helper.intl["error_no_chapters_found"]) - } else { - throw IOException(helper.intl["error_invalid_reading_list_item"]) + val chapterDetails = when { + listItem.chapterId != null -> { + // Standard chapter + val request = GET("$apiUrl/Chapter?chapterId=${listItem.chapterId}", headersBuilder().build()) + client.newCall(request).execute().parseAs() + } + listItem.volumeId != null -> { + // Volume - get first chapter + val volumeRequest = GET("$apiUrl/Volume/${listItem.volumeId}", headersBuilder().build()) + val volume = client.newCall(volumeRequest).execute().parseAs() + volume.chapters.firstOrNull()?.let { + val chapRequest = GET("$apiUrl/Chapter?chapterId=${it.id}", headersBuilder().build()) + client.newCall(chapRequest).execute().parseAs() + } ?: 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( index = i, imageUrl = "$apiUrl/Reader/image?chapterId=${chapterDetails.id}&page=$i&extractPdf=true&apiKey=$apiKey", ) } - }.onErrorResumeNext { error -> - Log.e(LOG_TAG, "Error fetching reading list chapter pages", error) - Observable.error(error) + } catch (e: Exception) { + Log.e(LOG_TAG, "Error processing reading list item", e) + throw e } } // For normal titles - return Observable.fromCallable { + try { // Check if this is a volume URL if (chapter.url.contains("/Volume/") || chapter.url.startsWith("volume_")) { val volumeId = when { @@ -1621,7 +1590,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou } } - (initialPages + remainingPages).toList() + return@withContext (initialPages + remainingPages).toList() } else { // Original chapter handling 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}") } - (0 until chapterDetails.pages).map { i -> + return@withContext (0 until chapterDetails.pages).map { i -> Page( index = i, 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 - 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 - (0 until fallbackPageCount).map { i -> + return@withContext (0 until fallbackPageCount).map { i -> Page( index = i, 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 { return try { val result = response.parseAs>() - val mangaList = result.map { helper.createSeriesDto(it, apiUrl, baseUrl, apiKey) } + val mangaList = result.map { helper.createSeriesDto(it, apiUrl, baseUrl) } MangasPage(mangaList, false) } catch (e: Exception) { 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 = 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 */ + private fun getFilterList( + genres: List, + tags: List, + ageRatings: List, + collections: List, + languages: List, + libraries: List, + people: List, + pubStatus: List, + smartFilters: Array, + ): FilterList { + return FilterList( + SortFilter(sortableList.map { it.first }.toTypedArray()), + + StatusSeparator(), + StatusFilterGroup(), + + // Release year filter + ReleaseYearRangeGroup( + listOf( + ReleaseYearRange("Min"), + ReleaseYearRange("Max"), + ), + ), + + GenreFilterGroup(genres.map { GenreFilter(it) }), + TagFilterGroup(tags.map { TagFilter(it) }), + CollectionFilterGroup(collections.map { CollectionFilter(it) }), + LibrariesFilterGroup(libraries.map { LibraryFilter(it) }), + LanguageFilterGroup(languages.map { LanguageFilter(it) }), + PubStatusFilterGroup(pubStatus.map { PubStatusFilter(it) }), + AgeRatingFilterGroup(ageRatings.map { AgeRatingFilter(it) }), + + 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) }), + ) + } + override fun getFilterList(): FilterList { - val toggledFilters = getToggledFilters() - - val filters = try { - val filtersLoaded = mutableListOf>() - - filtersLoaded.add(SortFilter(sortableList.map { it.first }.toTypedArray())) - - 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( - 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", - "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(), - ) - } - - filtersLoaded - } catch (e: Exception) { - Log.e(LOG_TAG, "[FILTERS] Error while creating filter list", e) - FilterList(emptyList()) - } - return FilterList(filters) + return getFilterList( + genres = genresListMeta.map { it.title }, + tags = tagsListMeta.map { it.title }, + ageRatings = ageRatingsListMeta.map { it.title }, + 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) { @@ -2179,165 +2124,88 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou Log.v(LOG_TAG, "[Login] Login successful") } + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + init { if (apiUrl.isNotBlank()) { - Single.fromCallable { - doLogin() - try { // Get current version - val requestUrl = "$apiUrl/Server/server-info-slim" - val serverInfoDto = client.newCall(GET(requestUrl, headersBuilder().build())) + scope.launch { + try { + doLogin() + + // Load all filters in parallel + val deferredFilters = listOf( + async { + genresListMeta = loadMetadata("$apiUrl/Metadata/genres") + Log.d(LOG_TAG, "Loaded ${genresListMeta.size} genres") + }, + async { + tagsListMeta = loadMetadata("$apiUrl/Metadata/tags") + Log.d(LOG_TAG, "Loaded ${tagsListMeta.size} tags") + }, + async { + ageRatingsListMeta = loadMetadata("$apiUrl/Metadata/age-ratings") + Log.d(LOG_TAG, "Loaded ${ageRatingsListMeta.size} age ratings") + }, + async { + collectionsListMeta = loadMetadata("$apiUrl/Collection") + Log.d(LOG_TAG, "Loaded ${collectionsListMeta.size} collections") + }, + async { + languagesListMeta = loadMetadata("$apiUrl/Metadata/languages") + Log.d(LOG_TAG, "Loaded ${languagesListMeta.size} languages") + }, + async { + libraryListMeta = loadMetadata("$apiUrl/Library/libraries") + Log.d(LOG_TAG, "Loaded ${libraryListMeta.size} libraries") + }, + async { + peopleListMeta = loadMetadata("$apiUrl/Metadata/people") + Log.d(LOG_TAG, "Loaded ${peopleListMeta.size} people") + }, + async { + pubStatusListMeta = loadMetadata("$apiUrl/Metadata/publication-status") + Log.d(LOG_TAG, "Loaded ${pubStatusListMeta.size} publication statuses") + }, + async { + 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() - 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()}") + Log.d(LOG_TAG, "Kavita version: ${serverInfoDto.kavitaVersion}") } 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>(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 { - pubStatusListMeta = response.body.use { json.decodeFromString(it.string()) } - } catch (e: Exception) { - Log.e(LOG_TAG, "Error decoding publication status JSON", e) - pubStatusListMeta = 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() - } - } - } catch (e: Exception) { - throw LoadingFilterFailed("Failed Loading Filters", e.cause) + Log.e(LOG_TAG, "Initialization error", e) } } - .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) - }, - ) } } + + // 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 loadMetadata(url: String): T { + return client.newCall(GET(url, headersBuilder().build())) + .execute() + .parseAs() + } } diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaHelper.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaHelper.kt index 1cdb9c39..b10f7981 100644 --- a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaHelper.kt +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaHelper.kt @@ -47,18 +47,17 @@ class KavitaHelper { } } - // Rating Providers from Series-Details-Plus - private fun getProviderName(provider: Int): String = when (provider) { - 0 -> "User" - 1 -> "AniList" - 2 -> "MyAnimeList" - 3 -> "MangaUpdates" - else -> "Rating" - } + // @todo Rating Providers from Series-Details-Plus +// private fun getProviderName(provider: Int): String = when (provider) { +// 0 -> "User" +// 1 -> "AniList" +// 2 -> "MyAnimeList" +// 3 -> "MangaUpdates" +// 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 { -// url = "$baseUrl/library/${obj.libraryId}/series/${obj.id}" url = "$baseUrl/Series/${obj.id}" title = obj.name thumbnail_url = "$baseUrl/image/series-cover?seriesId=${obj.id}&apiKey=$apiKey" @@ -102,16 +101,24 @@ class KavitaHelper { } ChapterType.SingleFileVolume -> { - cleanChapterTitle( - when { - volume.name.any { it.isLetter() } -> "v${formatVolumeNumber(volume)} - ${volume.name}" - else -> "Volume ${formatVolumeNumber(volume)}" - }, - ChapterTitleContext( - mangaTitle = mangaTitle, - volumeName = volume.name, - ), - ) + val volumeNumber = formatVolumeNumber(volume) + val cleanVolumeName = volume.name.trim() + + // Always use the API's volume number, never parse from title + when { + // If name is empty or just numbers (Kavita default) + cleanVolumeName.isEmpty() || cleanVolumeName.none { it.isLetter() } -> { + cleanChapterTitle("Volume $volumeNumber") + } + // If name already contains volume info (Vol. 3, V3, etc.) + cleanVolumeName.contains(Regex("(?i)(vol|volume|v)[.\\s]*\\d+")) -> { + cleanChapterTitle(cleanVolumeName) + } + // Normal case - prefix with volume number + else -> { + cleanChapterTitle("$volumeNumber - $cleanVolumeName") + } + } } 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 chapter_number = when { type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->