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()
- }
-}