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
}