Remove extensions redirecting to other extensions (#8076)
* Remove Apolltoons (replaced by Mundo Manhwa) * Remove Arctic Scan (replaced by Yushuke Mangas) * chore: update comment bacakomik.co -> bacakomik.one * Remove Black Scans (replaced by Yugen Mangás) * Remove KomikIndo.info (replaced by Mangasusu) * Remove Snow Scans (replaced by Galaxy) * Remove Vex Manga (replaced by Vortex Scans) * Remove MangaSaki (replaced by Mangasail) * Remove Xmanhwa (replaced by ManhwaDen)
| @ -1,10 +0,0 @@ | ||||
| ext { | ||||
|     extName = 'Vex Manga' | ||||
|     extClass = '.VexManga' | ||||
|     themePkg = 'mangathemesia' | ||||
|     baseUrl = 'https://vexmanga.com' | ||||
|     overrideVersionCode = 3 | ||||
|     isNsfw = false | ||||
| } | ||||
| 
 | ||||
| apply from: "$rootDir/common.gradle" | ||||
| Before Width: | Height: | Size: 5.3 KiB | 
| Before Width: | Height: | Size: 2.7 KiB | 
| Before Width: | Height: | Size: 7.9 KiB | 
| Before Width: | Height: | Size: 16 KiB | 
| Before Width: | Height: | Size: 26 KiB | 
| @ -1,74 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.ar.vexmanga | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia | ||||
| import eu.kanade.tachiyomi.source.model.Page | ||||
| import eu.kanade.tachiyomi.source.model.SChapter | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import kotlinx.serialization.json.jsonArray | ||||
| import kotlinx.serialization.json.jsonPrimitive | ||||
| import org.jsoup.nodes.Document | ||||
| import org.jsoup.nodes.Element | ||||
| import java.lang.IllegalArgumentException | ||||
| import java.util.Calendar | ||||
| 
 | ||||
| class VexManga : MangaThemesia( | ||||
|     "فيكس مانجا", | ||||
|     "https://vexmanga.com", | ||||
|     "ar", | ||||
| ) { | ||||
|     override fun searchMangaSelector() = ".listarchives .latest-recom, .listupd .latest-series, ${super.searchMangaSelector()}" | ||||
|     override val sendViewCount = false | ||||
|     override fun chapterListSelector() = ".ulChapterList > a, ${super.chapterListSelector()}" | ||||
| 
 | ||||
|     override val seriesArtistSelector = | ||||
|         ".tsinfo .imptdt:contains(الرسام) i, ${super.seriesArtistSelector}" | ||||
|     override val seriesAuthorSelector = | ||||
|         ".tsinfo .imptdt:contains(المؤلف) i, ${super.seriesAuthorSelector}" | ||||
|     override val seriesStatusSelector = | ||||
|         ".tsinfo .imptdt:contains(الحالة) i, ${super.seriesStatusSelector}" | ||||
|     override val seriesTypeSelector = | ||||
|         ".tsinfo .imptdt:contains(النوع) i, ${super.seriesTypeSelector}" | ||||
| 
 | ||||
|     override fun String?.parseStatus() = when { | ||||
|         this == null -> SManga.UNKNOWN | ||||
|         this.contains("مستمر", ignoreCase = true) -> SManga.ONGOING | ||||
|         this.contains("مكتمل", ignoreCase = true) -> SManga.COMPLETED | ||||
|         this.contains("متوقف", ignoreCase = true) -> SManga.ON_HIATUS | ||||
|         else -> SManga.UNKNOWN | ||||
|     } | ||||
| 
 | ||||
|     override fun chapterFromElement(element: Element) = SChapter.create().apply { | ||||
|         setUrlWithoutDomain(element.attr("href")) | ||||
|         name = element.select(".chapternum").text() | ||||
|         date_upload = element.select(".chapterdate").text().parseRelativeDate() | ||||
|     } | ||||
| 
 | ||||
|     private fun String.parseRelativeDate(): Long { | ||||
|         val number = Regex("""(\d+)""").find(this)?.value?.toIntOrNull() ?: return 0 | ||||
|         val cal = Calendar.getInstance() | ||||
| 
 | ||||
|         return when { | ||||
|             this.contains("أيام", true) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis | ||||
|             this.contains("ساعة", true) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis | ||||
|             this.contains("دقائق", true) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis | ||||
|             this.contains("أسبوعين", true) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis | ||||
|             this.contains("أشهر", true) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis | ||||
|             else -> 0 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun pageListParse(document: Document): List<Page> { | ||||
|         val docString = document.toString() | ||||
|         val imageListJson = JSON_IMAGE_LIST_REGEX.find(docString)?.destructured?.toList()?.get(0).orEmpty() | ||||
|         val imageList = try { | ||||
|             json.parseToJsonElement(imageListJson).jsonArray | ||||
|         } catch (_: IllegalArgumentException) { | ||||
|             emptyList() | ||||
|         } | ||||
|         val scriptPages = imageList.mapIndexed { i, jsonEl -> | ||||
|             Page(i, document.location(), jsonEl.jsonPrimitive.content) | ||||
|         } | ||||
| 
 | ||||
|         return scriptPages | ||||
|     } | ||||
| } | ||||
| @ -1,8 +0,0 @@ | ||||
| ext { | ||||
|     extName = 'MangaSaki' | ||||
|     extClass = '.MangaSaki' | ||||
|     extVersionCode = 1 | ||||
|     isNsfw = true | ||||
| } | ||||
| 
 | ||||
| apply from: "$rootDir/common.gradle" | ||||
| Before Width: | Height: | Size: 4.4 KiB | 
| Before Width: | Height: | Size: 2.5 KiB | 
| Before Width: | Height: | Size: 5.8 KiB | 
| Before Width: | Height: | Size: 10 KiB | 
| Before Width: | Height: | Size: 14 KiB | 
| @ -1,265 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.en.mangasaki | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.network.GET | ||||
| import eu.kanade.tachiyomi.source.model.Filter | ||||
| import eu.kanade.tachiyomi.source.model.FilterList | ||||
| 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 kotlinx.serialization.Serializable | ||||
| import kotlinx.serialization.decodeFromString | ||||
| import kotlinx.serialization.json.Json | ||||
| import okhttp3.Request | ||||
| import okhttp3.Response | ||||
| import org.jsoup.nodes.Document | ||||
| import org.jsoup.nodes.Element | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.text.ParseException | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Locale | ||||
| 
 | ||||
| class MangaSaki : ParsedHttpSource() { | ||||
| 
 | ||||
|     override val name = "MangaSaki" | ||||
| 
 | ||||
|     override val baseUrl = "https://www.mangasaki.org" | ||||
| 
 | ||||
|     override val lang = "en" | ||||
| 
 | ||||
|     override val supportsLatest = true | ||||
| 
 | ||||
|     // popular | ||||
|     override fun popularMangaRequest(page: Int): Request { | ||||
|         return GET("$baseUrl/directory/hot?page=${page - 1}", headers) | ||||
|     } | ||||
| 
 | ||||
|     override fun popularMangaSelector() = ".directory_list tbody tr" | ||||
| 
 | ||||
|     override fun popularMangaFromElement(element: Element): SManga { | ||||
|         val manga = SManga.create() | ||||
|         val titleElement = element.selectFirst("td a img")!! | ||||
|         manga.title = titleElement.attr("title") | ||||
|         manga.setUrlWithoutDomain(element.selectFirst("td a")!!.attr("href")) | ||||
|         manga.thumbnail_url = titleElement.attr("src") | ||||
| 
 | ||||
|         return manga | ||||
|     } | ||||
| 
 | ||||
|     override fun popularMangaNextPageSelector() = "li.pager-next a" | ||||
| 
 | ||||
|     // latest | ||||
|     override fun latestUpdatesRequest(page: Int): Request { | ||||
|         return GET("$baseUrl/directory/new?page=${page - 1}", headers) | ||||
|     } | ||||
| 
 | ||||
|     override fun latestUpdatesSelector() = popularMangaSelector() | ||||
| 
 | ||||
|     override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element) | ||||
| 
 | ||||
|     override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() | ||||
| 
 | ||||
|     // search | ||||
|     private var searchMode: Boolean = false | ||||
| 
 | ||||
|     override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { | ||||
|         return if (query.isNotEmpty()) { | ||||
|             searchMode = true | ||||
|             GET("$baseUrl/search/node/$query?page=${page - 1}", headers) | ||||
|         } else { | ||||
|             searchMode = false | ||||
|             var url = "$baseUrl/tags/" | ||||
|             filters.forEach { filter -> | ||||
|                 when (filter) { | ||||
|                     is GenreFilter -> { | ||||
|                         url += "${filter.toUriPart()}?page=${page - 1}" | ||||
|                     } | ||||
|                     else -> {} | ||||
|                 } | ||||
|             } | ||||
|             GET(url, headers) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun searchMangaSelector(): String { | ||||
|         return if (!searchMode) { | ||||
|             "div.view-content div.views-row" | ||||
|         } else { | ||||
|             "ol.search-results li.search-result" | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun searchMangaFromElement(element: Element): SManga { | ||||
|         val manga = SManga.create() | ||||
|         if (!searchMode) { | ||||
|             manga.title = element.select("div.views-field-title a").text() | ||||
|             manga.setUrlWithoutDomain(element.select("div.views-field-title a").attr("href")) | ||||
|             manga.thumbnail_url = element.select("div.views-field-field-image2 img").attr("src") | ||||
|         } else { | ||||
|             // The site doesn't show thumbnails when using search | ||||
|             val titleElement = element.select("h3.title a") | ||||
|             manga.title = titleElement.text() | ||||
|             manga.setUrlWithoutDomain(titleElement.attr("href")) | ||||
|         } | ||||
| 
 | ||||
|         return manga | ||||
|     } | ||||
| 
 | ||||
|     override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() | ||||
| 
 | ||||
|     // manga details | ||||
|     override fun mangaDetailsParse(document: Document): SManga { | ||||
|         val manga = SManga.create() | ||||
|         manga.author = document.selectFirst("div.field-name-field-author div.field-item")?.text() | ||||
|         manga.genre = document.select("div.field-name-field-genres ul li a").joinToString { it.text() } | ||||
|         manga.description = document.select("div.field-name-body div.field-item p").text() | ||||
|         manga.thumbnail_url = document.select("div.field-name-field-image2 div.field-item img").attr("src") | ||||
| 
 | ||||
|         val statusText = document.select("div.field-name-field-status div.field-item").text() | ||||
|         manga.status = when { | ||||
|             statusText.contains("Ongoing", true) -> SManga.ONGOING | ||||
|             statusText.contains("Complete", true) -> SManga.COMPLETED | ||||
|             else -> SManga.UNKNOWN | ||||
|         } | ||||
| 
 | ||||
|         return manga | ||||
|     } | ||||
| 
 | ||||
|     // chapters | ||||
|     override fun chapterListRequest(manga: SManga) = chapterListRequest(manga.url, 1) | ||||
| 
 | ||||
|     private fun chapterListRequest(url: String, page: Int): Request { | ||||
|         return GET("$baseUrl$url?page=${page - 1}", headers) | ||||
|     } | ||||
| 
 | ||||
|     override fun chapterListParse(response: Response): List<SChapter> { | ||||
|         var document = response.asJsoup() | ||||
|         val chapters = document.select(chapterListSelector()).map(::chapterFromElement).toMutableList() | ||||
|         var nextPage = 2 | ||||
| 
 | ||||
|         while (document.select(latestUpdatesNextPageSelector()).isNotEmpty()) { | ||||
|             val dirtyPage = document.select("div#block-search-form form#search-block-form").attr("action") | ||||
|             val cleaningIndex = dirtyPage.lastIndexOf("?") | ||||
|             val cleanPage = dirtyPage.substring(0, cleaningIndex) | ||||
|             document = client.newCall(chapterListRequest(cleanPage, nextPage)).execute().asJsoup() | ||||
|             chapters.addAll(document.select(chapterListSelector()).map(::chapterFromElement)) | ||||
|             nextPage++ | ||||
|         } | ||||
| 
 | ||||
|         return chapters | ||||
|     } | ||||
| 
 | ||||
|     override fun chapterListSelector() = ".chlist tbody tr" | ||||
| 
 | ||||
|     private val dateFormat = SimpleDateFormat("dd MMM yyyy", Locale.ENGLISH) | ||||
| 
 | ||||
|     override fun chapterFromElement(element: Element): SChapter { | ||||
|         val chapter = SChapter.create() | ||||
|         chapter.setUrlWithoutDomain(element.select("a").attr("href")) | ||||
|         chapter.name = element.select("a").text() | ||||
|         chapter.date_upload = try { | ||||
|             element.select("td").last()?.text()?.let { | ||||
|                 dateFormat.parse(it)?.time ?: 0L | ||||
|             } ?: 0L | ||||
|         } catch (_: ParseException) { | ||||
|             0L | ||||
|         } | ||||
| 
 | ||||
|         return chapter | ||||
|     } | ||||
| 
 | ||||
|     // pages | ||||
|     override fun pageListParse(document: Document): List<Page> { | ||||
|         val jsonString = document.select("script:containsData(showmanga)").first()!!.data() | ||||
|             .substringAfter("(Drupal.settings, ") | ||||
|             .substringBeforeLast(");") | ||||
| 
 | ||||
|         return parseJSON(jsonString).mapIndexed { i, it -> | ||||
|             Page(i, imageUrl = it) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException() | ||||
| 
 | ||||
|     override fun getFilterList() = FilterList( | ||||
|         Filter.Header("NOTE: Ignored if using text search!"), | ||||
|         Filter.Separator(), | ||||
|         GenreFilter(), | ||||
|     ) | ||||
| 
 | ||||
|     private class GenreFilter : UriPartFilter( | ||||
|         "Category", | ||||
|         arrayOf( | ||||
|             Pair("Action", "action"), | ||||
|             Pair("Adult", "adult"), | ||||
|             Pair("Adventure", "adventure"), | ||||
|             Pair("Comedy", "comedy"), | ||||
|             Pair("Crime", "crime"), | ||||
|             Pair("Drama", "drama"), | ||||
|             Pair("Dungeons", "dungeons"), | ||||
|             Pair("Ecchi", "ecchi"), | ||||
|             Pair("Fantasy", "fantasy"), | ||||
|             Pair("GenderBender", "genderbender"), | ||||
|             Pair("Gender Bender", "gender-bender"), | ||||
|             Pair("Harem", "harem"), | ||||
|             Pair("Hentai", "hentai"), | ||||
|             Pair("Historical", "historical"), | ||||
|             Pair("Horror", "horror"), | ||||
|             Pair("Isekai", "isekai"), | ||||
|             Pair("Josei", "josei"), | ||||
|             Pair("Lolicon", "lolicon"), | ||||
|             Pair("Magical Girls", "magical-girls"), | ||||
|             Pair("MartialArts", "martialarts"), | ||||
|             Pair("Martial Arts", "martial-arts"), | ||||
|             Pair("Mature", "mature"), | ||||
|             Pair("Mecha", "mecha"), | ||||
|             Pair("Medical", "medical"), | ||||
|             Pair("N/A", "na"), | ||||
|             Pair("Philosophical", "philosophical"), | ||||
|             Pair("Psychological", "psychological"), | ||||
|             Pair("SchoolLife", "schoollife"), | ||||
|             Pair("School Life", "school-life"), | ||||
|             Pair("Sci-fi", "sci-fi"), | ||||
|             Pair("Sci-fi Shounen", "sci-fi-shounen"), | ||||
|             Pair("Seinen", "seinen"), | ||||
|             Pair("Shotacon", "shotacon"), | ||||
|             Pair("Shoujo", "shoujo"), | ||||
|             Pair("ShoujoAi", "shoujoai"), | ||||
|             Pair("Shoujo Ai", "shoujo-ai"), | ||||
|             Pair("Shounen", "shounen"), | ||||
|             Pair("ShounenAi", "shounenai"), | ||||
|             Pair("Shounen-Ai", "shounen-ai"), | ||||
|             Pair("SliceofLife", "slicelife"), | ||||
|             Pair("Slice of Life", "slice-life"), | ||||
|             Pair("Smut", "smut"), | ||||
|             Pair("Sports", "sports"), | ||||
|             Pair("Superhero", "superhero"), | ||||
|             Pair("Supernatural", "supernatural"), | ||||
|             Pair("System", "system"), | ||||
|             Pair("Thriller", "thriller"), | ||||
|             Pair("Tragedy", "tragedy"), | ||||
|             Pair("Webtoons", "webtoons"), | ||||
|             Pair("Wuxia", "wuxia"), | ||||
|             Pair("Yuri", "yuri"), | ||||
|         ), | ||||
|     ) | ||||
| 
 | ||||
|     private val json: Json by injectLazy() | ||||
| 
 | ||||
|     private fun parseJSON(jsonString: String): List<String> { | ||||
|         val jsonData = json.decodeFromString<JSONData>(jsonString) | ||||
|         return jsonData.showmanga.paths.filter { it.contains("mangasaki") } | ||||
|     } | ||||
| 
 | ||||
|     private open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) : | ||||
|         Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) { | ||||
|         fun toUriPart() = vals[state].second | ||||
|     } | ||||
| 
 | ||||
|     @Serializable | ||||
|     class JSONData(val showmanga: ShowMangaData) | ||||
| 
 | ||||
|     @Serializable | ||||
|     class ShowMangaData(val paths: List<String>) | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| ext { | ||||
|     extName = 'Snow Scans' | ||||
|     extClass = '.SnowScans' | ||||
|     themePkg = 'mangathemesia' | ||||
|     baseUrl = 'https://snowscans.com' | ||||
|     overrideVersionCode = 0 | ||||
|     isNsfw = false | ||||
| } | ||||
| 
 | ||||
| apply from: "$rootDir/common.gradle" | ||||
| Before Width: | Height: | Size: 8.1 KiB | 
| Before Width: | Height: | Size: 3.9 KiB | 
| Before Width: | Height: | Size: 13 KiB | 
| Before Width: | Height: | Size: 27 KiB | 
| Before Width: | Height: | Size: 44 KiB | 
| @ -1,10 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.en.snowscans | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia | ||||
| 
 | ||||
| class SnowScans : MangaThemesia( | ||||
|     "Snow Scans", | ||||
|     "https://snowscans.com", | ||||
|     "en", | ||||
|     mangaUrlDirectory = "/series", | ||||
| ) | ||||
| @ -1,10 +0,0 @@ | ||||
| ext { | ||||
|     extName = 'Xmanhwa' | ||||
|     extClass = '.Xmanhwa' | ||||
|     themePkg = 'madara' | ||||
|     baseUrl = 'https://www.xmanhwa.me' | ||||
|     overrideVersionCode = 0 | ||||
|     isNsfw = true | ||||
| } | ||||
| 
 | ||||
| apply from: "$rootDir/common.gradle" | ||||
| Before Width: | Height: | Size: 7.6 KiB | 
| Before Width: | Height: | Size: 3.8 KiB | 
| Before Width: | Height: | Size: 12 KiB | 
| Before Width: | Height: | Size: 26 KiB | 
| Before Width: | Height: | Size: 43 KiB | 
| @ -1,14 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.en.xmanhwa | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.multisrc.madara.Madara | ||||
| 
 | ||||
| class Xmanhwa : Madara( | ||||
|     "Xmanhwa", | ||||
|     "https://www.xmanhwa.me", | ||||
|     "en", | ||||
| ) { | ||||
|     override val useLoadMoreRequest = LoadMoreStrategy.Never | ||||
|     override val useNewChapterEndpoint = true | ||||
| 
 | ||||
|     override val filterNonMangaItems = false | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| ext { | ||||
|     extName = 'Apolltoons' | ||||
|     extClass = '.Apolltoons' | ||||
|     themePkg = 'madara' | ||||
|     baseUrl = 'https://apolltoons.xyz' | ||||
|     overrideVersionCode = 0 | ||||
|     isNsfw = true | ||||
| } | ||||
| 
 | ||||
| apply from: "$rootDir/common.gradle" | ||||
| Before Width: | Height: | Size: 5.4 KiB | 
| Before Width: | Height: | Size: 2.8 KiB | 
| Before Width: | Height: | Size: 8.2 KiB | 
| Before Width: | Height: | Size: 16 KiB | 
| Before Width: | Height: | Size: 25 KiB | 
| @ -1,7 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.es.apolltoons | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.multisrc.madara.Madara | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Locale | ||||
| 
 | ||||
| class Apolltoons : Madara("Apolltoons", "https://apolltoons.xyz", "es", SimpleDateFormat("dd MMMMM, yyyy", Locale("es"))) | ||||
| @ -24,7 +24,7 @@ class KomikIndoID : ParsedHttpSource() { | ||||
|     override val client: OkHttpClient = network.cloudflareClient | ||||
|     private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US) | ||||
| 
 | ||||
|     // similar/modified theme of "https://bacakomik.co" | ||||
|     // similar/modified theme of "https://bacakomik.one" | ||||
|     override fun popularMangaRequest(page: Int): Request { | ||||
|         return GET("$baseUrl/daftar-manga/page/$page/?order=popular", headers) | ||||
|     } | ||||
|  | ||||
| @ -1,10 +0,0 @@ | ||||
| ext { | ||||
|     extName = 'KomikIndo.info' | ||||
|     extClass = '.KomikIndoInfo' | ||||
|     themePkg = 'zmanga' | ||||
|     baseUrl = 'https://komikindo.info' | ||||
|     overrideVersionCode = 1 | ||||
|     isNsfw = true | ||||
| } | ||||
| 
 | ||||
| apply from: "$rootDir/common.gradle" | ||||
| Before Width: | Height: | Size: 7.0 KiB | 
| Before Width: | Height: | Size: 3.8 KiB | 
| Before Width: | Height: | Size: 11 KiB | 
| Before Width: | Height: | Size: 22 KiB | 
| Before Width: | Height: | Size: 36 KiB | 
| @ -1,10 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.id.komikindoinfo | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.multisrc.zmanga.ZManga | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Locale | ||||
| 
 | ||||
| class KomikIndoInfo : ZManga("KomikIndo.info", "https://komikindo.info", "id", dateFormat = SimpleDateFormat("MMM d, yyyy", Locale("id"))) { | ||||
| 
 | ||||
|     override val hasProjectPage = true | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| ext { | ||||
|     extName = 'Arctic Scan' | ||||
|     extClass = '.ArcticScan' | ||||
|     themePkg = 'madara' | ||||
|     baseUrl = 'https://arcticscan.top' | ||||
|     overrideVersionCode = 0 | ||||
|     isNsfw = false | ||||
| } | ||||
| 
 | ||||
| apply from: "$rootDir/common.gradle" | ||||
| Before Width: | Height: | Size: 3.1 KiB | 
| Before Width: | Height: | Size: 1.8 KiB | 
| Before Width: | Height: | Size: 4.1 KiB | 
| Before Width: | Height: | Size: 7.2 KiB | 
| Before Width: | Height: | Size: 9.9 KiB | 
| @ -1,14 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.pt.arcticscan | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.multisrc.madara.Madara | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Locale | ||||
| 
 | ||||
| class ArcticScan : Madara( | ||||
|     "Arctic Scan", | ||||
|     "https://arcticscan.top", | ||||
|     "pt-BR", | ||||
|     dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT), | ||||
| ) { | ||||
|     override val useNewChapterEndpoint = true | ||||
| } | ||||
| @ -1,22 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <application> | ||||
|         <activity | ||||
|             android:name=".pt.blackscans.BlackScansUrlActivity" | ||||
|             android:excludeFromRecents="true" | ||||
|             android:exported="true" | ||||
|             android:theme="@android:style/Theme.NoDisplay"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW" /> | ||||
| 
 | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <category android:name="android.intent.category.BROWSABLE" /> | ||||
| 
 | ||||
|                 <data | ||||
|                     android:host="blackscans.site" | ||||
|                     android:pathPattern="/series/..*" | ||||
|                     android:scheme="https" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|     </application> | ||||
| </manifest> | ||||
| @ -1,7 +0,0 @@ | ||||
| ext { | ||||
|     extName = 'Black Scans' | ||||
|     extClass = '.BlackScans' | ||||
|     extVersionCode = 1 | ||||
| } | ||||
| 
 | ||||
| apply from: "$rootDir/common.gradle" | ||||
| Before Width: | Height: | Size: 2.8 KiB | 
| Before Width: | Height: | Size: 1.6 KiB | 
| Before Width: | Height: | Size: 3.7 KiB | 
| Before Width: | Height: | Size: 6.7 KiB | 
| Before Width: | Height: | Size: 9.2 KiB | 
| @ -1,179 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.pt.blackscans | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import eu.kanade.tachiyomi.network.GET | ||||
| import eu.kanade.tachiyomi.network.POST | ||||
| import eu.kanade.tachiyomi.network.interceptor.rateLimitHost | ||||
| 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.HttpSource | ||||
| import kotlinx.serialization.SerialName | ||||
| import kotlinx.serialization.Serializable | ||||
| import kotlinx.serialization.decodeFromString | ||||
| import kotlinx.serialization.json.Json | ||||
| import kotlinx.serialization.json.decodeFromStream | ||||
| import okhttp3.HttpUrl.Companion.toHttpUrl | ||||
| import okhttp3.MediaType.Companion.toMediaType | ||||
| import okhttp3.Request | ||||
| import okhttp3.RequestBody | ||||
| import okhttp3.RequestBody.Companion.toRequestBody | ||||
| import okhttp3.Response | ||||
| import okio.Buffer | ||||
| import rx.Observable | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.text.SimpleDateFormat | ||||
| 
 | ||||
| class BlackScans : HttpSource() { | ||||
| 
 | ||||
|     override val name = "Black Scans" | ||||
| 
 | ||||
|     override val baseUrl = "https://blackscans.site" | ||||
| 
 | ||||
|     override val lang = "pt-BR" | ||||
| 
 | ||||
|     override val supportsLatest = true | ||||
| 
 | ||||
|     override val client = network.cloudflareClient.newBuilder() | ||||
|         .rateLimitHost(API_URL.toHttpUrl(), 2) | ||||
|         .build() | ||||
| 
 | ||||
|     private val json: Json by injectLazy() | ||||
| 
 | ||||
|     // ============================== Popular ============================== | ||||
| 
 | ||||
|     override fun popularMangaRequest(page: Int) = GET("$API_URL/api/series/", headers) | ||||
| 
 | ||||
|     override fun popularMangaParse(response: Response): MangasPage { | ||||
|         val mangas = response.parseAs<List<MangaDto>>().map { manga -> | ||||
|             SManga.create().apply { | ||||
|                 title = manga.title | ||||
|                 thumbnail_url = "$API_URL/media/${manga.cover}" | ||||
|                 url = "/series/${manga.code}" | ||||
|             } | ||||
|         } | ||||
|         return MangasPage(mangas, false) | ||||
|     } | ||||
| 
 | ||||
|     // ============================== Latest ============================== | ||||
| 
 | ||||
|     override fun latestUpdatesRequest(page: Int) = GET("$API_URL/api/series/updates/", headers) | ||||
| 
 | ||||
|     override fun latestUpdatesParse(response: Response) = popularMangaParse(response) | ||||
| 
 | ||||
|     // ============================== Search ============================== | ||||
| 
 | ||||
|     override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = popularMangaRequest(page) | ||||
| 
 | ||||
|     override fun searchMangaParse(response: Response) = popularMangaParse(response) | ||||
| 
 | ||||
|     override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { | ||||
|         if (query.startsWith(PREFIX_SEARCH)) { | ||||
|             val mangaCode = query.substringAfter(PREFIX_SEARCH) | ||||
|             return fetchMangaDetails(SManga.create().apply { url = "/series/$mangaCode" }) | ||||
|                 .map { manga -> MangasPage(listOf(manga), false) } | ||||
|         } | ||||
| 
 | ||||
|         return super.fetchSearchManga(page, query, filters).map { mangasPage -> | ||||
|             val mangas = mangasPage.mangas.filter { manga -> manga.title.contains(query, true) } | ||||
|             mangasPage.copy(mangas) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // ============================== Details ============================= | ||||
| 
 | ||||
|     override fun getMangaUrl(manga: SManga) = "$baseUrl${manga.url}" | ||||
| 
 | ||||
|     override fun mangaDetailsRequest(manga: SManga) = | ||||
|         POST("$API_URL/api/serie/", headers, manga.createPostPayload()) | ||||
| 
 | ||||
|     override fun mangaDetailsParse(response: Response): SManga { | ||||
|         return response.parseAs<MangaDetailsDto>().let { dto -> | ||||
|             SManga.create().apply { | ||||
|                 title = dto.title | ||||
|                 description = dto.synopsis | ||||
|                 thumbnail_url = "$API_URL/media/${dto.cover}" | ||||
|                 author = dto.author | ||||
|                 artist = dto.artist | ||||
|                 genre = dto.genres.joinToString() | ||||
|                 url = "/series/${dto.code}" | ||||
|                 status = dto.status.toMangaStatus() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun String.toMangaStatus(): Int { | ||||
|         return when (this.lowercase()) { | ||||
|             "ongoing" -> SManga.ONGOING | ||||
|             "completed" -> SManga.COMPLETED | ||||
|             else -> SManga.UNKNOWN | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // ============================== Chapters ============================ | ||||
| 
 | ||||
|     override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url}" | ||||
| 
 | ||||
|     override fun chapterListRequest(manga: SManga): Request { | ||||
|         val payload = manga.createPostPayload("series_code") | ||||
|         return POST("$API_URL/api/series/chapters/", headers, payload) | ||||
|     } | ||||
| 
 | ||||
|     override fun chapterListParse(response: Response): List<SChapter> { | ||||
|         val series = response.request.body!!.parseAs<SeriesDto>() | ||||
| 
 | ||||
|         return response.parseAs<ChapterList>().chapters.map { chapter -> | ||||
|             SChapter.create().apply { | ||||
|                 name = chapter.name | ||||
|                 date_upload = chapter.uploadAt.toDate() | ||||
|                 url = "/series/${series.code}/${chapter.code}" | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // ============================== Pages =============================== | ||||
| 
 | ||||
|     override fun imageUrlParse(response: Response) = "" | ||||
| 
 | ||||
|     override fun pageListRequest(chapter: SChapter): Request { | ||||
|         val chapterCode = chapter.url.substringAfterLast("/") | ||||
|         val payload = """{"chapter_code":"$chapterCode"}""" | ||||
|             .toRequestBody("application/json".toMediaType()) | ||||
|         return POST("$API_URL/api/chapter/info/", headers, payload) | ||||
|     } | ||||
| 
 | ||||
|     override fun pageListParse(response: Response): List<Page> { | ||||
|         return response.parseAs<PagesDto>().images.mapIndexed { index, imageUrl -> | ||||
|             Page(index, imageUrl = "$API_URL//media/$imageUrl") | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // ============================== Utils =============================== | ||||
| 
 | ||||
|     @Serializable | ||||
|     private class SeriesDto(@SerialName("series_code") val code: String) | ||||
| 
 | ||||
|     private fun SManga.createPostPayload(field: String = "code"): RequestBody { | ||||
|         val mangaCode = url.substringAfterLast("/") | ||||
|         return """{"$field": "$mangaCode"}""".toRequestBody("application/json".toMediaType()) | ||||
|     } | ||||
| 
 | ||||
|     private inline fun <reified T> Response.parseAs(): T = use { | ||||
|         json.decodeFromStream(it.body.byteStream()) | ||||
|     } | ||||
|     private inline fun <reified T> RequestBody.parseAs(): T = | ||||
|         json.decodeFromString(Buffer().also { writeTo(it) }.readUtf8()) | ||||
| 
 | ||||
|     private fun String.toDate() = | ||||
|         try { dateFormat.parse(this)!!.time } catch (_: Exception) { 0 } | ||||
| 
 | ||||
|     companion object { | ||||
|         const val API_URL = "https://api.blackscans.site" | ||||
|         const val PREFIX_SEARCH = "id:" | ||||
| 
 | ||||
|         @SuppressLint("SimpleDateFormat") | ||||
|         val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'") | ||||
|     } | ||||
| } | ||||
| @ -1,37 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.pt.blackscans | ||||
| 
 | ||||
| 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 BlackScansUrlActivity : Activity() { | ||||
| 
 | ||||
|     private val tag = javaClass.simpleName | ||||
| 
 | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         val pathSegments = intent?.data?.pathSegments | ||||
|         if (pathSegments != null && pathSegments.size > 1) { | ||||
|             val item = pathSegments[1] | ||||
|             val mainIntent = Intent().apply { | ||||
|                 action = "eu.kanade.tachiyomi.SEARCH" | ||||
|                 putExtra("query", "${BlackScans.PREFIX_SEARCH}$item") | ||||
|                 putExtra("filter", packageName) | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 startActivity(mainIntent) | ||||
|             } catch (e: ActivityNotFoundException) { | ||||
|                 Log.e(tag, e.toString()) | ||||
|             } | ||||
|         } else { | ||||
|             Log.e(tag, "could not parse uri from intent $intent") | ||||
|         } | ||||
| 
 | ||||
|         finish() | ||||
|         exitProcess(0) | ||||
|     } | ||||
| } | ||||
| @ -1,44 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.pt.blackscans | ||||
| 
 | ||||
| import kotlinx.serialization.SerialName | ||||
| import kotlinx.serialization.Serializable | ||||
| import kotlinx.serialization.json.JsonNames | ||||
| 
 | ||||
| @Serializable | ||||
| class MangaDetailsDto( | ||||
|     val title: String, | ||||
|     val artist: String, | ||||
|     val author: String, | ||||
|     val code: String, | ||||
|     val genres: List<String>, | ||||
|     @SerialName("path_cover") | ||||
|     val cover: String, | ||||
|     val status: String, | ||||
|     val synopsis: String, | ||||
| ) | ||||
| 
 | ||||
| @Serializable | ||||
| class MangaDto( | ||||
|     val code: String, | ||||
|     val title: String, | ||||
|     @JsonNames("path_cover") | ||||
|     val cover: String, | ||||
| ) | ||||
| 
 | ||||
| @Serializable | ||||
| class ChapterList( | ||||
|     val chapters: List<Chapter>, | ||||
| ) | ||||
| 
 | ||||
| @Serializable | ||||
| data class Chapter( | ||||
|     val code: String, | ||||
|     val name: String, | ||||
|     @SerialName("upload_date") | ||||
|     val uploadAt: String, | ||||
| ) | ||||
| 
 | ||||
| @Serializable | ||||
| class PagesDto( | ||||
|     val images: List<String>, | ||||
| ) | ||||
 Vetle Ledaal
						Vetle Ledaal