Merge branch 'feature/search_query'
This commit is contained in:
@@ -5,8 +5,6 @@ import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.domain
|
||||
import org.koitharu.kotatsu.parsers.util.mergeWith
|
||||
|
||||
private const val HEADER_REFERER = "Referer"
|
||||
|
||||
@@ -20,11 +18,7 @@ internal class CommonHeadersInterceptor : Interceptor {
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val sourceHeaders = parser?.getRequestHeaders()
|
||||
val headersBuilder = request.headers.newBuilder()
|
||||
if (sourceHeaders != null) {
|
||||
headersBuilder.mergeWith(sourceHeaders, replaceExisting = false)
|
||||
}
|
||||
if (headersBuilder[HEADER_REFERER] == null && parser != null) {
|
||||
headersBuilder[HEADER_REFERER] = "https://${parser.domain}/"
|
||||
}
|
||||
|
||||
@@ -5,8 +5,13 @@ import okhttp3.HttpUrl
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.koitharu.kotatsu.parsers.core.LegacyPagedMangaParser
|
||||
import org.koitharu.kotatsu.parsers.core.LegacySinglePageMangaParser
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.domain
|
||||
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
|
||||
import org.koitharu.kotatsu.parsers.model.search.QueryCriteria
|
||||
import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.Include
|
||||
import org.koitharu.kotatsu.parsers.model.search.SearchableField.*
|
||||
import org.koitharu.kotatsu.parsers.util.medianOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.mimeType
|
||||
import org.koitharu.kotatsu.test_util.*
|
||||
@@ -22,7 +27,7 @@ internal class MangaParserTest {
|
||||
@MangaSources
|
||||
fun list(source: MangaParserSource) = runTest(timeout = timeout) {
|
||||
val parser = context.newParserInstance(source)
|
||||
val list = parser.getList(0, parser.defaultSortOrder, MangaListFilter.EMPTY)
|
||||
val list = parser.getList(MangaSearchQuery.Builder().build())
|
||||
checkMangaList(list, "list")
|
||||
assert(list.all { it.source == source })
|
||||
}
|
||||
@@ -31,12 +36,13 @@ internal class MangaParserTest {
|
||||
@MangaSources
|
||||
fun pagination(source: MangaParserSource) = runTest(timeout = timeout) {
|
||||
val parser = context.newParserInstance(source)
|
||||
if (parser is SinglePageMangaParser) {
|
||||
if (parser is LegacySinglePageMangaParser) {
|
||||
return@runTest
|
||||
}
|
||||
val page1 = parser.getList(0, parser.defaultSortOrder, MangaListFilter.EMPTY)
|
||||
val page2 = parser.getList(page1.size, parser.defaultSortOrder, MangaListFilter.EMPTY)
|
||||
if (parser is PagedMangaParser) {
|
||||
val page1 = parser.getList(MangaSearchQuery.EMPTY)
|
||||
val page2 =
|
||||
parser.getList(MangaSearchQuery.Builder().offset(page1.size).build())
|
||||
if (parser is LegacyPagedMangaParser) {
|
||||
assert(parser.pageSize >= page1.size) {
|
||||
"Page size is ${page1.size} but ${parser.pageSize} expected"
|
||||
}
|
||||
@@ -52,18 +58,20 @@ internal class MangaParserTest {
|
||||
|
||||
@ParameterizedTest(name = "{index}|search|{0}")
|
||||
@MangaSources
|
||||
fun search(source: MangaParserSource) = runTest(timeout = timeout) {
|
||||
fun searchByTitleName(source: MangaParserSource) = runTest(timeout = timeout) {
|
||||
val parser = context.newParserInstance(source)
|
||||
val subject = parser.getList(
|
||||
offset = 0,
|
||||
order = SortOrder.POPULARITY,
|
||||
filter = MangaListFilter.EMPTY,
|
||||
).minByOrNull {
|
||||
val subject = parser.getList(MangaSearchQuery.EMPTY).minByOrNull {
|
||||
it.title.length
|
||||
} ?: error("No manga found")
|
||||
|
||||
val query = subject.title
|
||||
check(query.isNotBlank()) { "Manga title '$query' is blank" }
|
||||
val list = parser.getList(0, SortOrder.RELEVANCE, MangaListFilter(query = query))
|
||||
val list = parser.getList(
|
||||
MangaSearchQuery.Builder()
|
||||
.order(SortOrder.RELEVANCE)
|
||||
.criterion(QueryCriteria.Match(TITLE_NAME, query))
|
||||
.build(),
|
||||
)
|
||||
assert(list.isNotEmpty()) { "Empty search results by \"$query\"" }
|
||||
assert(list.singleOrNull { it.url == subject.url && it.id == subject.id } != null) {
|
||||
"Single subject '${subject.title} (${subject.publicUrl})' not found in search results"
|
||||
@@ -92,9 +100,10 @@ internal class MangaParserTest {
|
||||
|
||||
val tag = tags.last()
|
||||
val list = parser.getList(
|
||||
offset = 0,
|
||||
order = parser.defaultSortOrder,
|
||||
filter = MangaListFilter(tags = setOf(tag)),
|
||||
MangaSearchQuery.Builder()
|
||||
.offset(0)
|
||||
.criterion(Include(TAG, setOf(tag)))
|
||||
.build(),
|
||||
)
|
||||
checkMangaList(list, "${tag.title} (${tag.key})")
|
||||
assert(list.all { it.source == source })
|
||||
@@ -104,11 +113,16 @@ internal class MangaParserTest {
|
||||
@MangaSources
|
||||
fun tagsMultiple(source: MangaParserSource) = runTest(timeout = timeout) {
|
||||
val parser = context.newParserInstance(source)
|
||||
if (!parser.filterCapabilities.isMultipleTagsSupported) return@runTest
|
||||
// if (!parser.filterCapabilities.isMultipleTagsSupported) return@runTest
|
||||
val tags = parser.getFilterOptions().availableTags.shuffled().take(2).toSet()
|
||||
|
||||
val filter = MangaListFilter(tags = tags)
|
||||
val list = parser.getList(0, parser.defaultSortOrder, filter)
|
||||
val list = parser.getList(
|
||||
MangaSearchQuery.Builder()
|
||||
.offset(0)
|
||||
.criterion(Include(TAG, tags))
|
||||
.build(),
|
||||
)
|
||||
|
||||
checkMangaList(list, "${tags.joinToString { it.title }} (${tags.joinToString { it.key }})")
|
||||
assert(list.all { it.source == source })
|
||||
}
|
||||
@@ -121,12 +135,15 @@ internal class MangaParserTest {
|
||||
if (locales.isEmpty()) {
|
||||
return@runTest
|
||||
}
|
||||
val filter = MangaListFilter(
|
||||
locale = locales.random(),
|
||||
originalLocale = locales.random(),
|
||||
val locale = locales.random()
|
||||
val list = parser.getList(
|
||||
MangaSearchQuery.Builder()
|
||||
.criterion(Include(LANGUAGE, setOf(locale)))
|
||||
.criterion(Include(LANGUAGE, setOf(locale)))
|
||||
.criterion(Include(ORIGINAL_LANGUAGE, setOf(locales.random())))
|
||||
.build(),
|
||||
)
|
||||
val list = parser.getList(offset = 0, order = parser.defaultSortOrder, filter)
|
||||
checkMangaList(list, filter.locale.toString())
|
||||
checkMangaList(list, locale.toString())
|
||||
assert(list.all { it.source == source })
|
||||
}
|
||||
|
||||
@@ -135,8 +152,9 @@ internal class MangaParserTest {
|
||||
@MangaSources
|
||||
fun details(source: MangaParserSource) = runTest(timeout = timeout) {
|
||||
val parser = context.newParserInstance(source)
|
||||
val list = parser.getList(0, parser.defaultSortOrder, MangaListFilter.EMPTY)
|
||||
val manga = list[3]
|
||||
val list = parser.getList(MangaSearchQuery.EMPTY)
|
||||
|
||||
val manga = list[0]
|
||||
parser.getDetails(manga).apply {
|
||||
assert(!chapters.isNullOrEmpty()) { "Chapters are null or empty" }
|
||||
assert(publicUrl.isUrlAbsolute()) { "Manga public url is not absolute: '$publicUrl'" }
|
||||
@@ -165,7 +183,7 @@ internal class MangaParserTest {
|
||||
@MangaSources
|
||||
fun pages(source: MangaParserSource) = runTest(timeout = timeout) {
|
||||
val parser = context.newParserInstance(source)
|
||||
val list = parser.getList(0, parser.defaultSortOrder, MangaListFilter.EMPTY)
|
||||
val list = parser.getList(MangaSearchQuery.EMPTY)
|
||||
val manga = list.first()
|
||||
val chapter = parser.getDetails(manga).chapters?.firstOrNull() ?: error("Chapter is null at ${manga.publicUrl}")
|
||||
val pages = parser.getPages(chapter)
|
||||
@@ -220,7 +238,7 @@ internal class MangaParserTest {
|
||||
@MangaSources
|
||||
fun link(source: MangaParserSource) = runTest(timeout = timeout) {
|
||||
val parser = context.newParserInstance(source)
|
||||
val manga = parser.getList(0, parser.defaultSortOrder, MangaListFilter.EMPTY).first()
|
||||
val manga = parser.getList(MangaSearchQuery.Builder().build()).first()
|
||||
val resolved = context.newLinkResolver(manga.publicUrl).getManga()
|
||||
Assertions.assertNotNull(resolved)
|
||||
resolved ?: return@runTest
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.koitharu.kotatsu.parsers.model.search
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.*
|
||||
import org.koitharu.kotatsu.parsers.model.search.SearchableField.*
|
||||
import java.util.*
|
||||
|
||||
class MangaSearchQueryCapabilitiesTest {
|
||||
|
||||
private val capabilities = MangaSearchQueryCapabilities(
|
||||
capabilities = setOf(
|
||||
SearchCapability(TITLE_NAME, setOf(Match::class), multiValue = false, otherCriteria = false),
|
||||
SearchCapability(TAG, setOf(Include::class, Exclude::class), multiValue = true, otherCriteria = true),
|
||||
SearchCapability(PUBLICATION_YEAR, setOf(Range::class), multiValue = false, otherCriteria = true),
|
||||
SearchCapability(STATE, setOf(Include::class), multiValue = false, otherCriteria = true),
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun validateValidSingleCriterionQuery() {
|
||||
val query = MangaSearchQuery.Builder()
|
||||
.criterion(Match(TITLE_NAME, "title"))
|
||||
.build()
|
||||
|
||||
assertDoesNotThrow { capabilities.validate(query) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateUnsupportedFieldThrowsException() {
|
||||
val query = MangaSearchQuery.Builder()
|
||||
.criterion(Include(ORIGINAL_LANGUAGE, setOf(Locale.ENGLISH)))
|
||||
.build()
|
||||
|
||||
assertThrows(IllegalArgumentException::class.java) { capabilities.validate(query) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateUnsupportedMultiValueThrowsException() {
|
||||
val query = MangaSearchQuery.Builder()
|
||||
.criterion(Include(STATE, setOf(MangaState.ONGOING, MangaState.FINISHED)))
|
||||
.build()
|
||||
|
||||
assertThrows(IllegalArgumentException::class.java) { capabilities.validate(query) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateMultipleCriteriaWithOtherCriteriaAllowed() {
|
||||
val query = MangaSearchQuery.Builder()
|
||||
.criterion(Include(TAG, setOf(buildTag("tag1"), buildTag("tag2"))))
|
||||
.criterion(Exclude(TAG, setOf(buildTag("tag3"))))
|
||||
.build()
|
||||
|
||||
assertDoesNotThrow { capabilities.validate(query) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateMultipleCriteriaWithStrictCapabilityThrowsException() {
|
||||
val query = MangaSearchQuery.Builder()
|
||||
.criterion(Match(TITLE_NAME, "title"))
|
||||
.criterion(Range(PUBLICATION_YEAR, 1990, 2000))
|
||||
.build()
|
||||
|
||||
assertThrows(IllegalArgumentException::class.java) { capabilities.validate(query) }
|
||||
}
|
||||
|
||||
private fun buildTag(name: String) = MangaTag(title = name, key = "${name}Key", source = MangaParserSource.DUMMY)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.parsers.util
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.koitharu.kotatsu.parsers.core.LegacyMangaParser
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContextMock
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.newParser
|
||||
@@ -25,7 +26,7 @@ class IntentFilterGenerator {
|
||||
if (source == MangaParserSource.DUMMY) {
|
||||
continue
|
||||
}
|
||||
val parser = source.newParser(MangaLoaderContextMock)
|
||||
val parser = source.newParser(MangaLoaderContextMock) as LegacyMangaParser
|
||||
parser.configKeyDomain.presetValues.forEach { domain ->
|
||||
writer.appendTab().append("<data android:host=\"").append(domain).appendLine("\" />")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.koitharu.kotatsu.parsers.util
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType.MANGA
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType.MANHUA
|
||||
import org.koitharu.kotatsu.parsers.model.Demographic.SEINEN
|
||||
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
|
||||
import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.*
|
||||
import org.koitharu.kotatsu.parsers.model.search.SearchableField.*
|
||||
import java.util.*
|
||||
|
||||
class ListFilterToSearchQueryConverterTest {
|
||||
|
||||
@Test
|
||||
fun convertToMangaSearchQueryTest() {
|
||||
val tags = setOf(buildMangaTag("tag1"), buildMangaTag("tag2"))
|
||||
val excludedTags = setOf(buildMangaTag("exclude_tag"))
|
||||
val states = setOf(MangaState.ONGOING)
|
||||
val contentRatings = setOf(ContentRating.SAFE)
|
||||
val contentTypes = setOf(MANGA, MANHUA)
|
||||
val demographics = setOf(SEINEN)
|
||||
|
||||
val filter = MangaListFilter(
|
||||
query = "title_name",
|
||||
tags = tags,
|
||||
tagsExclude = excludedTags,
|
||||
locale = Locale.ENGLISH,
|
||||
originalLocale = Locale.JAPANESE,
|
||||
states = states,
|
||||
contentRating = contentRatings,
|
||||
types = contentTypes,
|
||||
demographics = demographics,
|
||||
year = 2020,
|
||||
yearFrom = 1997,
|
||||
yearTo = 2024,
|
||||
)
|
||||
|
||||
val searchQuery = convertToMangaSearchQuery(0, SortOrder.NEWEST, filter)
|
||||
|
||||
val expectedQuery = MangaSearchQuery.Builder()
|
||||
.offset(0)
|
||||
.order(SortOrder.NEWEST)
|
||||
.criterion(Match(TITLE_NAME, "title_name"))
|
||||
.criterion(Include(TAG, tags))
|
||||
.criterion(Exclude(TAG, excludedTags))
|
||||
.criterion(Include(LANGUAGE, setOf(Locale.ENGLISH)))
|
||||
.criterion(Include(ORIGINAL_LANGUAGE, setOf(Locale.JAPANESE)))
|
||||
.criterion(Include(STATE, states))
|
||||
.criterion(Include(CONTENT_RATING, contentRatings))
|
||||
.criterion(Include(CONTENT_TYPE, contentTypes))
|
||||
.criterion(Include(DEMOGRAPHIC, demographics))
|
||||
.criterion(Range(PUBLICATION_YEAR, 1997, 2024))
|
||||
.criterion(Match(PUBLICATION_YEAR, 2020))
|
||||
.build()
|
||||
|
||||
assertEquals(expectedQuery, searchQuery)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun convertToMangaSearchQueryWithEmptyFieldsTest() {
|
||||
val filter = MangaListFilter()
|
||||
|
||||
val searchQuery = convertToMangaSearchQuery(0, SortOrder.NEWEST, filter)
|
||||
|
||||
assertEquals(MangaSearchQuery.Builder().offset(0).order(SortOrder.NEWEST).build(), searchQuery)
|
||||
}
|
||||
|
||||
private fun buildMangaTag(name: String): MangaTag {
|
||||
return MangaTag(
|
||||
key = "${name}Key",
|
||||
title = name,
|
||||
source = MangaParserSource.DUMMY,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.koitharu.kotatsu.parsers.util
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType.MANGA
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType.MANHUA
|
||||
import org.koitharu.kotatsu.parsers.model.Demographic.SEINEN
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
|
||||
import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.*
|
||||
import org.koitharu.kotatsu.parsers.model.search.SearchableField.*
|
||||
import java.util.*
|
||||
|
||||
class ConvertToMangaListFilterTest {
|
||||
|
||||
@Test
|
||||
fun convertToMangaListFilterTest() {
|
||||
val tags = setOf(buildMangaTag("tag1"), buildMangaTag("tag2"))
|
||||
val excludedTags = setOf(buildMangaTag("exclude_tag"))
|
||||
val states = setOf(MangaState.ONGOING)
|
||||
val contentRatings = setOf(ContentRating.SAFE)
|
||||
val contentTypes = setOf(MANGA, MANHUA)
|
||||
val demographics = setOf(SEINEN)
|
||||
|
||||
val query = MangaSearchQuery.Builder()
|
||||
.criterion(Match(TITLE_NAME, "title_name"))
|
||||
.criterion(Include(TAG, tags))
|
||||
.criterion(Exclude(TAG, excludedTags))
|
||||
.criterion(Include(LANGUAGE, setOf(Locale.ENGLISH)))
|
||||
.criterion(Include(ORIGINAL_LANGUAGE, setOf(Locale.JAPANESE)))
|
||||
.criterion(Include(STATE, states))
|
||||
.criterion(Include(CONTENT_RATING, contentRatings))
|
||||
.criterion(Include(CONTENT_TYPE, contentTypes))
|
||||
.criterion(Include(DEMOGRAPHIC, demographics))
|
||||
.criterion(Range(PUBLICATION_YEAR, 1997, 2024))
|
||||
.criterion(Match(PUBLICATION_YEAR, 2020))
|
||||
.build()
|
||||
|
||||
val listFilter = convertToMangaListFilter(query)
|
||||
|
||||
assertEquals(listFilter.query, "title_name")
|
||||
assertEquals(listFilter.tags, tags)
|
||||
assertEquals(listFilter.tagsExclude, excludedTags)
|
||||
assertEquals(listFilter.locale, Locale.ENGLISH)
|
||||
assertEquals(listFilter.originalLocale, Locale.JAPANESE)
|
||||
assertEquals(listFilter.states, states)
|
||||
assertEquals(listFilter.contentRating, contentRatings)
|
||||
assertEquals(listFilter.types, contentTypes)
|
||||
assertEquals(listFilter.demographics, demographics)
|
||||
assertEquals(listFilter.year, 2020)
|
||||
assertEquals(listFilter.yearFrom, 1997)
|
||||
assertEquals(listFilter.yearTo, 2024)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun convertToMangaListFilterWithMultipleTagsIncludeTest() {
|
||||
val tags1 = setOf(buildMangaTag("tag1"), buildMangaTag("tag2"))
|
||||
val tags2 = setOf(buildMangaTag("tag3"), buildMangaTag("tag4"))
|
||||
|
||||
val query = MangaSearchQuery.Builder()
|
||||
.criterion(Include(TAG, tags1))
|
||||
.criterion(Include(TAG, tags2))
|
||||
.build()
|
||||
|
||||
val listFilter = convertToMangaListFilter(query)
|
||||
|
||||
assertEquals(listFilter.tags, tags1 union tags2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun convertToMangaListFilterWithUnsupportedFieldTest() {
|
||||
val query = MangaSearchQuery.Builder()
|
||||
.criterion(Include(AUTHOR, setOf(buildMangaTag("author"))))
|
||||
.build()
|
||||
|
||||
val exception = assertThrows<IllegalArgumentException> {
|
||||
convertToMangaListFilter(query)
|
||||
}
|
||||
|
||||
assert(exception.message!!.contains("Unsupported field for Include criterion: AUTHOR"))
|
||||
}
|
||||
|
||||
private fun buildMangaTag(name: String): MangaTag {
|
||||
return MangaTag(
|
||||
key = "${name}Key",
|
||||
title = name,
|
||||
source = MangaParserSource.DUMMY,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user