diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/grouple/GroupleParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/grouple/GroupleParser.kt index 40a2a0a2..cc85cd1d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/grouple/GroupleParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/grouple/GroupleParser.kt @@ -1,9 +1,11 @@ package org.koitharu.kotatsu.parsers.site.grouple -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.Interceptor import okhttp3.Response import okhttp3.internal.headersContentLength @@ -236,30 +238,42 @@ internal abstract class GroupleParser( override suspend fun getPageUrl(page: MangaPage): String { val parts = page.url.split('|') + if (parts.size < 2) { + throw ParseException("No servers found for page", page.url) + } val path = parts.last() - val servers = parts.dropLast(1).toSet() - val cachedServer = cachedPagesServer - if (!cachedServer.isNullOrEmpty() && cachedServer in servers && tryHead(concatUrl(cachedServer, path))) { - return concatUrl(cachedServer, path) + // fast path + cachedPagesServer?.let { host -> + val url = concatUrl("https://$host/", path) + if (tryHead(url)) { + return url + } else { + cachedPagesServer = null + } } - if (servers.isEmpty()) { - throw ParseException("No servers found for page", page.url) + // slow path + val candidates = HashSet((parts.size - 1) * 2) + for (i in 0 until parts.size - 1) { + val server = parts[i].trim().ifEmpty { "https://$domain/" } + candidates.add(concatUrl(server, path)) + candidates.add(concatUrl(server, path.substringBeforeLast('?'))) } - val server = try { - coroutineScope { - servers.map { server -> - async { - val host = server.trim().ifEmpty { "https://$domain/" } - if (tryHead(concatUrl(host, path))) host else null + return try { + channelFlow { + for (url in candidates) { + launch { + if (tryHead(url)) { + send(url) + } } - }.awaitFirst { it != null } - }.also { - cachedPagesServer = it + } + }.first().also { + cachedPagesServer = it.toHttpUrlOrNull()?.host } } catch (e: NoSuchElementException) { - servers.random() + assert(false) { e.toString() } + candidates.random() } - return concatUrl(checkNotNull(server).ifEmpty { "https://$domain/" }, path) } override suspend fun getTags(): Set { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Coroutines.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Coroutines.kt index 52fb8b17..fffb94f2 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Coroutines.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Coroutines.kt @@ -2,39 +2,34 @@ package org.koitharu.kotatsu.parsers.util import kotlinx.coroutines.Deferred import kotlinx.coroutines.Job -import kotlinx.coroutines.selects.select +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import kotlin.coroutines.cancellation.CancellationException fun Iterable.cancelAll(cause: CancellationException? = null) { forEach { it.cancel(cause) } } -suspend fun Iterable>.awaitFirst(): T = select { - for (async in this@awaitFirst) { - async.onAwait { it } - } -}.also { this@awaitFirst.cancelAll() } +suspend fun Iterable>.awaitFirst(): T { + return channelFlow { + for (deferred in this@awaitFirst) { + launch { + send(deferred.await()) + } + } + }.first().also { this@awaitFirst.cancelAll() } +} suspend fun Collection>.awaitFirst(condition: (T) -> Boolean): T { - var result: Any? = NULL - var counter = size - while (result === NULL && counter > 0) { - val candidate = select { - for (async in this@awaitFirst) { - async.onAwait { it } + return channelFlow { + for (deferred in this@awaitFirst) { + launch { + val result = deferred.await() + if (condition(result)) { + send(result) + } } } - if (condition(candidate)) { - result = candidate - } - counter-- - } - cancelAll() - if (result === NULL) { - throw NoSuchElementException() - } - @Suppress("UNCHECKED_CAST") - return result as T + }.first().also { this@awaitFirst.cancelAll() } } - -private val NULL = Any() diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt index 4c508ad5..84d7343a 100644 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt @@ -114,20 +114,24 @@ internal class MangaParserTest { @MangaSources fun pages(source: MangaSource) = runTest { val parser = source.newParser(context) - val list = parser.getList(0, sortOrder = SortOrder.POPULARITY, tags = null) + val list = parser.getList(0, sortOrder = SortOrder.UPDATED, tags = null) val manga = list.first() - val chapter = parser.getDetails(manga).chapters?.firstOrNull() ?: error("Chapter is null") + val chapter = parser.getDetails(manga).chapters?.firstOrNull() ?: error("Chapter is null at ${manga.publicUrl}") val pages = parser.getPages(chapter) assert(pages.isNotEmpty()) assert(pages.isDistinctBy { it.id }) assert(pages.all { it.source == source }) - val page = pages.medianOrNull() ?: error("No page") - val pageUrl = parser.getPageUrl(page) - assert(pageUrl.isNotEmpty()) - assert(pageUrl.isUrlAbsolute()) - checkImageRequest(pageUrl, page.source) + arrayOf( + pages.first(), + pages.medianOrNull() ?: error("No page"), + ).forEach { page -> + val pageUrl = parser.getPageUrl(page) + assert(pageUrl.isNotEmpty()) + assert(pageUrl.isUrlAbsolute()) + checkImageRequest(pageUrl, page.source) + } } @ParameterizedTest(name = "{index}|favicon|{0}")