Update parsers and adjust database

This commit is contained in:
Koitharu
2025-02-28 15:35:56 +02:00
parent 8724f5b30c
commit 7a01fdd04c
13 changed files with 81 additions and 35 deletions

View File

@@ -28,15 +28,16 @@ class JsonDeserializer(private val json: JSONObject) {
fun toMangaEntity() = MangaEntity( fun toMangaEntity() = MangaEntity(
id = json.getLong("id"), id = json.getLong("id"),
title = json.getString("title"), title = json.getString("title"),
altTitle = json.getStringOrNull("alt_title"), altTitles = json.getStringOrNull("alt_title"),
url = json.getString("url"), url = json.getString("url"),
publicUrl = json.getStringOrNull("public_url").orEmpty(), publicUrl = json.getStringOrNull("public_url").orEmpty(),
rating = json.getDouble("rating").toFloat(), rating = json.getDouble("rating").toFloat(),
isNsfw = json.getBooleanOrDefault("nsfw", false), isNsfw = json.getBooleanOrDefault("nsfw", false),
contentRating = json.getStringOrNull("content_rating"),
coverUrl = json.getString("cover_url"), coverUrl = json.getString("cover_url"),
largeCoverUrl = json.getStringOrNull("large_cover_url"), largeCoverUrl = json.getStringOrNull("large_cover_url"),
state = json.getStringOrNull("state"), state = json.getStringOrNull("state"),
author = json.getStringOrNull("author"), authors = json.getStringOrNull("author"),
source = json.getString("source"), source = json.getString("source"),
) )

View File

@@ -58,15 +58,16 @@ class JsonSerializer private constructor(private val json: JSONObject) {
JSONObject().apply { JSONObject().apply {
put("id", e.id) put("id", e.id)
put("title", e.title) put("title", e.title)
put("alt_title", e.altTitle) put("alt_title", e.altTitles)
put("url", e.url) put("url", e.url)
put("public_url", e.publicUrl) put("public_url", e.publicUrl)
put("rating", e.rating) put("rating", e.rating)
put("nsfw", e.isNsfw) put("nsfw", e.isNsfw)
put("content_rating", e.contentRating)
put("cover_url", e.coverUrl) put("cover_url", e.coverUrl)
put("large_cover_url", e.largeCoverUrl) put("large_cover_url", e.largeCoverUrl)
put("state", e.state) put("state", e.state)
put("author", e.author) put("author", e.authors)
put("source", e.source) put("source", e.source)
}, },
) )

View File

@@ -40,6 +40,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration21To22
import org.koitharu.kotatsu.core.db.migrations.Migration22To23 import org.koitharu.kotatsu.core.db.migrations.Migration22To23
import org.koitharu.kotatsu.core.db.migrations.Migration23To24 import org.koitharu.kotatsu.core.db.migrations.Migration23To24
import org.koitharu.kotatsu.core.db.migrations.Migration24To23 import org.koitharu.kotatsu.core.db.migrations.Migration24To23
import org.koitharu.kotatsu.core.db.migrations.Migration24To25
import org.koitharu.kotatsu.core.db.migrations.Migration2To3 import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4 import org.koitharu.kotatsu.core.db.migrations.Migration3To4
import org.koitharu.kotatsu.core.db.migrations.Migration4To5 import org.koitharu.kotatsu.core.db.migrations.Migration4To5
@@ -67,7 +68,7 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao import org.koitharu.kotatsu.tracker.data.TracksDao
const val DATABASE_VERSION = 24 const val DATABASE_VERSION = 25
@Database( @Database(
entities = [ entities = [
@@ -136,6 +137,7 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration22To23(), Migration22To23(),
Migration23To24(), Migration23To24(),
Migration24To23(), Migration24To23(),
Migration24To25(),
) )
fun MangaDatabase(context: Context): MangaDatabase = Room fun MangaDatabase(context: Context): MangaDatabase = Room

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.core.db.entity package org.koitharu.kotatsu.core.db.entity
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaState
@@ -8,8 +9,11 @@ import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.longHashCode import org.koitharu.kotatsu.parsers.util.longHashCode
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.toArraySet
import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.parsers.util.toTitleCase
private const val VALUES_DIVIDER = '\n'
// Entity to model // Entity to model
fun TagEntity.toMangaTag() = MangaTag( fun TagEntity.toMangaTag() = MangaTag(
@@ -22,18 +26,19 @@ fun Collection<TagEntity>.toMangaTags() = mapToSet(TagEntity::toMangaTag)
fun Collection<TagEntity>.toMangaTagsList() = map(TagEntity::toMangaTag) fun Collection<TagEntity>.toMangaTagsList() = map(TagEntity::toMangaTag)
fun MangaEntity.toManga(tags: Set<MangaTag>, chapters: List<ChapterEntity>?) = Manga( // TODO fun MangaEntity.toManga(tags: Set<MangaTag>, chapters: List<ChapterEntity>?) = Manga(
id = this.id, id = this.id,
title = this.title, title = this.title,
altTitle = this.altTitle, altTitles = this.altTitles?.split(VALUES_DIVIDER)?.toArraySet().orEmpty(),
state = this.state?.let { MangaState(it) }, state = this.state?.let { MangaState(it) },
rating = this.rating, rating = this.rating,
isNsfw = this.isNsfw, contentRating = ContentRating(this.contentRating)
?: if (isNsfw) ContentRating.ADULT else null,
url = this.url, url = this.url,
publicUrl = this.publicUrl, publicUrl = this.publicUrl,
coverUrl = this.coverUrl, coverUrl = this.coverUrl,
largeCoverUrl = this.largeCoverUrl, largeCoverUrl = this.largeCoverUrl,
author = this.author, authors = this.authors?.split(VALUES_DIVIDER)?.toArraySet().orEmpty(),
source = MangaSource(this.source), source = MangaSource(this.source),
tags = tags, tags = tags,
chapters = chapters?.toMangaChapters(), chapters = chapters?.toMangaChapters(),
@@ -66,12 +71,13 @@ fun Manga.toEntity() = MangaEntity(
source = source.name, source = source.name,
largeCoverUrl = largeCoverUrl, largeCoverUrl = largeCoverUrl,
coverUrl = coverUrl.orEmpty(), coverUrl = coverUrl.orEmpty(),
altTitle = altTitle, altTitles = altTitles.joinToString(VALUES_DIVIDER.toString()),
rating = rating, rating = rating,
isNsfw = isNsfw, isNsfw = isNsfw,
contentRating = contentRating?.name,
state = state?.name, state = state?.name,
title = title, title = title,
author = author, authors = authors.joinToString(VALUES_DIVIDER.toString()),
) )
fun MangaTag.toEntity() = TagEntity( fun MangaTag.toEntity() = TagEntity(
@@ -108,3 +114,7 @@ fun SortOrder(name: String, fallback: SortOrder): SortOrder = runCatching {
fun MangaState(name: String): MangaState? = runCatching { fun MangaState(name: String): MangaState? = runCatching {
MangaState.valueOf(name) MangaState.valueOf(name)
}.getOrNull() }.getOrNull()
fun ContentRating(name: String?): ContentRating? = runCatching {
ContentRating.valueOf(name ?: return@runCatching null)
}.getOrNull()

View File

@@ -10,14 +10,15 @@ data class MangaEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val id: Long, @ColumnInfo(name = "manga_id") val id: Long,
@ColumnInfo(name = "title") val title: String, @ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "alt_title") val altTitle: String?, @ColumnInfo(name = "alt_title") val altTitles: String?,
@ColumnInfo(name = "url") val url: String, @ColumnInfo(name = "url") val url: String,
@ColumnInfo(name = "public_url") val publicUrl: String, @ColumnInfo(name = "public_url") val publicUrl: String,
@ColumnInfo(name = "rating") val rating: Float, // normalized value [0..1] or -1 @ColumnInfo(name = "rating") val rating: Float, // normalized value [0..1] or -1
@ColumnInfo(name = "nsfw") val isNsfw: Boolean, // TODO change to contentRating @ColumnInfo(name = "nsfw") val isNsfw: Boolean,
@ColumnInfo(name = "content_rating") val contentRating: String?,
@ColumnInfo(name = "cover_url") val coverUrl: String, @ColumnInfo(name = "cover_url") val coverUrl: String,
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String?, @ColumnInfo(name = "large_cover_url") val largeCoverUrl: String?,
@ColumnInfo(name = "state") val state: String?, @ColumnInfo(name = "state") val state: String?,
@ColumnInfo(name = "author") val author: String?, @ColumnInfo(name = "author") val authors: String?,
@ColumnInfo(name = "source") val source: String, @ColumnInfo(name = "source") val source: String,
) )

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration24To25 : Migration(24, 25) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE manga ADD COLUMN content_rating TEXT DEFAULT NULL")
db.execSQL("UPDATE manga SET content_rating = (SELECT IIF(m.nsfw, 'ADULT', NULL) FROM manga AS m WHERE manga.manga_id = m.manga_id)")
}
}

View File

@@ -7,6 +7,8 @@ import kotlinx.parcelize.Parcelize
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.util.ext.readParcelableCompat import org.koitharu.kotatsu.core.util.ext.readParcelableCompat
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
import org.koitharu.kotatsu.core.util.ext.readStringSet
import org.koitharu.kotatsu.core.util.ext.writeStringSet
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@Parcelize @Parcelize
@@ -20,7 +22,7 @@ data class ParcelableManga(
override fun ParcelableManga.write(parcel: Parcel, flags: Int) = with(manga) { override fun ParcelableManga.write(parcel: Parcel, flags: Int) = with(manga) {
parcel.writeLong(id) parcel.writeLong(id)
parcel.writeString(title) parcel.writeString(title)
parcel.writeString(altTitle) parcel.writeStringSet(altTitles)
parcel.writeString(url) parcel.writeString(url)
parcel.writeString(publicUrl) parcel.writeString(publicUrl)
parcel.writeFloat(rating) parcel.writeFloat(rating)
@@ -30,7 +32,7 @@ data class ParcelableManga(
parcel.writeString(description.takeIf { withDescription }) parcel.writeString(description.takeIf { withDescription })
parcel.writeParcelable(ParcelableMangaTags(tags), flags) parcel.writeParcelable(ParcelableMangaTags(tags), flags)
parcel.writeSerializable(state) parcel.writeSerializable(state)
parcel.writeString(author) parcel.writeStringSet(authors)
parcel.writeString(source.name) parcel.writeString(source.name)
} }
@@ -38,7 +40,7 @@ data class ParcelableManga(
Manga( Manga(
id = parcel.readLong(), id = parcel.readLong(),
title = requireNotNull(parcel.readString()), title = requireNotNull(parcel.readString()),
altTitle = parcel.readString(), altTitles = parcel.readStringSet(),
url = requireNotNull(parcel.readString()), url = requireNotNull(parcel.readString()),
publicUrl = requireNotNull(parcel.readString()), publicUrl = requireNotNull(parcel.readString()),
rating = parcel.readFloat(), rating = parcel.readFloat(),
@@ -48,7 +50,7 @@ data class ParcelableManga(
description = parcel.readString(), description = parcel.readString(),
tags = requireNotNull(parcel.readParcelableCompat<ParcelableMangaTags>()).tags, tags = requireNotNull(parcel.readParcelableCompat<ParcelableMangaTags>()).tags,
state = parcel.readSerializableCompat(), state = parcel.readSerializableCompat(),
author = parcel.readString(), authors = parcel.readStringSet(),
chapters = null, chapters = null,
source = MangaSource(parcel.readString()), source = MangaSource(parcel.readString()),
), ),

View File

@@ -2,22 +2,22 @@ package org.koitharu.kotatsu.core.parser
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.AbstractMangaParser
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities
import java.util.EnumSet import java.util.EnumSet
/** /**
* This parser is just for parser development, it should not be used in releases * This parser is just for parser development, it should not be used in releases
*/ */
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaParserSource.DUMMY) { class DummyParser(context: MangaLoaderContext) : AbstractMangaParser(context, MangaParserSource.DUMMY) {
override val configKeyDomain: ConfigKey.Domain override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain("localhost") get() = ConfigKey.Domain("localhost")
@@ -25,14 +25,14 @@ class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaParse
override val availableSortOrders: Set<SortOrder> override val availableSortOrders: Set<SortOrder>
get() = EnumSet.allOf(SortOrder::class.java) get() = EnumSet.allOf(SortOrder::class.java)
override val filterCapabilities: MangaListFilterCapabilities override val searchQueryCapabilities: MangaSearchQueryCapabilities
get() = MangaListFilterCapabilities() get() = MangaSearchQueryCapabilities()
override suspend fun getFilterOptions(): MangaListFilterOptions = stub(null)
override suspend fun getDetails(manga: Manga): Manga = stub(manga) override suspend fun getDetails(manga: Manga): Manga = stub(manga)
override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> = stub(null) override suspend fun getFilterOptions(): MangaListFilterOptions = stub(null)
override suspend fun getList(query: MangaSearchQuery): List<Manga> = stub(null)
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = stub(null) override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = stub(null)

View File

@@ -11,6 +11,7 @@ import androidx.core.content.IntentCompat
import androidx.core.os.BundleCompat import androidx.core.os.BundleCompat
import androidx.core.os.ParcelCompat import androidx.core.os.ParcelCompat
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import org.koitharu.kotatsu.parsers.util.toArraySet
import java.io.Serializable import java.io.Serializable
import java.util.EnumSet import java.util.EnumSet
@@ -84,6 +85,14 @@ fun <E : Enum<E>> Parcel.readEnumSet(cls: Class<E>): Set<E>? {
return set return set
} }
fun Parcel.writeStringSet(set: Set<String>?) {
writeStringArray(set?.toTypedArray().orEmpty())
}
fun Parcel.readStringSet(): Set<String> {
return this.createStringArray()?.toArraySet().orEmpty()
}
fun <T> SavedStateHandle.require(key: String): T { fun <T> SavedStateHandle.require(key: String): T {
return checkNotNull(get(key)) { return checkNotNull(get(key)) {
"Value $key not found in SavedStateHandle or has a wrong type" "Value $key not found in SavedStateHandle or has a wrong type"

View File

@@ -31,6 +31,7 @@ class FavouritesContainerViewModel @Inject constructor(
val onActionDone = MutableEventFlow<ReversibleAction>() val onActionDone = MutableEventFlow<ReversibleAction>()
private val categoriesStateFlow = favouritesRepository.observeCategoriesForLibrary() private val categoriesStateFlow = favouritesRepository.observeCategoriesForLibrary()
.withErrorHandling()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
val categories = combine( val categories = combine(

View File

@@ -26,6 +26,7 @@ import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
import org.koitharu.kotatsu.parsers.util.json.toStringSet
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.parsers.util.toTitleCase
import java.io.File import java.io.File
@@ -38,10 +39,12 @@ class MangaIndex(source: String?) {
require(!manga.isLocal) { "Local manga information cannot be stored" } require(!manga.isLocal) { "Local manga information cannot be stored" }
json.put(KEY_ID, manga.id) json.put(KEY_ID, manga.id)
json.put(KEY_TITLE, manga.title) json.put(KEY_TITLE, manga.title)
json.put(KEY_TITLE_ALT, manga.altTitle) json.put(KEY_TITLE_ALT, manga.altTitle) // for backward compatibility
json.put(KEY_ALT_TITLES, JSONArray(manga.altTitles))
json.put(KEY_URL, manga.url) json.put(KEY_URL, manga.url)
json.put(KEY_PUBLIC_URL, manga.publicUrl) json.put(KEY_PUBLIC_URL, manga.publicUrl)
json.put(KEY_AUTHOR, manga.author) json.put(KEY_AUTHOR, manga.author) // for backward compatibility
json.put(KEY_AUTHORS, JSONArray(manga.authors))
json.put(KEY_COVER, manga.coverUrl) json.put(KEY_COVER, manga.coverUrl)
json.put(KEY_DESCRIPTION, manga.description) json.put(KEY_DESCRIPTION, manga.description)
json.put(KEY_RATING, manga.rating) json.put(KEY_RATING, manga.rating)
@@ -73,10 +76,12 @@ class MangaIndex(source: String?) {
Manga( Manga(
id = json.getLong(KEY_ID), id = json.getLong(KEY_ID),
title = json.getString(KEY_TITLE), title = json.getString(KEY_TITLE),
altTitle = json.getStringOrNull(KEY_TITLE_ALT), altTitles = json.optJSONArray(KEY_ALT_TITLES)?.toStringSet()
?: setOfNotNull(json.getStringOrNull(KEY_TITLE_ALT)),
url = json.getString(KEY_URL), url = json.getString(KEY_URL),
publicUrl = json.getStringOrNull(KEY_PUBLIC_URL).orEmpty(), publicUrl = json.getStringOrNull(KEY_PUBLIC_URL).orEmpty(),
author = json.getStringOrNull(KEY_AUTHOR), authors = json.optJSONArray(KEY_AUTHORS)?.toStringSet()
?: setOfNotNull(json.getStringOrNull(KEY_AUTHOR)),
largeCoverUrl = json.getStringOrNull(KEY_COVER_LARGE), largeCoverUrl = json.getStringOrNull(KEY_COVER_LARGE),
source = source, source = source,
rating = json.getFloatOrDefault(KEY_RATING, RATING_UNKNOWN), rating = json.getFloatOrDefault(KEY_RATING, RATING_UNKNOWN),
@@ -198,9 +203,11 @@ class MangaIndex(source: String?) {
private const val KEY_ID = "id" private const val KEY_ID = "id"
private const val KEY_TITLE = "title" private const val KEY_TITLE = "title"
private const val KEY_TITLE_ALT = "title_alt" private const val KEY_TITLE_ALT = "title_alt"
private const val KEY_ALT_TITLES = "alt_titles"
private const val KEY_URL = "url" private const val KEY_URL = "url"
private const val KEY_PUBLIC_URL = "public_url" private const val KEY_PUBLIC_URL = "public_url"
private const val KEY_AUTHOR = "author" private const val KEY_AUTHOR = "author"
private const val KEY_AUTHORS = "authors"
private const val KEY_COVER = "cover" private const val KEY_COVER = "cover"
private const val KEY_DESCRIPTION = "description" private const val KEY_DESCRIPTION = "description"
private const val KEY_RATING = "rating" private const val KEY_RATING = "rating"

View File

@@ -126,12 +126,12 @@ class LocalMangaParser(private val uri: Uri) {
} else { } else {
null null
}, },
altTitle = null, altTitles = emptySet(),
rating = -1f, rating = -1f,
contentRating = null, contentRating = null,
tags = setOf(), tags = emptySet(),
state = null, state = null,
author = null, authors = emptySet(),
largeCoverUrl = null, largeCoverUrl = null,
description = null, description = null,
) )

View File

@@ -31,7 +31,7 @@ material = "1.13.0-alpha10"
moshi = "1.15.2" moshi = "1.15.2"
okhttp = "4.12.0" okhttp = "4.12.0"
okio = "3.10.2" okio = "3.10.2"
parsers = "1.6" parsers = "ddb9b13df7"
preference = "1.2.1" preference = "1.2.1"
recyclerview = "1.4.0" recyclerview = "1.4.0"
room = "2.6.1" room = "2.6.1"