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 |     override val client: OkHttpClient = network.cloudflareClient | ||||||
|     private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US) |     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 { |     override fun popularMangaRequest(page: Int): Request { | ||||||
|         return GET("$baseUrl/daftar-manga/page/$page/?order=popular", headers) |         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