unyeet MangaFox (#14893)
* unyeet MangaFox * change string concat to template strings * I SPENT ONE MORNING FOR NOTHING BECAUSE I FORGOT THEY HAD A MOBILE PAGE * remove trailing slash from mobile url * simplify pagelistrequest * chore: remove dependency on :lib-unpacker * Update issue_moderator.yml * Update MangaFox.kt * update genres Co-authored-by: Carlos <2092019+CarlosEsco@users.noreply.github.com>
This commit is contained in:
parent
95e6d6504b
commit
318f6fe9fb
|
@ -37,7 +37,7 @@ jobs:
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "both",
|
"type": "both",
|
||||||
"regex": ".*(mangafox|hq\\s*dragon|manga\\s*host|supermangas|superhentais|union\\s*mangas|yes\\s*mangas|manhuascan|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|mangakakalot\\.fun|manganel(?!o)|mangaonline\\.fun|mangatoday|manga\\.town|onemanga\\.info|koushoku).*",
|
"regex": ".*(hq\\s*dragon|manga\\s*host|supermangas|superhentais|union\\s*mangas|yes\\s*mangas|manhuascan|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|mangakakalot\\.fun|manganel(?!o)|mangaonline\\.fun|mangatoday|manga\\.town|onemanga\\.info|koushoku).*",
|
||||||
"ignoreCase": true,
|
"ignoreCase": true,
|
||||||
"message": "{match} will not be added back as it is too difficult to maintain. Read #3475 for more information."
|
"message": "{match} will not be added back as it is too difficult to maintain. Read #3475 for more information."
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
- CocoManga (COCO漫画) https://github.com/tachiyomiorg/tachiyomi-extensions/pull/11445
|
- CocoManga (COCO漫画) https://github.com/tachiyomiorg/tachiyomi-extensions/pull/11445
|
||||||
- CopyManga (拷贝漫画) https://github.com/tachiyomiorg/tachiyomi-extensions/pull/12376
|
- CopyManga (拷贝漫画) https://github.com/tachiyomiorg/tachiyomi-extensions/pull/12376
|
||||||
- fanfox.net (MangaFox) https://github.com/tachiyomiorg/tachiyomi-extensions/issues/988
|
|
||||||
- Hitomi.la https://github.com/tachiyomiorg/tachiyomi-extensions/pull/11613
|
- Hitomi.la https://github.com/tachiyomiorg/tachiyomi-extensions/pull/11613
|
||||||
- HQ Dragon https://github.com/tachiyomiorg/tachiyomi-extensions/pull/7065
|
- HQ Dragon https://github.com/tachiyomiorg/tachiyomi-extensions/pull/7065
|
||||||
- Koushoku https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13329
|
- Koushoku https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13329
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -0,0 +1,12 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
extName = 'MangaFox'
|
||||||
|
pkgNameSuffix = 'en.mangafox'
|
||||||
|
extClass = '.MangaFox'
|
||||||
|
extVersionCode = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
After Width: | Height: | Size: 100 KiB |
|
@ -0,0 +1,322 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.mangafox
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
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 okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class MangaFox : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val name: String = "MangaFox"
|
||||||
|
|
||||||
|
override val baseUrl: String = "https://fanfox.net"
|
||||||
|
|
||||||
|
private val mobileUrl: String = "https://m.fanfox.net"
|
||||||
|
|
||||||
|
override val lang: String = "en"
|
||||||
|
|
||||||
|
override val supportsLatest: Boolean = true
|
||||||
|
|
||||||
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
|
.rateLimit(1, 1)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun headersBuilder(): Headers.Builder = super.headersBuilder().add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
val pageStr = if (page != 1) "$page.html" else ""
|
||||||
|
return GET("$baseUrl/directory/$pageStr", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaSelector(): String = "ul.manga-list-1-list li"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||||
|
element.select("a").first().let {
|
||||||
|
setUrlWithoutDomain(it.attr("href"))
|
||||||
|
title = it.attr("title")
|
||||||
|
thumbnail_url = it.select("img").attr("abs:src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector(): String = ".pager-list-left a.active + a + a"
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
val pageStr = if (page != 1) "$page.html" else ""
|
||||||
|
return GET("$baseUrl/directory/$pageStr?latest", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector(): String = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector(): String = popularMangaNextPageSelector()
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val genres = mutableListOf<Int>()
|
||||||
|
val genresEx = mutableListOf<Int>()
|
||||||
|
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
|
addPathSegment("search")
|
||||||
|
addQueryParameter("title", query)
|
||||||
|
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is UriPartFilter -> addQueryParameter(filter.query, filter.toUriPart())
|
||||||
|
is GenreFilter -> filter.state.forEach {
|
||||||
|
when (it.state) {
|
||||||
|
Filter.TriState.STATE_INCLUDE -> genres.add(it.id)
|
||||||
|
Filter.TriState.STATE_EXCLUDE -> genresEx.add(it.id)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FilterWithMethodAndText -> {
|
||||||
|
val method = filter.state[0] as UriPartFilter
|
||||||
|
val text = filter.state[1] as TextSearchFilter
|
||||||
|
addQueryParameter(method.query, method.toUriPart())
|
||||||
|
addQueryParameter(text.query, text.state)
|
||||||
|
}
|
||||||
|
is RatingFilter -> filter.state.forEach {
|
||||||
|
addQueryParameter(it.query, it.toUriPart())
|
||||||
|
}
|
||||||
|
is TextSearchFilter -> addQueryParameter(filter.query, filter.state)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addQueryParameter("genres", genres.joinToString(","))
|
||||||
|
addQueryParameter("nogenres", genresEx.joinToString(","))
|
||||||
|
addQueryParameter("sort", "")
|
||||||
|
addQueryParameter("stype", "1")
|
||||||
|
}.build().toString()
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector(): String = "ul.manga-list-4-list li"
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector()
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||||
|
document.select(".detail-info-right").first().let {
|
||||||
|
author = it.select(".detail-info-right-say a").joinToString(", ") { it.text() }
|
||||||
|
genre = it.select(".detail-info-right-tag-list a").joinToString(", ") { it.text() }
|
||||||
|
description = it.select("p.fullcontent").first()?.text()
|
||||||
|
status = it.select(".detail-info-right-title-tip").first()?.text().orEmpty().let { parseStatus(it) }
|
||||||
|
thumbnail_url = document.select(".detail-info-cover-img").first()?.attr("abs:src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "ul.detail-main-list li a"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||||
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
|
name = element.select(".detail-main-list-main p").first()?.text().orEmpty()
|
||||||
|
date_upload = element.select(".detail-main-list-main p").last()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseChapterDate(date: String): Long {
|
||||||
|
return if ("Today" in date || " ago" in date) {
|
||||||
|
Calendar.getInstance().apply {
|
||||||
|
set(Calendar.HOUR_OF_DAY, 0)
|
||||||
|
set(Calendar.MINUTE, 0)
|
||||||
|
set(Calendar.SECOND, 0)
|
||||||
|
set(Calendar.MILLISECOND, 0)
|
||||||
|
}.timeInMillis
|
||||||
|
} else if ("Yesterday" in date) {
|
||||||
|
Calendar.getInstance().apply {
|
||||||
|
add(Calendar.DATE, -1)
|
||||||
|
set(Calendar.HOUR_OF_DAY, 0)
|
||||||
|
set(Calendar.MINUTE, 0)
|
||||||
|
set(Calendar.SECOND, 0)
|
||||||
|
set(Calendar.MILLISECOND, 0)
|
||||||
|
}.timeInMillis
|
||||||
|
} else {
|
||||||
|
kotlin.runCatching {
|
||||||
|
SimpleDateFormat("MMM d,yyyy", Locale.ENGLISH).parse(date)?.time
|
||||||
|
}.getOrNull() ?: 0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
val mobilePath = chapter.url.replace("/manga/", "/roll_manga/")
|
||||||
|
|
||||||
|
val headers = headersBuilder().set("Referer", "$mobileUrl/").build()
|
||||||
|
|
||||||
|
return GET("$mobileUrl$mobilePath", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> =
|
||||||
|
document.select("#viewer img").mapIndexed { idx, it ->
|
||||||
|
Page(idx, imageUrl = it.attr("abs:data-original"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
||||||
|
|
||||||
|
private fun parseStatus(status: String) = when {
|
||||||
|
status.contains("Ongoing") -> SManga.ONGOING
|
||||||
|
status.contains("Completed") -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList = FilterList(
|
||||||
|
NameFilter(),
|
||||||
|
EntryTypeFilter(),
|
||||||
|
CompletedFilter(),
|
||||||
|
AuthorFilter(),
|
||||||
|
ArtistFilter(),
|
||||||
|
RatingFilter(),
|
||||||
|
YearFilter(),
|
||||||
|
GenreFilter(getGenreList()),
|
||||||
|
)
|
||||||
|
|
||||||
|
open class UriPartFilter(
|
||||||
|
name: String,
|
||||||
|
val query: String,
|
||||||
|
private val vals: Array<Pair<String, String>>,
|
||||||
|
state: Int = 0
|
||||||
|
) : Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
|
||||||
|
fun toUriPart() = vals[state].second
|
||||||
|
}
|
||||||
|
|
||||||
|
open class TextSearchMethodFilter(name: String, query: String) :
|
||||||
|
UriPartFilter(name, query, arrayOf(Pair("contain", "cw"), Pair("begin", "bw"), Pair("end", "ew")))
|
||||||
|
|
||||||
|
open class TextSearchFilter(name: String, val query: String) : Filter.Text(name)
|
||||||
|
|
||||||
|
open class FilterWithMethodAndText(name: String, state: List<Filter<*>>) :
|
||||||
|
Filter.Group<Filter<*>>(name, state)
|
||||||
|
|
||||||
|
private class NameFilter : TextSearchFilter("Name", "name")
|
||||||
|
|
||||||
|
private class EntryTypeFilter : UriPartFilter(
|
||||||
|
"Type",
|
||||||
|
"type",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Any", "0"),
|
||||||
|
Pair("Japanese Manga", "1"),
|
||||||
|
Pair("Korean Manhwa", "2"),
|
||||||
|
Pair("Chinese Manhua", "3"),
|
||||||
|
Pair("European Manga", "4"),
|
||||||
|
Pair("American Manga", "5"),
|
||||||
|
Pair("HongKong Manga", "6"),
|
||||||
|
Pair("Other Manga", "7"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private class AuthorMethodFilter : TextSearchMethodFilter("Method", "author_method")
|
||||||
|
|
||||||
|
private class AuthorTextFilter : TextSearchFilter("Author", "author")
|
||||||
|
|
||||||
|
private class AuthorFilter : FilterWithMethodAndText("Author", listOf(AuthorMethodFilter(), AuthorTextFilter()))
|
||||||
|
|
||||||
|
private class ArtistMethodFilter : TextSearchMethodFilter("Method", "artist_method")
|
||||||
|
|
||||||
|
private class ArtistTextFilter : TextSearchFilter("Artist", "artist")
|
||||||
|
|
||||||
|
private class ArtistFilter : FilterWithMethodAndText("Artist", listOf(ArtistMethodFilter(), ArtistTextFilter()))
|
||||||
|
|
||||||
|
private class RatingMethodFilter : UriPartFilter(
|
||||||
|
"Method",
|
||||||
|
"rating_method",
|
||||||
|
arrayOf(
|
||||||
|
Pair("is", "eq"),
|
||||||
|
Pair("less than", "lt"),
|
||||||
|
Pair("more than", "gt"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private class RatingValueFilter : UriPartFilter(
|
||||||
|
"Rating",
|
||||||
|
"rating",
|
||||||
|
arrayOf(
|
||||||
|
Pair("any star", ""),
|
||||||
|
Pair("no star", "0"),
|
||||||
|
Pair("1 star", "1"),
|
||||||
|
Pair("2 stars", "2"),
|
||||||
|
Pair("3 stars", "3"),
|
||||||
|
Pair("4 stars", "4"),
|
||||||
|
Pair("5 stars", "5"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private class RatingFilter : Filter.Group<UriPartFilter>("Rating", listOf(RatingMethodFilter(), RatingValueFilter()))
|
||||||
|
|
||||||
|
private class YearMethodFilter : UriPartFilter(
|
||||||
|
"Method",
|
||||||
|
"released_method",
|
||||||
|
arrayOf(
|
||||||
|
Pair("on", "eq"),
|
||||||
|
Pair("before", "lt"),
|
||||||
|
Pair("after", "gt"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private class YearTextFilter : TextSearchFilter("Release year", "released")
|
||||||
|
|
||||||
|
private class YearFilter : FilterWithMethodAndText("Release year", listOf(YearMethodFilter(), YearTextFilter()))
|
||||||
|
|
||||||
|
private class CompletedFilter : UriPartFilter(
|
||||||
|
"Completed Series",
|
||||||
|
"st",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Either", "0"),
|
||||||
|
Pair("Yes", "2"),
|
||||||
|
Pair("No", "1"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private class Genre(name: String, val id: Int) : Filter.TriState(name)
|
||||||
|
private class GenreFilter(genres: List<Genre>) : Filter.Group<Genre>("Genre", genres)
|
||||||
|
|
||||||
|
// console.log([...document.querySelectorAll(".tag-box a")].map(e => `Genre("${e.innerHTML}", ${e.dataset.val})`).join(",\n"))
|
||||||
|
private fun getGenreList() = listOf(
|
||||||
|
Genre("Action", 1),
|
||||||
|
Genre("Adventure", 2),
|
||||||
|
Genre("Comedy", 3),
|
||||||
|
Genre("Drama", 4),
|
||||||
|
Genre("Fantasy", 5),
|
||||||
|
Genre("Martial Arts", 6),
|
||||||
|
Genre("Shounen", 7),
|
||||||
|
Genre("Horror", 8),
|
||||||
|
Genre("Supernatural", 9),
|
||||||
|
Genre("Harem", 10),
|
||||||
|
Genre("Psychological", 11),
|
||||||
|
Genre("Romance", 12),
|
||||||
|
Genre("School Life", 13),
|
||||||
|
Genre("Shoujo", 14),
|
||||||
|
Genre("Mystery", 15),
|
||||||
|
Genre("Sci-fi", 16),
|
||||||
|
Genre("Seinen", 17),
|
||||||
|
Genre("Tragedy", 18),
|
||||||
|
Genre("Ecchi", 19),
|
||||||
|
Genre("Sports", 20),
|
||||||
|
Genre("Slice of Life", 21),
|
||||||
|
Genre("Mature", 22),
|
||||||
|
Genre("Shoujo Ai", 23),
|
||||||
|
Genre("Webtoons", 24),
|
||||||
|
Genre("Doujinshi", 25),
|
||||||
|
Genre("One Shot", 26),
|
||||||
|
Genre("Smut", 27),
|
||||||
|
Genre("Yaoi", 28),
|
||||||
|
Genre("Josei", 29),
|
||||||
|
Genre("Historical", 30),
|
||||||
|
Genre("Shounen Ai", 31),
|
||||||
|
Genre("Gender Bender", 32),
|
||||||
|
Genre("Adult", 33),
|
||||||
|
Genre("Yuri", 34),
|
||||||
|
Genre("Mecha", 35),
|
||||||
|
Genre("Lolicon", 36),
|
||||||
|
Genre("Shotacon", 37)
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue