From 6fa99791b6be2380dd57bf0dc14a9411feea74eb Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 15 Aug 2023 12:46:46 +0300 Subject: [PATCH] Handle manga links --- app/src/main/AndroidManifest.xml | 665 ++++++++++++++++++ .../koitharu/kotatsu/core/db/dao/MangaDao.kt | 4 + .../org/koitharu/kotatsu/core/model/Manga.kt | 8 + .../core/parser/MangaDataRepository.kt | 9 +- .../kotatsu/core/parser/MangaLinkResolver.kt | 119 ++++ .../core/parser/RemoteMangaRepository.kt | 24 +- .../koitharu/kotatsu/core/util/ShareHelper.kt | 3 + .../koitharu/kotatsu/core/util/ext/String.kt | 2 +- 8 files changed, 824 insertions(+), 10 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLinkResolver.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ead6c4c9c..b10f2b619 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -71,6 +71,17 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaDao.kt index 7f078bb7f..404fa10c3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaDao.kt @@ -19,6 +19,10 @@ abstract class MangaDao { @Query("SELECT * FROM manga WHERE manga_id = :id") abstract suspend fun find(id: Long): MangaWithTags? + @Transaction + @Query("SELECT * FROM manga WHERE public_url = :publicUrl") + abstract suspend fun findByPublicUrl(publicUrl: String): MangaWithTags? + @Transaction @Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit") abstract suspend fun searchByTitle(query: String, limit: Int): List diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt index 6314c0c6e..2d7101042 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.core.model +import android.net.Uri import androidx.core.os.LocaleListCompat import org.koitharu.kotatsu.core.util.ext.iterator import org.koitharu.kotatsu.details.ui.model.ChapterListItem @@ -66,3 +67,10 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? { val Manga.isLocal: Boolean get() = source == MangaSource.LOCAL + +val Manga.appUrl: Uri + get() = Uri.parse("https://kotatsu.app/manga").buildUpon() + .appendQueryParameter("source", source.name) + .appendQueryParameter("name", title) + .appendQueryParameter("url", url) + .build() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaDataRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaDataRepository.kt index 81b13ada2..f110024ef 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaDataRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaDataRepository.kt @@ -17,10 +17,12 @@ import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.reader.domain.ReaderColorFilter import javax.inject.Inject +import javax.inject.Provider @Reusable class MangaDataRepository @Inject constructor( private val db: MangaDatabase, + private val resolverProvider: Provider, ) { suspend fun saveReaderMode(manga: Manga, mode: ReaderMode) { @@ -63,10 +65,15 @@ class MangaDataRepository @Inject constructor( return db.mangaDao.find(mangaId)?.toManga() } + suspend fun findMangaByPublicUrl(publicUrl: String): Manga? { + return db.mangaDao.findByPublicUrl(publicUrl)?.toManga() + } + suspend fun resolveIntent(intent: MangaIntent): Manga? = when { intent.manga != null -> intent.manga intent.mangaId != 0L -> findMangaById(intent.mangaId) - else -> null // TODO resolve uri + intent.uri != null -> resolverProvider.get().resolve(intent.uri) + else -> null } suspend fun storeManga(manga: Manga) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLinkResolver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLinkResolver.kt new file mode 100644 index 000000000..f4f17390d --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLinkResolver.kt @@ -0,0 +1,119 @@ +package org.koitharu.kotatsu.core.parser + +import android.net.Uri +import dagger.Reusable +import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty +import org.koitharu.kotatsu.explore.data.MangaSourcesRepository +import org.koitharu.kotatsu.parsers.exception.NotFoundException +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.almostEquals +import org.koitharu.kotatsu.parsers.util.levenshteinDistance +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.parsers.util.toRelativeUrl +import javax.inject.Inject + +@Reusable +class MangaLinkResolver @Inject constructor( + private val repositoryFactory: MangaRepository.Factory, + private val sourcesRepository: MangaSourcesRepository, + private val dataRepository: MangaDataRepository, +) { + + suspend fun resolve(uri: Uri): Manga { + return if (uri.host == "kotatsu.app") { + resolveAppLink(uri) + } else { + resolveExternalLink(uri) + } ?: throw NotFoundException("Manga not found", uri.toString()) + } + + suspend fun resolveAppLink(uri: Uri): Manga? { + require(uri.pathSegments.singleOrNull() == "manga") { "Invalid url" } + val sourceName = requireNotNull(uri.getQueryParameter("source")) { "Source is not specified" } + val source = MangaSource(sourceName) + require(source != MangaSource.DUMMY) { "Manga source $sourceName is not supported" } + val repo = repositoryFactory.create(source) + return repo.findExact( + url = uri.getQueryParameter("url"), + title = uri.getQueryParameter("name"), + ) + } + + suspend fun resolveExternalLink(uri: Uri): Manga? { + dataRepository.findMangaByPublicUrl(uri.toString())?.let { + return it + } + val host = uri.host ?: return null + val repo = sourcesRepository.allMangaSources.asSequence() + .map { source -> + repositoryFactory.create(source) as RemoteMangaRepository + }.find { repo -> + host in repo.domains + } ?: return null + return repo.findExact(uri.toString().toRelativeUrl(host), null) + } + + private suspend fun MangaRepository.findExact(url: String?, title: String?): Manga? { + if (!title.isNullOrEmpty()) { + val list = getList(0, title) + if (url != null) { + list.find { it.url == url }?.let { + return it + } + } + list.minByOrNull { it.title.levenshteinDistance(title) } + ?.takeIf { it.title.almostEquals(title, 0.2f) } + ?.let { return it } + } + val seed = getDetailsNoCache( + getSeedManga(source, url ?: return null, title), + ) + return runCatchingCancellable { + val seedTitle = seed.title.ifEmpty { + seed.altTitle + }.ifNullOrEmpty { + seed.author + } ?: return@runCatchingCancellable null + val seedList = getList(0, seedTitle) + seedList.first { x -> x.url == url } + }.getOrThrow() + } + + private suspend fun MangaRepository.getDetailsNoCache(manga: Manga): Manga { + return if (this is RemoteMangaRepository) { + getDetails(manga, withCache = false) + } else { + getDetails(manga) + } + } + + private fun getSeedManga(source: MangaSource, url: String, title: String?) = Manga( + id = run { + var h = 1125899906842597L + source.name.forEach { c -> + h = 31 * h + c.code + } + url.forEach { c -> + h = 31 * h + c.code + } + h + }, + title = title.orEmpty(), + altTitle = null, + url = url, + publicUrl = "", + rating = 0.0f, + isNsfw = source.contentType == ContentType.HENTAI, + coverUrl = "", + tags = emptySet(), + state = null, + author = null, + largeCoverUrl = null, + description = null, + chapters = null, + source = source, + ) +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index 84ad71d5f..e52b7e011 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt @@ -51,6 +51,9 @@ class RemoteMangaRepository( getConfig()[parser.configKeyDomain] = value } + val domains: Array + get() = parser.configKeyDomain.presetValues + val headers: Headers get() = parser.headers @@ -70,14 +73,7 @@ class RemoteMangaRepository( return parser.getList(offset, tags, sortOrder) } - override suspend fun getDetails(manga: Manga): Manga { - cache.getDetails(source, manga.url)?.let { return it } - val details = asyncSafe { - parser.getDetails(manga) - } - cache.putDetails(source, manga.url, details) - return details.await() - } + override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, withCache = true) override suspend fun getPages(chapter: MangaChapter): List { cache.getPages(source, chapter.url)?.let { return it } @@ -103,6 +99,18 @@ class RemoteMangaRepository( return related.await() } + suspend fun getDetails(manga: Manga, withCache: Boolean): Manga { + if (!withCache) { + return parser.getDetails(manga) + } + cache.getDetails(source, manga.url)?.let { return it } + val details = asyncSafe { + parser.getDetails(manga) + } + cache.putDetails(source, manga.url, details) + return details.await() + } + fun getAuthProvider(): MangaParserAuthProvider? = parser as? MangaParserAuthProvider fun getConfigKeys(): List> = ArrayList>().also { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ShareHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ShareHelper.kt index 57d5e7c80..c16502090 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ShareHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ShareHelper.kt @@ -8,6 +8,7 @@ import androidx.core.content.FileProvider import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.logs.FileLogger +import org.koitharu.kotatsu.core.model.appUrl import org.koitharu.kotatsu.parsers.model.Manga import java.io.File @@ -22,6 +23,8 @@ class ShareHelper(private val context: Context) { append(manga.title) append("\n \n") append(manga.publicUrl) + append("\n \n") + append(manga.appUrl) } ShareCompat.IntentBuilder(context) .setText(text) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt index b0124dd74..5fe420064 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt @@ -4,7 +4,7 @@ import androidx.annotation.FloatRange import org.koitharu.kotatsu.parsers.util.levenshteinDistance import java.util.UUID -inline fun C?.ifNullOrEmpty(defaultValue: () -> C): C { +inline fun C?.ifNullOrEmpty(defaultValue: () -> C): C { return if (this.isNullOrEmpty()) defaultValue() else this }