diff --git a/.github/workflows/issue_moderator.yml b/.github/workflows/issue_moderator.yml index c2f7233ff..84d9179f2 100644 --- a/.github/workflows/issue_moderator.yml +++ b/.github/workflows/issue_moderator.yml @@ -37,7 +37,7 @@ jobs: }, { "type": "both", - "regex": ".*(mangago|mangafox|hq\\s*dragon|manga\\s*host|supermangas|superhentais|union\\s*mangas|yes\\s*mangas|manhuascan|heroscan|manhwahot|leitor\\.?net|manga\\s*livre|tsuki\\s*mangas|manga\\s*yabu|mangas\\.in|mangas\\.pw|hentaikai|toptoon\\+?|cocomanga|hitomi\\.la|copymanga|neox|1manga\\.co|mangafox\\.fun|mangahere\\.onl|manga\\s*hub|mangakakalot\\.fun|manganel(?!o)|mangaonline\\.fun|mangapanda\\.onl|mangareader\\.site|mangatoday|manga\\.town|onemanga\\.info).*", + "regex": ".*(mangago|mangafox|hq\\s*dragon|manga\\s*host|supermangas|superhentais|union\\s*mangas|yes\\s*mangas|manhuascan|heroscan|manhwahot|leitor\\.?net|manga\\s*livre|tsuki\\s*mangas|manga\\s*yabu|mangas\\.in|mangas\\.pw|hentaikai|toptoon\\+?|cocomanga|hitomi\\.la|copymanga|neox|1manga\\.co|mangafox\\.fun|mangahere\\.onl|manga\\s*hub|mangakakalot\\.fun|manganel(?!o)|mangaonline\\.fun|mangapanda\\.onl|mangareader\\.site|mangatoday|manga\\.town|onemanga\\.info|koushoku).*", "ignoreCase": true, "message": "{match} will not be added back as it is too difficult to maintain. Read #3475 for more information" }, diff --git a/src/en/koushoku/AndroidManifest.xml b/src/en/koushoku/AndroidManifest.xml deleted file mode 100644 index 20f02414a..000000000 --- a/src/en/koushoku/AndroidManifest.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/src/en/koushoku/build.gradle b/src/en/koushoku/build.gradle deleted file mode 100644 index 1d29b84e0..000000000 --- a/src/en/koushoku/build.gradle +++ /dev/null @@ -1,12 +0,0 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' - -ext { - extName = 'Koushoku' - pkgNameSuffix = 'en.koushoku' - extClass = '.Koushoku' - extVersionCode = 15 - isNsfw = true -} - -apply from: "$rootDir/common.gradle" diff --git a/src/en/koushoku/res/mipmap-hdpi/ic_launcher.png b/src/en/koushoku/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 0c9640875..000000000 Binary files a/src/en/koushoku/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/koushoku/res/mipmap-mdpi/ic_launcher.png b/src/en/koushoku/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 67771ee44..000000000 Binary files a/src/en/koushoku/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/koushoku/res/mipmap-xhdpi/ic_launcher.png b/src/en/koushoku/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 8ef3c0ba1..000000000 Binary files a/src/en/koushoku/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/koushoku/res/mipmap-xxhdpi/ic_launcher.png b/src/en/koushoku/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index ad2a381f2..000000000 Binary files a/src/en/koushoku/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/koushoku/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/koushoku/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 3fd854742..000000000 Binary files a/src/en/koushoku/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/koushoku/res/web_hi_res_512.png b/src/en/koushoku/res/web_hi_res_512.png deleted file mode 100644 index 39703f89f..000000000 Binary files a/src/en/koushoku/res/web_hi_res_512.png and /dev/null differ diff --git a/src/en/koushoku/src/eu/kanade/tachiyomi/extension/en/koushoku/Koushoku.kt b/src/en/koushoku/src/eu/kanade/tachiyomi/extension/en/koushoku/Koushoku.kt deleted file mode 100644 index e785d2455..000000000 --- a/src/en/koushoku/src/eu/kanade/tachiyomi/extension/en/koushoku/Koushoku.kt +++ /dev/null @@ -1,290 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.koushoku - -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.asObservableSuccess -import eu.kanade.tachiyomi.network.interceptor.rateLimitHost -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.online.ParsedHttpSource -import eu.kanade.tachiyomi.util.asJsoup -import okhttp3.Headers -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import org.jsoup.nodes.Document -import org.jsoup.nodes.Element -import rx.Observable -import java.text.SimpleDateFormat -import java.util.Locale -import kotlin.random.Random - -class Koushoku : ParsedHttpSource() { - companion object { - const val PREFIX_ID_SEARCH = "id:" - - const val thumbnailSelector = "figure img" - const val magazinesSelector = ".metadata a[href^='/magazines/']" - - private val PATTERN_IMAGES = "(.+/)(\\d+)(.*)".toRegex() - private val DATE_FORMAT = SimpleDateFormat("E, d MMM yyy HH:mm:ss 'UTC'", Locale.US) - } - - override val baseUrl = "https://koushoku.org" - override val name = "Koushoku" - override val lang = "en" - override val supportsLatest = true - - override val client: OkHttpClient = network.cloudflareClient.newBuilder() - .addInterceptor(KoushokuWebViewInterceptor()) - // Site: 40req per 1 minute - // Here: 1req per 2 sec -> 30req per 1 minute - // (somewhat lower due to caching) - .rateLimitHost("https://koushoku.org".toHttpUrl(), 1, 2) - .build() - - override fun headersBuilder(): Headers.Builder { - val chromeStableVersion = listOf("104.0.5112.69", "103.0.5060.71", "103.0.5060.70", "103.0.5060.53", "103.0.5060.129", "102.0.5005.99", "102.0.5005.98", "102.0.5005.78", "102.0.5005.125").random() - val chromeCanaryVersion = listOf("106.0.5227.0", "106.0.5209.0", "106.0.5206.0", "106.0.5201.2", "106.0.5201.0", "106.0.5200.0", "106.0.5199.0", "106.0.5197.0", "106.0.5196.0", "105.0.5195.2", "105.0.5194.0", "105.0.5193.0", "105.0.5192.0", "105.0.5191.0", "105.0.5190.0", "105.0.5189.0", "105.0.5186.0", "105.0.5185.0", "105.0.5184.0", "105.0.5182.0", "105.0.5180.0", "105.0.5179.3", "105.0.5178.0", "105.0.5177.2", "105.0.5176.0", "105.0.5175.0", "105.0.5174.0", "105.0.5173.0", "105.0.5172.0", "105.0.5171.0").random() - val chromeVersion = if (Random.nextFloat() > 0.2) chromeStableVersion else chromeCanaryVersion - - val deviceInfo = if (Random.nextFloat() > 0.2) "" else "; " + listOf("SM-S908B", "SM-S908U", "SM-A536B", "SM-A536U", "SM-S901B", "SM-S901U", "SM-A736B", "SM-G973F", "SM-A528B", "SM-G975U", "SM-G990B", "SM-G990U").random() - val androidVersion = IntRange(if (deviceInfo.isEmpty()) 9 else 11, 12).random() - - return super.headersBuilder() - .set("User-Agent", "Mozilla/5.0 (Linux; Android $androidVersion$deviceInfo) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$chromeVersion Mobile Safari/537.36") - .add("Referer", "$baseUrl/") - } - - override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/?page=$page", headers) - override fun latestUpdatesSelector() = "#archives.feed .entries > .entry" - override fun latestUpdatesNextPageSelector() = "footer nav li:has(a.active) + li:not(:last-child) > a" - - override fun latestUpdatesFromElement(element: Element) = SManga.create().apply { - setUrlWithoutDomain(element.selectFirst("a").attr("href")) - title = element.selectFirst("[title]").attr("title") - thumbnail_url = element.selectFirst(thumbnailSelector).absUrl("src") - } - - private fun searchMangaByIdRequest(id: String) = GET("$baseUrl/archive/$id", headers) - - // taken from Tsumino ext - private fun searchMangaByIdParse(response: Response, id: String): MangasPage { - val details = mangaDetailsParse(response) - details.url = "/archive/$id" - return MangasPage(listOf(details), false) - } - - // taken from Tsumino ext - override fun fetchSearchManga( - page: Int, - query: String, - filters: FilterList - ): Observable { - return if (query.startsWith(PREFIX_ID_SEARCH)) { - val id = query.removePrefix(PREFIX_ID_SEARCH) - client.newCall(searchMangaByIdRequest(id)).asObservableSuccess() - .map { response -> searchMangaByIdParse(response, id) } - } else { - super.fetchSearchManga(page, query, filters) - } - } - - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val url = "$baseUrl/search".toHttpUrlOrNull()!!.newBuilder() - .addQueryParameter("page", page.toString()) - - val filterList = if (filters.isEmpty()) getFilterList() else filters - filterList.findInstance()?.addQueryParameter(url) - url.addQueryParameter("q", buildAdvQuery(query, filterList)) - return GET(url.toString(), headers) - } - - private fun buildAdvQuery(query: String, filterList: FilterList): String { - val title = if (query.isNotBlank()) "title*:\"$query\" " else "" - val filters: List = filterList.filterIsInstance().map { filter -> - if (filter.state.isBlank()) return@map "" - val included = mutableListOf() - val excluded = mutableListOf() - val name = if (filter.name.lowercase().contentEquals("tags")) "tag" else filter.name.lowercase() - filter.state.split(",").map(String::trim).filterNot(String::isBlank).forEach { entry -> - if (entry.startsWith("-")) { - excluded.add(entry.slice(1 until entry.length)) - } else { - included.add(entry) - } - } - buildString { - if (included.isNotEmpty()) append("$name&*:\"${included.joinToString(",")}\" ") - if (excluded.isNotEmpty()) append("-$name&*:\"${excluded.joinToString(",")}\"") - } - } - return "$title${ - filters.filterNot(String::isBlank).joinToString(" ", transform = String::trim) - }" - } - - override fun searchMangaSelector() = latestUpdatesSelector() - override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector() - override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element) - - override fun popularMangaRequest(page: Int) = GET("$baseUrl/popular?page=$page", headers) - override fun popularMangaSelector() = latestUpdatesSelector() - override fun popularMangaNextPageSelector() = latestUpdatesNextPageSelector() - override fun popularMangaFromElement(element: Element) = latestUpdatesFromElement(element) - - override fun fetchMangaDetails(manga: SManga): Observable { - return if (!manga.initialized) { - super.fetchMangaDetails(manga) - } else { - Observable.just(manga) - } - } - - override fun mangaDetailsParse(document: Document) = SManga.create().apply { - title = document.selectFirst(".metadata > h1").text() - - // Reuse cover from browse - thumbnail_url = document.selectFirst(thumbnailSelector).absUrl("src") - .replace(Regex("/\\d+\\.webp\$"), "/288.webp") - - artist = document.select(".metadata a[href^='/artists/'], .metadata a[href^='/circles/']") - .joinToString { it.text() } - author = artist - genre = document.select(".metadata .tags a, $magazinesSelector") - .ifEmpty { null }?.joinToString { it.text() } - description = getDesc(document) - status = SManga.COMPLETED - } - - override fun chapterListParse(response: Response): List { - val document = response.asJsoup() - - return listOf( - SChapter.create().apply { - setUrlWithoutDomain(response.request.url.encodedPath) - name = "Chapter" - - val dateText = document.select("tr > td:first-child:contains(Uploaded Date) + td") - .text() - date_upload = runCatching { DATE_FORMAT.parse(dateText) } - .getOrNull() - ?.time - ?: 0 - } - ) - } - - override fun chapterFromElement(element: Element) = - throw UnsupportedOperationException("Not used") - - override fun chapterListSelector() = throw UnsupportedOperationException("Not used") - - override fun pageListRequest(chapter: SChapter) = GET("$baseUrl${chapter.url}/1", headers) - - override fun pageListParse(response: Response): List { - val document = response.asJsoup() - - val totalPages = document.selectFirst(".total")?.text()?.toInt() ?: 0 - if (totalPages == 0) - throw UnsupportedOperationException("Error: Empty pages (try Webview)") - - val match = PATTERN_IMAGES.find(response.request.url.toString())!! - val prefix = match.groupValues[1] - val suffix = match.groupValues[3] - - return (1..totalPages).map { - Page(it, "$prefix$it$suffix") - } - } - - override fun pageListParse(document: Document): List = - throw UnsupportedOperationException("Not used") - - override fun imageUrlParse(document: Document): String = - document.selectFirst(".main img, main img").absUrl("src") - - override fun getFilterList() = FilterList( - SortFilter( - "Sort", - arrayOf( - Sortable("ID", "id"), - Sortable("Title", "title"), - Sortable("Created Date", "created_at"), - Sortable("Uploaded Date", "published_at"), - Sortable("Pages", "pages"), - ) - ), - Filter.Header("Separate tags with commas (,)"), - Filter.Header("Prepend with dash (-) to exclude"), - ArtistFilter(), - CircleFilter(), - MagazineFilter(), - ParodyFilter(), - TagFilter(), - PagesFilter() - ) - - // Adapted from Mangadex ext - class SortFilter(displayName: String, private val sortables: Array) : - Filter.Sort( - displayName, - sortables.map(Sortable::title).toTypedArray(), - Selection(2, false) - ) { - fun addQueryParameter(url: HttpUrl.Builder) { - if (state != null) { - val sort = sortables[state!!.index].value - val order = when (state!!.ascending) { - true -> "asc" - false -> "desc" - } - - url.addQueryParameter("sort", sort) - url.addQueryParameter("order", order) - } - } - } - - data class Sortable(val title: String, val value: String) { - override fun toString(): String = title - } - - class ArtistFilter : Filter.Text("Artist") - class CircleFilter : Filter.Text("Circle") - class MagazineFilter : Filter.Text("Magazine") - class ParodyFilter : Filter.Text("Parody") - class TagFilter : Filter.Text("Tags") - class PagesFilter : Filter.Text("Pages") - - // Taken from nhentai ext - private inline fun Iterable<*>.findInstance() = find { it is T } as? T - - private fun getDesc(document: Document) = buildString { - val magazines = document.select(magazinesSelector) - if (magazines.isNotEmpty()) { - append("Magazines: ") - append(magazines.joinToString { it.text() }) - append("\n") - } - - val parodies = document.select(".metadata a[href^='/parodies/']") - if (parodies.isNotEmpty()) { - append("Parodies: ") - append(parodies.joinToString { it.text() }) - append("\n") - } - - val pages = document.selectFirst("tr > td:first-child:contains(Pages) + td") - append("Pages: ").append(pages.text()).append("\n") - - val size: Element? = document.selectFirst("tr > td:first-child:contains(Size) + td") - append("Size: ").append(size?.text() ?: "Unknown") - } -} diff --git a/src/en/koushoku/src/eu/kanade/tachiyomi/extension/en/koushoku/KoushokuUrlActivity.kt b/src/en/koushoku/src/eu/kanade/tachiyomi/extension/en/koushoku/KoushokuUrlActivity.kt deleted file mode 100644 index b3795b90f..000000000 --- a/src/en/koushoku/src/eu/kanade/tachiyomi/extension/en/koushoku/KoushokuUrlActivity.kt +++ /dev/null @@ -1,38 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.koushoku - -import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.Intent -import android.os.Bundle -import android.util.Log -import kotlin.system.exitProcess - -/** - * Springboard that accepts https://koushoku.org/archive/xxxxx intents and redirects them to - * the main Tachiyomi process. - */ -class KoushokuUrlActivity : Activity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val pathSegments = intent?.data?.pathSegments - if (pathSegments != null && pathSegments.size > 1) { - val id = pathSegments[1] - val mainIntent = Intent().apply { - action = "eu.kanade.tachiyomi.SEARCH" - putExtra("query", "${Koushoku.PREFIX_ID_SEARCH}$id") - putExtra("filter", packageName) - } - - try { - startActivity(mainIntent) - } catch (e: ActivityNotFoundException) { - Log.e("KoushokuUrlActivity", e.toString()) - } - } else { - Log.e("KoushokuUrlActivity", "could not parse uri from intent $intent") - } - - finish() - exitProcess(0) - } -} diff --git a/src/en/koushoku/src/eu/kanade/tachiyomi/extension/en/koushoku/KoushokuWebViewInterceptor.kt b/src/en/koushoku/src/eu/kanade/tachiyomi/extension/en/koushoku/KoushokuWebViewInterceptor.kt deleted file mode 100644 index 6bcc34335..000000000 --- a/src/en/koushoku/src/eu/kanade/tachiyomi/extension/en/koushoku/KoushokuWebViewInterceptor.kt +++ /dev/null @@ -1,73 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.koushoku - -import android.app.Application -import android.os.Handler -import android.os.Looper -import android.webkit.WebView -import android.webkit.WebViewClient -import okhttp3.Interceptor -import okhttp3.Response -import org.jsoup.Jsoup -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.io.IOException -import java.util.concurrent.CountDownLatch - -class KoushokuWebViewInterceptor : Interceptor { - - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - val response = chain.proceed(request) - - if (response.headers("Content-Type").any { it.contains("text/html") }) { - val responseBody = response.peekBody(1 * 1024 * 1024).string() - if (response.code == 403) { - val document = Jsoup.parse(responseBody) - if (document.selectFirst("h1")?.text()?.contains(Regex("banned$")) == true) { - throw IOException("You have been banned. Check WebView for details.") - } - } - - if (response.networkResponse != null) { - try { - proceedWithWebView(response, responseBody) - } catch (e: Exception) { - throw IOException(e) - } - } - } - - return response - } - - private fun proceedWithWebView(response: Response, responseBody: String) { - val latch = CountDownLatch(1) - val handler = Handler(Looper.getMainLooper()) - - handler.post { - val webView = WebView(Injekt.get()) - with(webView.settings) { - loadsImagesAutomatically = false - userAgentString = response.request.header("User-Agent") - } - - webView.webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView, url: String) { - webView.stopLoading() - webView.destroy() - latch.countDown() - } - } - - webView.loadDataWithBaseURL( - response.request.url.toString(), - responseBody, - "text/html", - "utf-8", - null - ) - } - - latch.await() - } -}