From 4d1d90a07b47a94f8916d72f3542b5cb11bed1ff Mon Sep 17 00:00:00 2001 From: Federico d'Alonzo Date: Thu, 2 Nov 2023 02:10:14 +0100 Subject: [PATCH] New Source: Project Suki (#18774) * projectsuki initial commit * update preferences * non-lazy client * buildMap -> mapOf Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com> * inline constants * switched from custom NormalizedURL to HttpUrl * band-aid fix for "No results found" Has edge case where current page has 30 results and next page has 0 results. * update remote & strip debug Log statements --------- Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com> --- src/all/projectsuki/AndroidManifest.xml | 35 ++ src/all/projectsuki/build.gradle | 15 + .../res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3028 bytes .../res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1802 bytes .../res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3798 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6732 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9113 bytes src/all/projectsuki/res/web_hi_res_512.png | Bin 0 -> 30746 bytes .../all/projectsuki/NormalizedURL.kt | 54 +++ .../tachiyomi/extension/all/projectsuki/PS.kt | 129 +++++ .../extension/all/projectsuki/PSBook.kt | 11 + .../extension/all/projectsuki/PSFilters.kt | 90 ++++ .../extension/all/projectsuki/ProjectSuki.kt | 443 ++++++++++++++++++ .../all/projectsuki/ProjectSukiUrlActivity.kt | 33 ++ 14 files changed, 810 insertions(+) create mode 100644 src/all/projectsuki/AndroidManifest.xml create mode 100644 src/all/projectsuki/build.gradle create mode 100644 src/all/projectsuki/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/all/projectsuki/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/all/projectsuki/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/all/projectsuki/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/all/projectsuki/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/all/projectsuki/res/web_hi_res_512.png create mode 100644 src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/NormalizedURL.kt create mode 100644 src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/PS.kt create mode 100644 src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/PSBook.kt create mode 100644 src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/PSFilters.kt create mode 100644 src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/ProjectSuki.kt create mode 100644 src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/ProjectSukiUrlActivity.kt diff --git a/src/all/projectsuki/AndroidManifest.xml b/src/all/projectsuki/AndroidManifest.xml new file mode 100644 index 000000000..867eb056f --- /dev/null +++ b/src/all/projectsuki/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/all/projectsuki/build.gradle b/src/all/projectsuki/build.gradle new file mode 100644 index 000000000..5ea5dc348 --- /dev/null +++ b/src/all/projectsuki/build.gradle @@ -0,0 +1,15 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Project Suki' + pkgNameSuffix = 'all.projectsuki' + extClass = '.ProjectSuki' + extVersionCode = 1 +} + +dependencies { + implementation(project(":lib-randomua")) +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/projectsuki/res/mipmap-hdpi/ic_launcher.png b/src/all/projectsuki/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..57b7a52966e14ee0c7a054cda8214c2c7baf25cb GIT binary patch literal 3028 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 Wnz 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 NQ`Wll_=08N{{g+wC(!@^ literal 0 HcmV?d00001 diff --git a/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/NormalizedURL.kt b/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/NormalizedURL.kt new file mode 100644 index 000000000..3f6840dde --- /dev/null +++ b/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/NormalizedURL.kt @@ -0,0 +1,54 @@ +package eu.kanade.tachiyomi.extension.all.projectsuki + +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import org.jsoup.nodes.Element + +typealias NormalizedURL = HttpUrl + +val NormalizedURL.rawAbsolute: String + get() = toString() + +private val psDomainURI = """https://projectsuki.com/""".toHttpUrl().toUri() + +val NormalizedURL.rawRelative: String? + get() { + val uri = toUri() + return psDomainURI + .relativize(uri) + .takeIf { it != uri } + ?.let { """/$it""" } + } + +private val protocolMatcher = """^https?://""".toRegex() +private val domainMatcher = """^https?://(?:[a-zA-Z\d\-]+\.)+[a-zA-Z\d\-]+""".toRegex() +fun String.toNormalURL(): NormalizedURL? { + if (contains(':') && !contains(protocolMatcher)) { + return null + } + + val toParse = StringBuilder() + + if (!contains(domainMatcher)) { + toParse.append("https://projectsuki.com") + if (!this.startsWith("/")) toParse.append('/') + } + + toParse.append(this) + + return toParse.toString().toHttpUrlOrNull() +} + +fun NormalizedURL.pathStartsWith(other: Iterable): Boolean = pathSegments.zip(other).all { (l, r) -> l == r } + +fun NormalizedURL.isPSUrl() = host.endsWith("${PS.identifier}.com") + +fun NormalizedURL.isBookURL() = isPSUrl() && pathSegments.first() == "book" +fun NormalizedURL.isReadURL() = isPSUrl() && pathStartsWith(PS.chapterPath) +fun NormalizedURL.isImagesGalleryURL() = isPSUrl() && pathStartsWith(PS.pagePath) + +fun Element.attrNormalizedUrl(attrName: String): NormalizedURL? { + val attrValue = attr("abs:$attrName").takeIf { it.isNotBlank() } ?: return null + return attrValue.toNormalURL() +} diff --git a/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/PS.kt b/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/PS.kt new file mode 100644 index 000000000..68c4dd935 --- /dev/null +++ b/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/PS.kt @@ -0,0 +1,129 @@ +@file:Suppress("MayBeConstant", "unused") + +package eu.kanade.tachiyomi.extension.all.projectsuki + +import org.jsoup.nodes.Element +import java.util.Calendar +import java.util.Locale +import kotlin.concurrent.getOrSet + +@Suppress("MemberVisibilityCanBePrivate") +internal object PS { + const val identifier: String = "projectsuki" + const val identifierShort: String = "ps" + + val bookPath = listOf("book") + val pagePath = listOf("images", "gallery") + val chapterPath = listOf("read") + + const val SEARCH_INTENT_PREFIX: String = "$identifierShort:" + + const val PREFERENCE_WHITELIST_LANGUAGES = "$identifier-languages-whitelist" + const val PREFERENCE_WHITELIST_LANGUAGES_TITLE = "Whitelist the following languages:" + const val PREFERENCE_WHITELIST_LANGUAGES_SUMMARY = + "Will keep project chapters in the following languages." + + " Takes precedence over blacklisted languages." + + " It will match the string present in the \"Language\" column of the chapter." + + " Whitespaces will be trimmed." + + " Leave empty to allow all languages." + + " Separate each entry with a comma ','" + + const val PREFERENCE_BLACKLIST_LANGUAGES = "$identifier-languages-blacklist" + const val PREFERENCE_BLACKLIST_LANGUAGES_TITLE = "Blacklist the following languages:" + const val PREFERENCE_BLACKLIST_LANGUAGES_SUMMARY = + "Will hide project chapters in the following languages." + + " Works identically to whitelisting." +} + +fun Element.containsBookLinks(): Boolean = select("a").any { + it.attrNormalizedUrl("href")?.isBookURL() == true +} + +fun Element.containsReadLinks(): Boolean = select("a").any { + it.attrNormalizedUrl("href")?.isReadURL() == true +} + +fun Element.containsImageGalleryLinks(): Boolean = select("a").any { + it.attrNormalizedUrl("href")?.isImagesGalleryURL() == true +} + +fun Element.getAllUrlElements(selector: String, attrName: String, predicate: (NormalizedURL) -> Boolean): Map { + return select(selector) + .mapNotNull { element -> element.attrNormalizedUrl(attrName)?.let { element to it } } + .filter { (_, url) -> predicate(url) } + .toMap() +} + +fun Element.getAllBooks(): Map { + val bookUrls = getAllUrlElements("a", "href") { it.isBookURL() } + val byID: Map> = bookUrls.groupBy { (_, url) -> url.pathSegments[1] /* /book/ */ } + + @Suppress("UNCHECKED_CAST") + return byID.mapValues { (bookid, elements) -> + val thumb: Element? = elements.entries.firstNotNullOfOrNull { (element, _) -> + element.select("img").firstOrNull() + } + val title = elements.entries.firstOrNull { (element, _) -> + element.select("img").isEmpty() && element.text().let { + it.isNotBlank() && it.lowercase(Locale.US) != "show more" + } + } + + if (thumb != null && title != null) { + PSBook(thumb, title.key, title.key.text(), bookid, title.value) + } else { + null + } + }.filterValues { it != null } as Map +} + +inline fun Map.groupBy(keySelector: (Map.Entry) -> SK): Map> = buildMap<_, MutableMap> { + this@groupBy.entries.forEach { entry -> + getOrPut(keySelector(entry)) { HashMap() }[entry.key] = entry.value + } +} + +private val absoluteDateFormat: ThreadLocal = ThreadLocal() +fun String.parseDate(ifFailed: Long = 0L): Long { + return when { + endsWith("ago") -> { + // relative + val number = takeWhile { it.isDigit() }.toInt() + val cal = Calendar.getInstance() + + when { + contains("day") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) } + contains("hour") -> cal.apply { add(Calendar.HOUR, -number) } + contains("minute") -> cal.apply { add(Calendar.MINUTE, -number) } + contains("second") -> cal.apply { add(Calendar.SECOND, -number) } + contains("week") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) } + contains("month") -> cal.apply { add(Calendar.MONTH, -number) } + contains("year") -> cal.apply { add(Calendar.YEAR, -number) } + else -> null + }?.timeInMillis ?: ifFailed + } + + else -> { + // absolute? + absoluteDateFormat.getOrSet { java.text.SimpleDateFormat("MMMM dd, yyyy", Locale.US) }.parse(this)?.time ?: ifFailed + } + } +} + +private val imageExtensions = setOf(".jpg", ".png", ".jpeg", ".webp", ".gif", ".avif", ".tiff") +private val simpleSrcVariants = listOf("src", "data-src", "data-lazy-src") +fun Element.imgNormalizedURL(): NormalizedURL? { + simpleSrcVariants.forEach { variant -> + if (hasAttr(variant)) { + return attrNormalizedUrl(variant) + } + } + + if (hasAttr("srcset")) { + return attr("abs:srcset").substringBefore(" ").toNormalURL() + } + + return attributes().firstOrNull { + it.key.contains("src") && imageExtensions.any { ext -> it.value.contains(ext) } + }?.value?.substringBefore(" ")?.toNormalURL() +} diff --git a/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/PSBook.kt b/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/PSBook.kt new file mode 100644 index 000000000..87198dee1 --- /dev/null +++ b/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/PSBook.kt @@ -0,0 +1,11 @@ +package eu.kanade.tachiyomi.extension.all.projectsuki + +import org.jsoup.nodes.Element + +data class PSBook( + val imgElement: Element, + val titleElement: Element, + val title: String, + val mangaID: String, + val url: NormalizedURL, +) diff --git a/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/PSFilters.kt b/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/PSFilters.kt new file mode 100644 index 000000000..dfa2d1646 --- /dev/null +++ b/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/PSFilters.kt @@ -0,0 +1,90 @@ +@file:Suppress("CanSealedSubClassBeObject") + +package eu.kanade.tachiyomi.extension.all.projectsuki + +import eu.kanade.tachiyomi.source.model.Filter +import okhttp3.HttpUrl + +@Suppress("NOTHING_TO_INLINE") +object PSFilters { + internal sealed interface AutoFilter { + fun applyTo(builder: HttpUrl.Builder) + } + + private inline fun HttpUrl.Builder.setAdv() = setQueryParameter("adv", "1") + + class Author : Filter.Text("Author"), AutoFilter { + + override fun applyTo(builder: HttpUrl.Builder) { + when { + state.isNotBlank() -> builder.setAdv().addQueryParameter("author", state) + } + } + + companion object { + val ownHeader by lazy { Header("Cannot search by multiple authors") } + } + } + + class Artist : Filter.Text("Artist"), AutoFilter { + + override fun applyTo(builder: HttpUrl.Builder) { + when { + state.isNotBlank() -> builder.setAdv().addQueryParameter("artist", state) + } + } + + companion object { + val ownHeader by lazy { Header("Cannot search by multiple artists") } + } + } + + class Status : Filter.Select("Status", Value.values()), AutoFilter { + enum class Value(val display: String, val query: String) { + ANY("Any", ""), + ONGOING("Ongoing", "ongoing"), + COMPLETED("Completed", "completed"), + HIATUS("Hiatus", "hiatus"), + CANCELLED("Cancelled", "cancelled"), + ; + + override fun toString(): String = display + + companion object { + private val values: Array = values() + operator fun get(ordinal: Int) = values[ordinal] + } + } + + override fun applyTo(builder: HttpUrl.Builder) { + when (val state = Value[state]) { + Value.ANY -> {} // default, do nothing + else -> builder.setAdv().addQueryParameter("status", state.query) + } + } + } + + class Origin : Filter.Select("Origin", Value.values()), AutoFilter { + enum class Value(val display: String, val query: String?) { + ANY("Any", null), + KOREA("Korea", "kr"), + CHINA("China", "cn"), + JAPAN("Japan", "jp"), + ; + + override fun toString(): String = display + + companion object { + private val values: Array = Value.values() + operator fun get(ordinal: Int) = values[ordinal] + } + } + + override fun applyTo(builder: HttpUrl.Builder) { + when (val state = Value[state]) { + Value.ANY -> {} // default, do nothing + else -> builder.setAdv().addQueryParameter("origin", state.query) + } + } + } +} diff --git a/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/ProjectSuki.kt b/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/ProjectSuki.kt new file mode 100644 index 000000000..0dbf62fc6 --- /dev/null +++ b/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/ProjectSuki.kt @@ -0,0 +1,443 @@ +package eu.kanade.tachiyomi.extension.all.projectsuki + +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.EditTextPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen +import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA +import eu.kanade.tachiyomi.lib.randomua.getPrefUAType +import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.network.interceptor.rateLimit +import eu.kanade.tachiyomi.source.ConfigurableSource +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.model.UpdateStrategy +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.Locale + +@Suppress("unused") +class ProjectSuki : HttpSource(), ConfigurableSource { + override val name: String = "Project Suki" + override val baseUrl: String = "https://projectsuki.com" + override val lang: String = "en" + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + private fun String.processLangPref(): List = split(",").map { it.trim().lowercase(Locale.US) } + + private val SharedPreferences.whitelistedLanguages: List + get() = getString(PS.PREFERENCE_WHITELIST_LANGUAGES, "")!! + .processLangPref() + + private val SharedPreferences.blacklistedLanguages: List + get() = getString(PS.PREFERENCE_BLACKLIST_LANGUAGES, "")!! + .processLangPref() + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + addRandomUAPreferenceToScreen(screen) + + screen.addPreference( + EditTextPreference(screen.context).apply { + key = PS.PREFERENCE_WHITELIST_LANGUAGES + title = PS.PREFERENCE_WHITELIST_LANGUAGES_TITLE + summary = PS.PREFERENCE_WHITELIST_LANGUAGES_SUMMARY + }, + ) + + screen.addPreference( + EditTextPreference(screen.context).apply { + key = PS.PREFERENCE_BLACKLIST_LANGUAGES + title = PS.PREFERENCE_BLACKLIST_LANGUAGES_TITLE + summary = PS.PREFERENCE_BLACKLIST_LANGUAGES_SUMMARY + }, + ) + } + + override val client: OkHttpClient = network.cloudflareClient.newBuilder() + .setRandomUserAgent( + userAgentType = preferences.getPrefUAType(), + customUA = preferences.getPrefCustomUA(), + filterInclude = listOf("chrome"), + ) + .rateLimit(4) + .build() + + override fun popularMangaRequest(page: Int) = GET(baseUrl, headers) + + // differentiating between popular and latest manga in the main page is + // *theoretically possible* but a pain, as such, this is fine "for now" + override fun popularMangaParse(response: Response): MangasPage { + val document = response.asJsoup() + val allBooks = document.getAllBooks() + return MangasPage( + mangas = allBooks.mapNotNull mangas@{ (_, psbook) -> + val (img, _, titleText, _, url) = psbook + + val relativeUrl = url.rawRelative ?: return@mangas null + + SManga.create().apply { + this.url = relativeUrl + this.title = titleText + this.thumbnail_url = img.imgNormalizedURL()?.rawAbsolute + } + }, + hasNextPage = false, + ) + } + + override val supportsLatest: Boolean = false + override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException() + override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException() + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return when { + /*query.startsWith(PS.SEARCH_INTENT_PREFIX) -> { + val id = query.substringAfter(PS.SEARCH_INTENT_PREFIX) + client.newCall(getMangaByIdAsSearchResult(id)) + .asObservableSuccess() + .map { response -> searchMangaParse(response) } + }*/ + + else -> Observable.defer { + try { + client.newCall(searchMangaRequest(page, query, filters)) + .asObservableSuccess() + } catch (e: NoClassDefFoundError) { + throw RuntimeException(e) + } + }.map { response -> searchMangaParse(response) } + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + return GET( + baseUrl.toHttpUrl().newBuilder().apply { + addPathSegment("search") + addQueryParameter("page", (page - 1).toString()) + addQueryParameter("q", query) + + filters.applyFilter(this) + filters.applyFilter(this) + filters.applyFilter(this) + filters.applyFilter(this) + }.build(), + headers, + ) + } + + private inline fun FilterList.applyFilter(to: HttpUrl.Builder) where T : Filter<*>, T : PSFilters.AutoFilter { + firstNotNullOfOrNull { it as? T }?.applyTo(to) + } + + override fun getFilterList() = FilterList( + Filter.Header("Filters only take effect when searching for something!"), + PSFilters.Origin(), + PSFilters.Status(), + PSFilters.Author.ownHeader, + PSFilters.Author(), + PSFilters.Artist.ownHeader, + PSFilters.Artist(), + ) + + override fun searchMangaParse(response: Response): MangasPage { + val document = response.asJsoup() + val allBooks = document.getAllBooks() + + val mangas = allBooks.mapNotNull mangas@{ (_, psbook) -> + val (img, _, titleText, _, url) = psbook + + val relativeUrl = url.rawRelative ?: return@mangas null + + SManga.create().apply { + this.url = relativeUrl + this.title = titleText + this.thumbnail_url = img.imgNormalizedURL()?.rawAbsolute + } + } + + return MangasPage( + mangas = mangas, + hasNextPage = mangas.size >= 30, // observed max number of results in search + ) + } + + override fun fetchMangaDetails(manga: SManga): Observable { + return client.newCall(mangaDetailsRequest(manga)) + .asObservableSuccess() + .map { response -> + mangaDetailsParse(response, incomplete = manga).apply { initialized = true } + } + } + + private val displayNoneMatcher = """display: ?none;""".toRegex() + private val emptyImageURLAbsolute = """https://projectsuki.com/images/gallery/empty.jpg""".toNormalURL()!!.rawAbsolute + private val emptyImageURLRelative = """https://projectsuki.com/images/gallery/empty.jpg""".toNormalURL()!!.rawRelative!! + override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException("not used") + private fun mangaDetailsParse(response: Response, incomplete: SManga): SManga { + val document = response.asJsoup() + val allLinks = document.getAllUrlElements("a", "href") { it.isPSUrl() } + + val thumb: Element? = document.select("img").firstOrNull { img -> + img.attr("onerror").let { + it.contains(emptyImageURLAbsolute) || + it.contains(emptyImageURLRelative) + } + } + + val authors: Map = allLinks.filter { (_, url) -> + url.queryParameterNames.contains("author") + } + + val artists: Map = allLinks.filter { (_, url) -> + url.queryParameterNames.contains("artist") + } + + val statuses: Map = allLinks.filter { (_, url) -> + url.queryParameterNames.contains("status") + } + + val origins: Map = allLinks.filter { (_, url) -> + url.queryParameterNames.contains("origin") + } + + val genres: Map = allLinks.filter { (_, url) -> + url.pathStartsWith(listOf("genre")) + } + + val description = document.select("#descriptionCollapse").joinToString("\n-----\n", postfix = "\n") { it.wholeText() } + + val alerts = document.select(".alert, .alert-info") + .filter( + predicate = { + it.parents().none { parent -> + parent.attr("style") + .contains(displayNoneMatcher) + } + }, + ) + + val userRating = document.select("#ratings") + .firstOrNull() + ?.children() + ?.count { it.hasClass("text-warning") } + ?.takeIf { it > 0 } + + return SManga.create().apply { + url = incomplete.url + title = incomplete.title + thumbnail_url = thumb?.imgNormalizedURL()?.rawAbsolute ?: incomplete.thumbnail_url + + author = authors.keys.joinToString(", ") { it.text() } + artist = artists.keys.joinToString(", ") { it.text() } + status = when (statuses.keys.joinToString("") { it.text().trim() }.lowercase(Locale.US)) { + "ongoing" -> SManga.ONGOING + "completed" -> SManga.PUBLISHING_FINISHED + "hiatus" -> SManga.ON_HIATUS + "cancelled" -> SManga.CANCELLED + else -> SManga.UNKNOWN + } + + this.description = buildString { + if (alerts.isNotEmpty()) { + appendLine("Alerts have been found, refreshing the manga later might help in removing them.") + appendLine() + + alerts.forEach { alert -> + var appendedSomething = false + alert.select("h4").singleOrNull()?.let { + appendLine(it.text()) + appendedSomething = true + } + alert.select("p").singleOrNull()?.let { + appendLine(it.text()) + appendedSomething = true + } + if (!appendedSomething) { + appendLine(alert.text()) + } + } + + appendLine() + appendLine() + } + + appendLine(description) + + fun appendToDescription(by: String, data: String?) { + if (data != null) append(by).appendLine(data) + } + + appendToDescription("User Rating: ", """${userRating ?: "?"}/5""") + appendToDescription("Authors: ", author) + appendToDescription("Artists: ", artist) + appendToDescription("Status: ", statuses.keys.joinToString(", ") { it.text() }) + appendToDescription("Origin: ", origins.keys.joinToString(", ") { it.text() }) + appendToDescription("Genres: ", genres.keys.joinToString(", ") { it.text() }) + } + + this.update_strategy = if (status != SManga.CANCELLED) UpdateStrategy.ALWAYS_UPDATE else UpdateStrategy.ONLY_FETCH_ONCE + this.genre = buildList { + addAll(genres.keys.map { it.text() }) + origins.values.forEach { url -> + when (url.queryParameter("origin")) { + "kr" -> add("Manhwa") + "cn" -> add("Manhua") + "jp" -> add("Manga") + } + } + }.joinToString(", ") + } + } + + private val chapterHeaderMatcher = """chapters?""".toRegex() + private val groupHeaderMatcher = """groups?""".toRegex() + private val dateHeaderMatcher = """added|date""".toRegex() + private val languageHeaderMatcher = """language""".toRegex() + private val chapterNumberMatcher = """[Cc][Hh][Aa][Pp][Tt][Ee][Rr]\s*(\d+)(?:\s*[.,-]\s*(\d+))?""".toRegex() + private val looseNumberMatcher = """(\d+)(?:\s*[.,-]\s*(\d+))?""".toRegex() + override fun chapterListParse(response: Response): List { + val document = response.asJsoup() + val chaptersTable = document.select("table").firstOrNull { it.containsReadLinks() } ?: return emptyList() + + val thead: Element = chaptersTable.select("thead").firstOrNull() ?: return emptyList() + val tbody: Element = chaptersTable.select("tbody").firstOrNull() ?: return emptyList() + + val columnTypes = thead.select("tr").firstOrNull()?.children()?.select("td") ?: return emptyList() + val textTypes = columnTypes.map { it.text().lowercase(Locale.US) } + val normalSize = textTypes.size + + val chaptersIndex: Int = textTypes.indexOfFirst { it.matches(chapterHeaderMatcher) }.takeIf { it >= 0 } ?: return emptyList() + val dateIndex: Int = textTypes.indexOfFirst { it.matches(dateHeaderMatcher) }.takeIf { it >= 0 } ?: return emptyList() + val groupIndex: Int? = textTypes.indexOfFirst { it.matches(groupHeaderMatcher) }.takeIf { it >= 0 } + val languageIndex: Int? = textTypes.indexOfFirst { it.matches(languageHeaderMatcher) }.takeIf { it >= 0 } + + val dataRows = tbody.children().select("tr") + + val blLangs = preferences.blacklistedLanguages + val wlLangs = preferences.whitelistedLanguages + + return dataRows.mapNotNull chapters@{ tr -> + val rowData = tr.children().select("td") + + if (rowData.size != normalSize) { + return@chapters null + } + + val chapter: Element = rowData[chaptersIndex] + val date: Element = rowData[dateIndex] + val group: Element? = groupIndex?.let(rowData::get) + val language: Element? = languageIndex?.let(rowData::get) + + language?.text()?.lowercase(Locale.US)?.let { lang -> + if (lang in blLangs && lang !in wlLangs) return@chapters null + } + + val chapterLink = chapter.select("a").first()!!.attrNormalizedUrl("href")!! + + val relativeURL = chapterLink.rawRelative ?: return@chapters null + + SChapter.create().apply { + chapter_number = chapter.text() + .let { (chapterNumberMatcher.find(it) ?: looseNumberMatcher.find(it)) } + ?.let { result -> + val integral = result.groupValues[1] + val fractional = result.groupValues.getOrNull(2) + + """${integral}$fractional""".toFloat() + } ?: -1f + + url = relativeURL + scanlator = group?.text() ?: "" + name = chapter.text() + date_upload = date.text().parseDate() + } + }.toList() + } + + override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Not used") + + private val callpageUrl = """https://projectsuki.com/callpage""" + private val jsonMediaType = "application/json;charset=UTF-8".toMediaType() + override fun fetchPageList(chapter: SChapter): Observable> { + // chapter.url is /read///... + val url = chapter.url.toNormalURL() ?: return Observable.just(emptyList()) + + val bookid = url.pathSegments[1] // + val chapterid = url.pathSegments[2] // + + val callpageHeaders = headersBuilder() + .add("X-Requested-With", "XMLHttpRequest") + .add("Content-Type", "application/json;charset=UTF-8") + .build() + + val callpageBody = Json.encodeToString( + mapOf( + "bookid" to bookid, + "chapterid" to chapterid, + "first" to "true", + ), + ).toRequestBody(jsonMediaType) + + return client.newCall( + POST(callpageUrl, callpageHeaders, callpageBody), + ).asObservableSuccess() + .map { response -> + callpageParse(chapter, response) + } + } + + @Suppress("UNUSED_PARAMETER") + private fun callpageParse(chapter: SChapter, response: Response): List { + // response contains the html src with images + val src = Json.parseToJsonElement(response.body.string()).jsonObject["src"]?.jsonPrimitive?.content ?: return emptyList() + val images = Jsoup.parseBodyFragment(src).select("img") + // images urls are /images/gallery///? (empty query for some reason) + val urls = images.mapNotNull { it.attrNormalizedUrl("src") } + if (urls.isEmpty()) return emptyList() + + val anUrl = urls.random() + val pageNums = urls.mapTo(ArrayList()) { it.pathSegments[4] } + pageNums += "001" + + fun makeURL(pageNum: String) = anUrl.newBuilder() + .setPathSegment(anUrl.pathSegments.lastIndex, pageNum) + .build() + + return pageNums.distinct().sortedBy { it.toInt() }.mapIndexed { index, number -> + Page( + index, + "", + makeURL(number).rawAbsolute, + ) + }.distinctBy { it.imageUrl } + } + + override fun pageListParse(response: Response): List = throw UnsupportedOperationException("not used") +} diff --git a/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/ProjectSukiUrlActivity.kt b/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/ProjectSukiUrlActivity.kt new file mode 100644 index 000000000..a72a020b1 --- /dev/null +++ b/src/all/projectsuki/src/eu/kanade/tachiyomi/extension/all/projectsuki/ProjectSukiUrlActivity.kt @@ -0,0 +1,33 @@ +package eu.kanade.tachiyomi.extension.all.projectsuki + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import kotlin.system.exitProcess + +class ProjectSukiUrlActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size > 1) { + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${PS.SEARCH_INTENT_PREFIX}${pathSegments[1]}") + putExtra("filter", packageName) + } + + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e("PSUrlActivity", e.toString()) + } + } else { + Log.e("PSUrlActivity", "could not parse uri from intent $intent") + } + + finish() + exitProcess(0) + } +}