Simple related manga search implementation
This commit is contained in:
@@ -8,6 +8,7 @@ import org.koitharu.kotatsu.parsers.network.OkHttpWebClient
|
||||
import org.koitharu.kotatsu.parsers.network.UserAgents
|
||||
import org.koitharu.kotatsu.parsers.network.WebClient
|
||||
import org.koitharu.kotatsu.parsers.util.FaviconParser
|
||||
import org.koitharu.kotatsu.parsers.util.RelatedMangaFinder
|
||||
import org.koitharu.kotatsu.parsers.util.domain
|
||||
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
|
||||
import java.util.*
|
||||
@@ -127,4 +128,14 @@ abstract class MangaParser @InternalParsersApi constructor(
|
||||
open fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
|
||||
keys.add(configKeyDomain)
|
||||
}
|
||||
|
||||
open suspend fun getRelatedManga(seed: Manga): List<Manga> {
|
||||
return RelatedMangaFinder(listOf(this)).invoke(seed)
|
||||
}
|
||||
|
||||
protected fun getParser(source: MangaSource) = if (this.source == source) {
|
||||
this
|
||||
} else {
|
||||
context.newParserInstance(source)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ internal abstract class GroupleParser(
|
||||
mapOf(
|
||||
"q" to query.urlEncoded(),
|
||||
"offset" to (offset upBy PAGE_SIZE_SEARCH).toString(),
|
||||
"fast-filter" to "CREATION",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -321,6 +322,15 @@ internal abstract class GroupleParser(
|
||||
keys.add(userAgentKey)
|
||||
}
|
||||
|
||||
override suspend fun getRelatedManga(seed: Manga): List<Manga> {
|
||||
val parsers = listOf(
|
||||
getParser(MangaSource.READMANGA_RU),
|
||||
getParser(MangaSource.MINTMANGA),
|
||||
getParser(MangaSource.SELFMANGA),
|
||||
)
|
||||
return RelatedMangaFinder(parsers).invoke(seed)
|
||||
}
|
||||
|
||||
private fun getSortKey(sortOrder: SortOrder) =
|
||||
when (sortOrder) {
|
||||
SortOrder.ALPHABETICAL -> "name"
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
|
||||
@MangaSourceParser("ATLANTISSCAN", "Atlantisscan", "pt")
|
||||
internal class Atlantisscan(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaSource.ATLANTISSCAN, "atlantisscan.com") {
|
||||
MadaraParser(context, MangaSource.ATLANTISSCAN, "br.atlantisscan.com", pageSize = 10) {
|
||||
|
||||
override val datePattern = "dd/MM/yyyy"
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.koitharu.kotatsu.parsers.util
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import org.koitharu.kotatsu.parsers.MangaParser
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
class RelatedMangaFinder(
|
||||
private val parsers: Collection<MangaParser>,
|
||||
) {
|
||||
|
||||
private val regexWhitespace = Regex("\\s+")
|
||||
|
||||
suspend operator fun invoke(seed: Manga): List<Manga> = coroutineScope {
|
||||
parsers.singleOrNull()?.let { parser ->
|
||||
findRelatedImpl(this, parser, seed)
|
||||
} ?: parsers.map { parser ->
|
||||
async {
|
||||
findRelatedImpl(this, parser, seed)
|
||||
}
|
||||
}.awaitAll().flatten()
|
||||
}
|
||||
|
||||
private suspend fun findRelatedImpl(scope: CoroutineScope, parser: MangaParser, seed: Manga): List<Manga> {
|
||||
val words = HashSet<String>()
|
||||
words += seed.title.split(regexWhitespace)
|
||||
seed.altTitle?.let {
|
||||
words += it.split(regexWhitespace)
|
||||
}
|
||||
if (words.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
val results = words.map { keyword ->
|
||||
scope.async {
|
||||
val result = parser.getList(0, keyword)
|
||||
result.filter { it.id != seed.id && it.containKeyword(keyword) }
|
||||
}
|
||||
}.awaitAll()
|
||||
return results.minBy { if (it.isEmpty()) Int.MAX_VALUE else it.size }
|
||||
}
|
||||
|
||||
private fun Manga.containKeyword(keyword: String): Boolean {
|
||||
return title.contains(keyword, ignoreCase = true) || altTitle?.contains(keyword, ignoreCase = true) == true
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
package org.koitharu.kotatsu.parsers.util
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.collection.arraySetOf
|
||||
import java.math.BigInteger
|
||||
import java.net.URLEncoder
|
||||
@@ -209,6 +210,18 @@ fun String.levenshteinDistance(other: String): Int {
|
||||
return cost[lhsLength - 1]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param threshold 0 = exact match
|
||||
*/
|
||||
fun String.almostEquals(other: String, @FloatRange(from = 0.0) threshold: Float): Boolean {
|
||||
if (threshold <= 0f) {
|
||||
return equals(other, ignoreCase = true)
|
||||
}
|
||||
val diff = lowercase().levenshteinDistance(other.lowercase()) / ((length + other.length) / 2f)
|
||||
return diff < threshold
|
||||
}
|
||||
|
||||
|
||||
inline fun <T> Appendable.appendAll(
|
||||
items: Iterable<T>,
|
||||
separator: CharSequence,
|
||||
@@ -223,4 +236,4 @@ inline fun <T> Appendable.appendAll(
|
||||
}
|
||||
append(transform(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.parsers
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.HttpUrl
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -22,6 +23,14 @@ internal class MangaParserTest {
|
||||
|
||||
private val context = MangaLoaderContextMock
|
||||
|
||||
@Test
|
||||
fun related() = runTest {
|
||||
val parser = context.newParserInstance(MangaSource.READMANGA_RU)
|
||||
val seed = parser.getList(0, "emanon").first()
|
||||
val related = parser.getRelatedManga(seed)
|
||||
assert(related.isNotEmpty() && seed !in related)
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{index}|list|{0}")
|
||||
@MangaSources
|
||||
fun list(source: MangaSource) = runTest {
|
||||
|
||||
Reference in New Issue
Block a user