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."
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

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
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

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.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>) :
Filter.Select<String>("Smart Filters", arrayOf(noSmartFilterSelected) + smartFilters)
class SortFilter(sortables: Array<String>) : Filter.Sort("Sort by", sortables, Selection(0, true))
@@ -38,8 +19,8 @@ val sortableList = listOf(
Pair("Random", 9),
)
class SpecialListFilter : Filter.Select<String>(
"Special Lists",
class SpecialListFilter(name: String = "Special Lists") : Filter.Select<String>(
name,
arrayOf("None", "Want to Read", "Reading Lists"),
)
@@ -47,9 +28,11 @@ class StatusFilter : Filter.Select<String>(
"Status",
arrayOf("Any", "Unread", "In Progress", "Read"),
)
class StatusSeparator :
Filter.Separator()
class StatusFilterGroup : Filter.Group<StatusFilter>(
"Status",
class StatusFilterGroup(name: String = "Status") : Filter.Group<StatusFilter>(
name,
listOf(StatusFilter()),
)
@@ -64,7 +47,8 @@ class GenreFilterGroup(genres: List<GenreFilter>) :
Filter.Group<GenreFilter>("Genres", genres)
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 AgeRatingFilterGroup(ageRatings: List<AgeRatingFilter>) :
@@ -90,8 +74,29 @@ class PubStatusFilter(name: String) : Filter.CheckBox(name, false)
class PubStatusFilterGroup(status: List<PubStatusFilter>) :
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) :
Filter.Header(name)
Filter.Header(name.toString())
class PeopleSeparatorFilter :
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.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<MangasPage> {
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<MangasPage> {
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<MangasPage> {
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<List<SChapter>> {
override suspend fun getChapterList(manga: SManga): List<SChapter> {
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<List<SeriesDto>>()
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<SManga> {
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<List<ReadingListItemDto>>()
} 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<List<ReadingListItemDto>>()
val allGenres = items.flatMap { item ->
try {
val seriesMetaRequest = GET("$apiUrl/series/metadata?seriesId=${item.seriesId}", headersBuilder().build())
client.newCall(seriesMetaRequest).execute()
.parseAs<SeriesDetailPlusDto>()
.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<SeriesDetailPlusDto>()
.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<List<SChapter>> {
private fun fetchReadingListItems(manga: SManga): List<SChapter> {
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<SChapter> {
@@ -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<SeriesDto>()
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<List<Page>> {
override suspend fun getPageList(chapter: SChapter): List<Page> = 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<ChapterDto>()
} 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<VolumeDto>()
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<ChapterDto>()
}
listItem.volumeId != null -> {
// Volume - get first chapter
val volumeRequest = GET("$apiUrl/Volume/${listItem.volumeId}", headersBuilder().build())
val volume = client.newCall(volumeRequest).execute().parseAs<VolumeDto>()
volume.chapters.firstOrNull()?.let {
val chapRequest = GET("$apiUrl/Chapter?chapterId=${it.id}", headersBuilder().build())
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(
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<List<SeriesDto>>()
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<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
*/
private fun getFilterList(
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()),
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<Filter<*>>()
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<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()}")
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<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 {
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 <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
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 ->