Compare commits
No commits in common. "ac57f5e3dde6197b96a3ae66e311c3dfeac4ec1d" and "01e67374ec95300fa6993c1df8302da0b3f4682a" have entirely different histories.
ac57f5e3dd
...
01e67374ec
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 4
|
baseVersionCode = 3
|
||||||
|
|
|
@ -130,8 +130,8 @@ abstract class GravureBlogger(
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
|
|
||||||
return document.select("div.post-body a:has(> img)").mapIndexed { i, it ->
|
return document.select("div.post-body img").mapIndexed { i, it ->
|
||||||
Page(i, imageUrl = it.absUrl("href"))
|
Page(i, imageUrl = it.absUrl("src"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 25
|
baseVersionCode = 24
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:i18n"))
|
api(project(":lib:i18n"))
|
||||||
|
|
|
@ -225,13 +225,13 @@ abstract class HeanCms(
|
||||||
throw Exception(intl.format("url_changed_error", name, name))
|
throw Exception(intl.format("url_changed_error", name, name))
|
||||||
}
|
}
|
||||||
|
|
||||||
val seriesSlug = manga.url.substringAfterLast("/").substringBefore("#")
|
val seriesId = manga.url.substringAfterLast("#")
|
||||||
|
|
||||||
val apiHeaders = headersBuilder()
|
val apiHeaders = headersBuilder()
|
||||||
.add("Accept", ACCEPT_JSON)
|
.add("Accept", ACCEPT_JSON)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return GET("$apiUrl/series/$seriesSlug", apiHeaders)
|
return GET("$apiUrl/series/id/$seriesId", apiHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
|
|
@ -1,153 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.multisrc.iken
|
|
||||||
|
|
||||||
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.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 eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
abstract class Iken(
|
|
||||||
override val name: String,
|
|
||||||
override val lang: String,
|
|
||||||
override val baseUrl: String,
|
|
||||||
) : HttpSource() {
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient
|
|
||||||
|
|
||||||
private val json by injectLazy<Json>()
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
|
||||||
.set("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
private var genres = emptyList<Pair<String, String>>()
|
|
||||||
private val titleCache by lazy {
|
|
||||||
val response = client.newCall(GET("$baseUrl/api/query?perPage=9999", headers)).execute()
|
|
||||||
val data = response.parseAs<SearchResponse>()
|
|
||||||
|
|
||||||
data.posts
|
|
||||||
.filterNot { it.isNovel }
|
|
||||||
.also { posts ->
|
|
||||||
genres = posts.flatMap {
|
|
||||||
it.genres.map { genre ->
|
|
||||||
genre.name to genre.id.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.associateBy { it.slug }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/home", headers)
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
val slugs = document.select("div:contains(Popular) + div.swiper div.manga-swipe > a")
|
|
||||||
.map { it.absUrl("href").substringAfterLast("/series/") }
|
|
||||||
|
|
||||||
val entries = slugs.mapNotNull {
|
|
||||||
titleCache[it]?.toSManga(baseUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
return MangasPage(entries, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", getFilterList())
|
|
||||||
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = "$baseUrl/api/query".toHttpUrl().newBuilder().apply {
|
|
||||||
addQueryParameter("page", page.toString())
|
|
||||||
addQueryParameter("perPage", perPage.toString())
|
|
||||||
addQueryParameter("searchTerm", query.trim())
|
|
||||||
filters.filterIsInstance<UrlPartFilter>().forEach {
|
|
||||||
it.addUrlParameter(this)
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
val data = response.parseAs<SearchResponse>()
|
|
||||||
val page = response.request.url.queryParameter("page")!!.toInt()
|
|
||||||
|
|
||||||
val entries = data.posts
|
|
||||||
.filterNot { it.isNovel }
|
|
||||||
.map { it.toSManga(baseUrl) }
|
|
||||||
|
|
||||||
val hasNextPage = data.totalCount > (page * perPage)
|
|
||||||
|
|
||||||
return MangasPage(entries, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
|
||||||
StatusFilter(),
|
|
||||||
TypeFilter(),
|
|
||||||
GenreFilter(genres),
|
|
||||||
Filter.Header("Open popular mangas if genre filter is empty"),
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
|
||||||
val id = manga.url.substringAfterLast("#")
|
|
||||||
val url = "$baseUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid="
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga): String {
|
|
||||||
val slug = manga.url.substringBeforeLast("#")
|
|
||||||
|
|
||||||
return "$baseUrl/series/$slug"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
|
||||||
val data = response.parseAs<Post<Manga>>()
|
|
||||||
|
|
||||||
assert(!data.post.isNovel) { "Novels are unsupported" }
|
|
||||||
|
|
||||||
// genres are only returned in search call
|
|
||||||
// and not when fetching details
|
|
||||||
return data.post.toSManga(baseUrl).apply {
|
|
||||||
genre = titleCache[data.post.slug]?.getGenres()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val data = response.parseAs<Post<ChapterListResponse>>()
|
|
||||||
|
|
||||||
assert(!data.post.isNovel) { "Novels are unsupported" }
|
|
||||||
|
|
||||||
return data.post.chapters
|
|
||||||
.filter { it.isPublic() }
|
|
||||||
.map { it.toSChapter(data.post.slug) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
return document.select("main section > img").mapIndexed { idx, img ->
|
|
||||||
Page(idx, imageUrl = img.absUrl("src"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response) =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T =
|
|
||||||
json.decodeFromString(body.string())
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val perPage = 18
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 30
|
baseVersionCode = 28
|
||||||
|
|
|
@ -188,11 +188,7 @@ abstract class LibGroup(
|
||||||
} else {
|
} else {
|
||||||
it.replace("\\", "")
|
it.replace("\\", "")
|
||||||
}
|
}
|
||||||
str.parseAs<AuthToken>().let { auth ->
|
returnValue = str.parseAs<AuthToken>()
|
||||||
if (auth.isValid()) {
|
|
||||||
returnValue = auth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
latch.countDown()
|
latch.countDown()
|
||||||
}
|
}
|
||||||
|
@ -328,7 +324,7 @@ abstract class LibGroup(
|
||||||
if (currentBranch.value.branchId == defaultBranchId && sortingList == "ms_mixing") { // ms_mixing with default branch from api
|
if (currentBranch.value.branchId == defaultBranchId && sortingList == "ms_mixing") { // ms_mixing with default branch from api
|
||||||
chapters.add(it.value.toSChapter(slugUrl, defaultBranchId, isScanUser()))
|
chapters.add(it.value.toSChapter(slugUrl, defaultBranchId, isScanUser()))
|
||||||
} else if (defaultBranchId == null && sortingList == "ms_mixing") { // ms_mixing with first branch in chapter
|
} else if (defaultBranchId == null && sortingList == "ms_mixing") { // ms_mixing with first branch in chapter
|
||||||
if (chapters.any { chpIt -> chpIt.chapter_number == it.value.number.toFloat() }) {
|
if (chapters.any { chpIt -> chpIt.chapter_number == it.value.itemNumber }) {
|
||||||
chapters.add(it.value.toSChapter(slugUrl, currentBranch.value.branchId, isScanUser()))
|
chapters.add(it.value.toSChapter(slugUrl, currentBranch.value.branchId, isScanUser()))
|
||||||
}
|
}
|
||||||
} else if (sortingList == "ms_combining") { // ms_combining
|
} else if (sortingList == "ms_combining") { // ms_combining
|
||||||
|
|
|
@ -202,6 +202,7 @@ class Chapter(
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val number: String,
|
val number: String,
|
||||||
val volume: String,
|
val volume: String,
|
||||||
|
@SerialName("item_number") val itemNumber: Float?,
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
class Branch(
|
class Branch(
|
||||||
|
@ -240,7 +241,7 @@ class Chapter(
|
||||||
url = "/$slugUrl/chapter?$branchStr&volume=$volume&number=$number"
|
url = "/$slugUrl/chapter?$branchStr&volume=$volume&number=$number"
|
||||||
scanlator = getTeamName(branchId) ?: if (isScanUser) getUserName(branchId) else null
|
scanlator = getTeamName(branchId) ?: if (isScanUser) getUserName(branchId) else null
|
||||||
date_upload = runCatching { LibGroup.simpleDateFormat.parse(first(branchId)!!.createdAt)!!.time }.getOrDefault(0L)
|
date_upload = runCatching { LibGroup.simpleDateFormat.parse(first(branchId)!!.createdAt)!!.time }.getOrDefault(0L)
|
||||||
chapter_number = number.toFloat()
|
chapter_number = itemNumber ?: -1f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,8 +267,8 @@ class Pages(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class AuthToken(
|
class AuthToken(
|
||||||
private val auth: Auth?,
|
private val auth: Auth,
|
||||||
private val token: Token?,
|
private val token: Token,
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
class Auth(
|
class Auth(
|
||||||
|
@ -282,15 +283,13 @@ class AuthToken(
|
||||||
@SerialName("access_token") val accessToken: String,
|
@SerialName("access_token") val accessToken: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun isValid(): Boolean = auth != null && token != null
|
|
||||||
|
|
||||||
fun isExpired(): Boolean {
|
fun isExpired(): Boolean {
|
||||||
val currentTime = System.currentTimeMillis()
|
val currentTime = System.currentTimeMillis()
|
||||||
val expiresIn = token!!.timestamp + (token.expiresIn * 1000)
|
val expiresIn = token.timestamp + (token.expiresIn * 1000)
|
||||||
return expiresIn < currentTime
|
return expiresIn < currentTime
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getToken(): String = "${token!!.tokenType} ${token.accessToken}"
|
fun getToken(): String = "${token.tokenType} ${token.accessToken}"
|
||||||
|
|
||||||
fun getUserId(): Int = auth!!.id
|
fun getUserId(): Int = auth.id
|
||||||
}
|
}
|
||||||
|
|
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 16 KiB |
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.readallcomicscom
|
package eu.kanade.tachiyomi.multisrc.readallcomics
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
@ -10,10 +10,8 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
|
@ -21,13 +19,11 @@ import org.jsoup.nodes.Element
|
||||||
import org.jsoup.select.Elements
|
import org.jsoup.select.Elements
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
class ReadAllComics : ParsedHttpSource() {
|
abstract class ReadAllComics(
|
||||||
|
override val name: String,
|
||||||
override val name = "ReadAllComics"
|
override val baseUrl: String,
|
||||||
|
override val lang: String,
|
||||||
override val baseUrl = "https://readallcomics.com"
|
) : ParsedHttpSource() {
|
||||||
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val supportsLatest = false
|
override val supportsLatest = false
|
||||||
|
|
||||||
|
@ -38,7 +34,7 @@ class ReadAllComics : ParsedHttpSource() {
|
||||||
.rateLimit(2)
|
.rateLimit(2)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private fun archivedCategoryInterceptor(chain: Interceptor.Chain): Response {
|
protected open fun archivedCategoryInterceptor(chain: Interceptor.Chain): Response {
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
val response = chain.proceed(request)
|
val response = chain.proceed(request)
|
||||||
|
|
||||||
|
@ -47,7 +43,7 @@ class ReadAllComics : ParsedHttpSource() {
|
||||||
request.url.toString(),
|
request.url.toString(),
|
||||||
)
|
)
|
||||||
|
|
||||||
val newUrl = document.selectFirst(".description-archive > p > span > a")
|
val newUrl = document.selectFirst(archivedCategorySelector())
|
||||||
?.attr("href")?.toHttpUrlOrNull()
|
?.attr("href")?.toHttpUrlOrNull()
|
||||||
?: return response
|
?: return response
|
||||||
|
|
||||||
|
@ -64,18 +60,33 @@ class ReadAllComics : ParsedHttpSource() {
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected open fun archivedCategorySelector() = ".description-archive > p > span > a"
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) =
|
override fun popularMangaRequest(page: Int) =
|
||||||
GET("$baseUrl${if (page > 1)"/page/$page/" else ""}", headers)
|
GET("$baseUrl${if (page > 1)"/page/$page/" else ""}", headers)
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
val mangas = document.select(popularMangaSelector()).mapNotNull {
|
||||||
|
nullablePopularManga(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasNextPage = document.select(popularMangaNextPageSelector()).first() != null
|
||||||
|
|
||||||
|
return MangasPage(mangas, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun nullablePopularManga(element: Element): SManga? {
|
||||||
val manga = SManga.create().apply {
|
val manga = SManga.create().apply {
|
||||||
val category = element.classNames()
|
val category = element.classNames()
|
||||||
.firstOrNull { it.startsWith("category-") }!!
|
.firstOrNull { it.startsWith("category-") }
|
||||||
.substringAfter("category-")
|
?.substringAfter("category-")
|
||||||
|
?: return null
|
||||||
|
|
||||||
url = "/category/$category/"
|
url = "/category/$category/"
|
||||||
title = category.replace("-", " ").capitalizeEachWord()
|
title = element.select(popularMangaTitleSelector()).text()
|
||||||
thumbnail_url = element.select("img").attr("abs:src")
|
thumbnail_url = element.select(popularMangaThumbnailSelector()).attr("abs:src")
|
||||||
}
|
}
|
||||||
|
|
||||||
return manga
|
return manga
|
||||||
|
@ -83,6 +94,8 @@ class ReadAllComics : ParsedHttpSource() {
|
||||||
|
|
||||||
override fun popularMangaSelector() = "#post-area > div"
|
override fun popularMangaSelector() = "#post-area > div"
|
||||||
override fun popularMangaNextPageSelector() = "div.pagenavi > a.next"
|
override fun popularMangaNextPageSelector() = "div.pagenavi > a.next"
|
||||||
|
protected open fun popularMangaTitleSelector() = "h2"
|
||||||
|
protected open fun popularMangaThumbnailSelector() = "img"
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
return if (page == 1) {
|
return if (page == 1) {
|
||||||
|
@ -94,15 +107,10 @@ class ReadAllComics : ParsedHttpSource() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
GET("$baseUrl/?story=${query.trim()}&s=&type=${searchType()}", headers)
|
||||||
addQueryParameter("story", query)
|
|
||||||
addQueryParameter("s", "")
|
|
||||||
addQueryParameter("type", "comic")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
protected open fun searchType() = "comic"
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
searchPageElements = response.asJsoup().select(searchMangaSelector())
|
searchPageElements = response.asJsoup().select(searchMangaSelector())
|
||||||
|
@ -125,32 +133,27 @@ class ReadAllComics : ParsedHttpSource() {
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
title = element.text()
|
title = element.text().trim()
|
||||||
thumbnail_url = "https://fakeimg.pl/200x300/?text=No%20Cover%0AOn%20Search&font_size=62"
|
thumbnail_url = searchCover
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaSelector() = ".categories a"
|
override fun searchMangaSelector() = ".categories a"
|
||||||
override fun searchMangaNextPageSelector() = null
|
override fun searchMangaNextPageSelector() = null
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
title = document.selectFirst("h1")!!.text()
|
title = document.select(mangaDetailsTitleSelector()).text().trim()
|
||||||
genre = document.select("p strong").joinToString { it.text() }
|
genre = document.select(mangaDetailsGenreSelector()).joinToString { it.text().trim() }
|
||||||
author = document.select("p > strong").last()?.text()
|
author = document.select(mangaDetailsAuthorSelector()).last()?.text()?.trim()
|
||||||
description = buildString {
|
description = document.select(mangaDetailsDescriptionSelector()).text().trim()
|
||||||
document.select(".b > strong").forEach { element ->
|
thumbnail_url = document.select(mangaDetailsThumbnailSelector()).attr("abs:src")
|
||||||
val vol = element.previousElementSibling()
|
|
||||||
if (isNotBlank()) {
|
|
||||||
append("\n\n")
|
|
||||||
}
|
|
||||||
if (vol?.tagName() == "span") {
|
|
||||||
append(vol.text(), "\n")
|
|
||||||
}
|
|
||||||
append(element.text())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thumbnail_url = document.select("p img").attr("abs:src")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected open fun mangaDetailsTitleSelector() = "h1"
|
||||||
|
protected open fun mangaDetailsGenreSelector() = "p strong"
|
||||||
|
protected open fun mangaDetailsAuthorSelector() = "p > strong"
|
||||||
|
protected open fun mangaDetailsDescriptionSelector() = ".b > strong"
|
||||||
|
protected open fun mangaDetailsThumbnailSelector() = "p img"
|
||||||
|
|
||||||
override fun chapterListSelector() = ".list-story a"
|
override fun chapterListSelector() = ".list-story a"
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||||
|
@ -159,25 +162,15 @@ class ReadAllComics : ParsedHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
return document.select("body img:not(body div[id=\"logo\"] img)").mapIndexed { idx, element ->
|
return document.select(pageListSelector()).mapIndexed { idx, element ->
|
||||||
Page(idx, "", element.attr("abs:src"))
|
Page(idx, "", element.attr("abs:src"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.capitalizeEachWord(): String {
|
protected open fun pageListSelector() = "body > div img"
|
||||||
val result = StringBuilder(length)
|
|
||||||
var capitalize = true
|
companion object {
|
||||||
for (char in this) {
|
private const val searchCover = "https://fakeimg.pl/200x300/?text=No%20Cover%0AOn%20Search&font_size=62"
|
||||||
result.append(
|
|
||||||
if (capitalize) {
|
|
||||||
char.uppercase()
|
|
||||||
} else {
|
|
||||||
char.lowercase()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
capitalize = char.isWhitespace()
|
|
||||||
}
|
|
||||||
return result.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) =
|
override fun imageUrlParse(document: Document) =
|
||||||
|
@ -190,4 +183,6 @@ class ReadAllComics : ParsedHttpSource() {
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
override fun latestUpdatesNextPageSelector() =
|
override fun latestUpdatesNextPageSelector() =
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
|
override fun popularMangaFromElement(element: Element) =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Galaxy'
|
extName = 'Galaxy'
|
||||||
extClass = '.GalaxyFactory'
|
extClass = '.GalaxyFactory'
|
||||||
extVersionCode = 4
|
extVersionCode = 2
|
||||||
isNsfw = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.galaxy
|
package eu.kanade.tachiyomi.extension.all.galaxy
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class GalaxyFactory : SourceFactory {
|
class GalaxyFactory : SourceFactory {
|
||||||
|
|
||||||
|
@ -15,53 +8,8 @@ class GalaxyFactory : SourceFactory {
|
||||||
override val id = 2602904659965278831
|
override val id = 2602904659965278831
|
||||||
}
|
}
|
||||||
|
|
||||||
class GalaxyManga :
|
class GalaxyManga : Galaxy("Galaxy Manga", "https://ayoub-zrr.xyz", "ar") {
|
||||||
Galaxy("Galaxy Manga", "https://galaxymanga.net", "ar"),
|
|
||||||
ConfigurableSource {
|
|
||||||
override val id = 2729515745226258240
|
override val id = 2729515745226258240
|
||||||
|
|
||||||
override val baseUrl by lazy { getPrefBaseUrl() }
|
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val RESTART_APP = ".لتطبيق الإعدادات الجديدة أعد تشغيل التطبيق"
|
|
||||||
private const val BASE_URL_PREF_TITLE = "تعديل الرابط"
|
|
||||||
private const val BASE_URL_PREF = "overrideBaseUrl"
|
|
||||||
private const val BASE_URL_PREF_SUMMARY = ".للاستخدام المؤقت. تحديث التطبيق سيؤدي الى حذف الإعدادات"
|
|
||||||
private const val DEFAULT_BASE_URL_PREF = "defaultBaseUrl"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
val baseUrlPref = androidx.preference.EditTextPreference(screen.context).apply {
|
|
||||||
key = BASE_URL_PREF
|
|
||||||
title = BASE_URL_PREF_TITLE
|
|
||||||
summary = BASE_URL_PREF_SUMMARY
|
|
||||||
this.setDefaultValue(super.baseUrl)
|
|
||||||
dialogTitle = BASE_URL_PREF_TITLE
|
|
||||||
dialogMessage = "Default: ${super.baseUrl}"
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, _ ->
|
|
||||||
Toast.makeText(screen.context, RESTART_APP, Toast.LENGTH_LONG).show()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
screen.addPreference(baseUrlPref)
|
|
||||||
}
|
|
||||||
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, super.baseUrl)!!
|
|
||||||
|
|
||||||
init {
|
|
||||||
preferences.getString(DEFAULT_BASE_URL_PREF, null).let { prefDefaultBaseUrl ->
|
|
||||||
if (prefDefaultBaseUrl != super.baseUrl) {
|
|
||||||
preferences.edit()
|
|
||||||
.putString(BASE_URL_PREF, super.baseUrl)
|
|
||||||
.putString(DEFAULT_BASE_URL_PREF, super.baseUrl)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createSources() = listOf(
|
override fun createSources() = listOf(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Hitomi'
|
extName = 'Hitomi'
|
||||||
extClass = '.HitomiFactory'
|
extClass = '.HitomiFactory'
|
||||||
extVersionCode = 32
|
extVersionCode = 31
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.hitomi
|
package eu.kanade.tachiyomi.extension.all.hitomi
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
@ -24,14 +19,9 @@ import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
|
@ -40,14 +30,13 @@ import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@OptIn(ExperimentalUnsignedTypes::class)
|
@OptIn(ExperimentalUnsignedTypes::class)
|
||||||
class Hitomi(
|
class Hitomi(
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
private val nozomiLang: String,
|
private val nozomiLang: String,
|
||||||
) : HttpSource(), ConfigurableSource {
|
) : HttpSource() {
|
||||||
|
|
||||||
override val name = "Hitomi"
|
override val name = "Hitomi"
|
||||||
|
|
||||||
|
@ -61,14 +50,7 @@ class Hitomi(
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
override val client = network.cloudflareClient
|
||||||
.addInterceptor(::Intercept)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
private fun imageType() = preferences.getString(PREF_IMAGETYPE, "webp")!!
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.set("referer", "$baseUrl/")
|
.set("referer", "$baseUrl/")
|
||||||
|
@ -506,7 +488,7 @@ class Hitomi(
|
||||||
private suspend fun Gallery.toSManga() = SManga.create().apply {
|
private suspend fun Gallery.toSManga() = SManga.create().apply {
|
||||||
title = this@toSManga.title
|
title = this@toSManga.title
|
||||||
url = galleryurl
|
url = galleryurl
|
||||||
author = groups?.joinToString { it.formatted } ?: artists?.joinToString { it.formatted }
|
author = groups?.joinToString { it.formatted }
|
||||||
artist = artists?.joinToString { it.formatted }
|
artist = artists?.joinToString { it.formatted }
|
||||||
genre = tags?.joinToString { it.formatted }
|
genre = tags?.joinToString { it.formatted }
|
||||||
thumbnail_url = files.first().let {
|
thumbnail_url = files.first().let {
|
||||||
|
@ -585,25 +567,14 @@ class Hitomi(
|
||||||
|
|
||||||
gallery.files.mapIndexed { idx, img ->
|
gallery.files.mapIndexed { idx, img ->
|
||||||
val hash = img.hash
|
val hash = img.hash
|
||||||
|
|
||||||
val typePref = imageType()
|
|
||||||
val avif = img.hasavif == 1 && typePref == "avif"
|
|
||||||
val jxl = img.hasjxl == 1 && typePref == "jxl"
|
|
||||||
|
|
||||||
val commonId = commonImageId()
|
val commonId = commonImageId()
|
||||||
val imageId = imageIdFromHash(hash)
|
val imageId = imageIdFromHash(hash)
|
||||||
val subDomain = 'a' + subdomainOffset(imageId)
|
val subDomain = 'a' + subdomainOffset(imageId)
|
||||||
|
|
||||||
val imageUrl = when {
|
|
||||||
jxl -> "https://${subDomain}a.$domain/jxl/$commonId$imageId/$hash.jxl"
|
|
||||||
avif -> "https://${subDomain}a.$domain/avif/$commonId$imageId/$hash.avif"
|
|
||||||
else -> "https://${subDomain}a.$domain/webp/$commonId$imageId/$hash.webp"
|
|
||||||
}
|
|
||||||
|
|
||||||
Page(
|
Page(
|
||||||
idx,
|
idx,
|
||||||
"$baseUrl/reader/$id.html",
|
"$baseUrl/reader/$id.html",
|
||||||
imageUrl,
|
"https://${subDomain}a.$domain/webp/$commonId$imageId/$hash.webp",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -686,45 +657,6 @@ class Hitomi(
|
||||||
return hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1")
|
return hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = PREF_IMAGETYPE
|
|
||||||
title = "Images Type"
|
|
||||||
entries = arrayOf("webp", "avif", "jxl")
|
|
||||||
entryValues = arrayOf("webp", "avif", "jxl")
|
|
||||||
summary = "Clear chapter cache to apply changes"
|
|
||||||
setDefaultValue("webp")
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<Int>.toBytesList(): ByteArray = this.map { it.toByte() }.toByteArray()
|
|
||||||
private val signatureOne = listOf(0xFF, 0x0A).toBytesList()
|
|
||||||
private val signatureTwo = listOf(0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A).toBytesList()
|
|
||||||
fun ByteArray.startsWith(byteArray: ByteArray): Boolean {
|
|
||||||
if (this.size < byteArray.size) return false
|
|
||||||
return this.sliceArray(byteArray.indices).contentEquals(byteArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Intercept(chain: Interceptor.Chain): Response {
|
|
||||||
val response = chain.proceed(chain.request())
|
|
||||||
if (response.headers["Content-Type"] != "application/octet-stream") {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
val bytesPeek = max(signatureOne.size, signatureTwo.size).toLong()
|
|
||||||
val bytesArray = response.peekBody(bytesPeek).bytes()
|
|
||||||
if (!(bytesArray.startsWith(signatureOne) || bytesArray.startsWith(signatureTwo))) {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
val type = "image/jxl"
|
|
||||||
val body = response.body.bytes().toResponseBody(type.toMediaType())
|
|
||||||
return response.newBuilder()
|
|
||||||
.body(body)
|
|
||||||
.header("Content-Type", type)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||||
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
||||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||||
|
@ -732,8 +664,4 @@ class Hitomi(
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
||||||
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
|
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val PREF_IMAGETYPE = "pref_image_type"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,9 +21,6 @@ class Gallery(
|
||||||
@Serializable
|
@Serializable
|
||||||
class ImageFile(
|
class ImageFile(
|
||||||
val hash: String,
|
val hash: String,
|
||||||
val haswebp: Int,
|
|
||||||
val hasavif: Int,
|
|
||||||
val hasjxl: Int,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<application android:icon="@mipmap/ic_launcher">
|
<application android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
android:name=".all.ninenineninehentai.AnimeHUrlActivity"
|
android:name=".all.ninenineninehentai.NineNineNineHentaiUrlActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:host="animeh.to"/>
|
<data android:host="999hentai.net"/>
|
||||||
<data android:scheme="https"/>
|
<data android:scheme="https"/>
|
||||||
<data android:pathPattern="/hchapter/..*"/>
|
<data android:pathPattern="/hchapter/..*"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeH'
|
extName = '999Hentai'
|
||||||
extClass = '.AnimeHFactory'
|
extClass = '.NineNineNineHentaiFactory'
|
||||||
extVersionCode = 7
|
extVersionCode = 6
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 18 KiB |
|
@ -1,19 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.ninenineninehentai
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
|
|
||||||
class AnimeHFactory : SourceFactory {
|
|
||||||
override fun createSources() = listOf(
|
|
||||||
AnimeHAll(),
|
|
||||||
AnimeHEn(),
|
|
||||||
AnimeHJa(),
|
|
||||||
AnimeHZh(),
|
|
||||||
AnimeHEs(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
class AnimeHAll : AnimeH("all") { override val id = 5098173700376022513 }
|
|
||||||
class AnimeHEn : AnimeH("en") { override val id = 4370122548313941497 }
|
|
||||||
class AnimeHJa : AnimeH("ja", "jp") { override val id = 8948948503520127713 }
|
|
||||||
class AnimeHZh : AnimeH("zh", "cn") { override val id = 3874510362699054213 }
|
|
||||||
class AnimeHEs : AnimeH("es") { override val id = 2790053117909987291 }
|
|
|
@ -35,16 +35,16 @@ import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
open class AnimeH(
|
open class NineNineNineHentai(
|
||||||
final override val lang: String,
|
final override val lang: String,
|
||||||
private val siteLang: String = lang,
|
private val siteLang: String = lang,
|
||||||
) : HttpSource(), ConfigurableSource {
|
) : HttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
override val name = "AnimeH"
|
override val name = "999Hentai"
|
||||||
|
|
||||||
override val baseUrl = "https://animeh.to"
|
override val baseUrl = "https://999hentai.net"
|
||||||
|
|
||||||
private val apiUrl = "https://api.animeh.to/api"
|
private val apiUrl = "https://hapi.999hentai.net/api"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.ninenineninehentai
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
|
|
||||||
|
class NineNineNineHentaiFactory : SourceFactory {
|
||||||
|
override fun createSources() = listOf(
|
||||||
|
NineNineNineHentai("all"),
|
||||||
|
NineNineNineHentai("en"),
|
||||||
|
NineNineNineHentai("ja", "jp"),
|
||||||
|
NineNineNineHentai("zh", "cn"),
|
||||||
|
NineNineNineHentai("es"),
|
||||||
|
)
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class AnimeHUrlActivity : Activity() {
|
class NineNineNineHentaiUrlActivity : Activity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val pathSegments = intent?.data?.pathSegments
|
val pathSegments = intent?.data?.pathSegments
|
||||||
|
@ -15,17 +15,17 @@ class AnimeHUrlActivity : Activity() {
|
||||||
val id = pathSegments[1]
|
val id = pathSegments[1]
|
||||||
val mainIntent = Intent().apply {
|
val mainIntent = Intent().apply {
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
action = "eu.kanade.tachiyomi.SEARCH"
|
||||||
putExtra("query", "${AnimeH.SEARCH_PREFIX}$id")
|
putExtra("query", "${NineNineNineHentai.SEARCH_PREFIX}$id")
|
||||||
putExtra("filter", packageName)
|
putExtra("filter", packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startActivity(mainIntent)
|
startActivity(mainIntent)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Log.e("AnimeHUrlActivity", e.toString())
|
Log.e("999HentaiUrlActivity", e.toString())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.e("AnimeHUrlActivity", "could not parse uri from intent $intent")
|
Log.e("999HentaiUrlActivity", "could not parse uri from intent $intent")
|
||||||
}
|
}
|
||||||
|
|
||||||
finish()
|
finish()
|
|
@ -1,23 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name=".all.pandachaika.PandaChaikaUrlActivity"
|
|
||||||
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="panda.chaika.moe"
|
|
||||||
android:pathPattern="/archive/..*"
|
|
||||||
android:scheme="https"/>
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'PandaChaika'
|
extName = 'PandaChaika'
|
||||||
extClass = '.PandaChaikaFactory'
|
extClass = '.PandaChaikaFactory'
|
||||||
extVersionCode = 2
|
extVersionCode = 1
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.pandachaika
|
package eu.kanade.tachiyomi.extension.all.pandachaika
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservable
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
@ -44,9 +42,6 @@ class PandaChaika(
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val fakkuRegex = Regex("""(?:https?://)?(?:www\.)?fakku\.net/hentai/""")
|
|
||||||
private val ehentaiRegex = Regex("""(?:https?://)?e-hentai\.org/g/""")
|
|
||||||
|
|
||||||
// Popular
|
// Popular
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
return GET("$baseSearchUrl/?tags=$searchLang&sort=rating&apply=&json=&page=$page", headers)
|
return GET("$baseSearchUrl/?tags=$searchLang&sort=rating&apply=&json=&page=$page", headers)
|
||||||
|
@ -78,76 +73,6 @@ class PandaChaika(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
return when {
|
|
||||||
query.startsWith(PREFIX_ID_SEARCH) -> {
|
|
||||||
val id = query.removePrefix(PREFIX_ID_SEARCH).toInt()
|
|
||||||
client.newCall(GET("$baseUrl/api?archive=$id", headers))
|
|
||||||
.asObservable()
|
|
||||||
.map { response ->
|
|
||||||
searchMangaByIdParse(response, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
query.startsWith(PREFIX_EHEN_ID_SEARCH) -> {
|
|
||||||
val id = query.removePrefix(PREFIX_EHEN_ID_SEARCH).replace(ehentaiRegex, "")
|
|
||||||
val baseLink = "https://e-hentai.org/g/"
|
|
||||||
val fullLink = baseSearchUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
addQueryParameter("qsearch", baseLink + id)
|
|
||||||
addQueryParameter("json", "")
|
|
||||||
}.build()
|
|
||||||
client.newCall(GET(fullLink, headers))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map {
|
|
||||||
val archive = it.parseAs<ArchiveResponse>().archives.getOrNull(0)?.toSManga() ?: throw Exception("Not Found")
|
|
||||||
MangasPage(listOf(archive), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
query.startsWith(PREFIX_FAK_ID_SEARCH) -> {
|
|
||||||
val slug = query.removePrefix(PREFIX_FAK_ID_SEARCH).replace(fakkuRegex, "")
|
|
||||||
val baseLink = "https://www.fakku.net/hentai/"
|
|
||||||
val fullLink = baseSearchUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
addQueryParameter("qsearch", baseLink + slug)
|
|
||||||
addQueryParameter("json", "")
|
|
||||||
}.build()
|
|
||||||
client.newCall(GET(fullLink, headers))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map {
|
|
||||||
val archive = it.parseAs<ArchiveResponse>().archives.getOrNull(0)?.toSManga() ?: throw Exception("Not Found")
|
|
||||||
MangasPage(listOf(archive), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
query.startsWith(PREFIX_SOURCE_SEARCH) -> {
|
|
||||||
val url = query.removePrefix(PREFIX_SOURCE_SEARCH)
|
|
||||||
client.newCall(GET("$baseSearchUrl/?qsearch=$url&json=", headers))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map {
|
|
||||||
val archive = it.parseAs<ArchiveResponse>().archives.getOrNull(0)?.toSManga() ?: throw Exception("Not Found")
|
|
||||||
MangasPage(listOf(archive), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.fetchSearchManga(page, query, filters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchMangaByIdParse(response: Response, id: Int = 0): MangasPage {
|
|
||||||
val title = response.parseAs<Archive>().title
|
|
||||||
val fullLink = baseSearchUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
addQueryParameter("qsearch", title)
|
|
||||||
addQueryParameter("json", "")
|
|
||||||
}.build()
|
|
||||||
val archive = client.newCall(GET(fullLink, headers))
|
|
||||||
.execute()
|
|
||||||
.parseAs<ArchiveResponse>().archives
|
|
||||||
.find {
|
|
||||||
it.id == id
|
|
||||||
}
|
|
||||||
?.toSManga()
|
|
||||||
?: throw Exception("Invalid ID")
|
|
||||||
|
|
||||||
return MangasPage(listOf(archive), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
val library = response.parseAs<ArchiveResponse>()
|
val library = response.parseAs<ArchiveResponse>()
|
||||||
|
|
||||||
|
@ -325,11 +250,4 @@ class PandaChaika(
|
||||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||||
override fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException()
|
override fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException()
|
||||||
override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
|
override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val PREFIX_ID_SEARCH = "id:"
|
|
||||||
const val PREFIX_FAK_ID_SEARCH = "fakku:"
|
|
||||||
const val PREFIX_EHEN_ID_SEARCH = "ehentai:"
|
|
||||||
const val PREFIX_SOURCE_SEARCH = "source:"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
val dateReformat = SimpleDateFormat("EEEE, d MMM yyyy HH:mm (z)", Locale.ENGLISH)
|
val dateReformat = SimpleDateFormat("EEEE, d MMM yyyy HH:mm (z)", Locale.ENGLISH)
|
||||||
fun filterTags(include: String = "", exclude: List<String> = emptyList(), tags: List<String>): String? {
|
fun filterTags(include: String = "", exclude: List<String> = emptyList(), tags: List<String>): String {
|
||||||
return tags.filter { it.startsWith("$include:") && exclude.none { substring -> it.startsWith("$substring:") } }
|
return tags.filter { it.startsWith("$include:") && exclude.none { substring -> it.startsWith("$substring:") } }
|
||||||
.joinToString {
|
.joinToString {
|
||||||
it.substringAfter(":").replace("_", " ").split(" ").joinToString(" ") { s ->
|
it.substringAfter(":").replace("_", " ").split(" ").joinToString(" ") { s ->
|
||||||
|
@ -16,13 +16,13 @@ fun filterTags(include: String = "", exclude: List<String> = emptyList(), tags:
|
||||||
if (sr.isLowerCase()) sr.titlecase(Locale.getDefault()) else sr.toString()
|
if (sr.isLowerCase()) sr.titlecase(Locale.getDefault()) else sr.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.takeIf { it.isNotBlank() }
|
}
|
||||||
}
|
}
|
||||||
fun getReadableSize(bytes: Double): String {
|
fun getReadableSize(bytes: Double): String {
|
||||||
return when {
|
return when {
|
||||||
bytes >= 300 * 1000 * 1000 -> "${"%.2f".format(bytes / (1000.0 * 1000.0 * 1000.0))} GB"
|
bytes >= 300 * 1024 * 1024 -> "${"%.2f".format(bytes / (1024.0 * 1024.0 * 1024.0))} GB"
|
||||||
bytes >= 100 * 1000 -> "${"%.2f".format(bytes / (1000.0 * 1000.0))} MB"
|
bytes >= 100 * 1024 -> "${"%.2f".format(bytes / (1024.0 * 1024.0))} MB"
|
||||||
bytes >= 1000 -> "${"%.2f".format(bytes / (1000.0))} kB"
|
bytes >= 1024 -> "${"%.2f".format(bytes / (1024.0))} KB"
|
||||||
else -> "$bytes B"
|
else -> "$bytes B"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,14 +31,13 @@ fun getReadableSize(bytes: Double): String {
|
||||||
class Archive(
|
class Archive(
|
||||||
val download: String,
|
val download: String,
|
||||||
val posted: Long,
|
val posted: Long,
|
||||||
val title: String,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class LongArchive(
|
class LongArchive(
|
||||||
private val thumbnail: String,
|
private val thumbnail: String,
|
||||||
private val title: String,
|
private val title: String,
|
||||||
val id: Int,
|
private val id: Int,
|
||||||
private val posted: Long?,
|
private val posted: Long?,
|
||||||
private val public_date: Long?,
|
private val public_date: Long?,
|
||||||
private val filecount: Int,
|
private val filecount: Int,
|
||||||
|
@ -51,47 +50,35 @@ class LongArchive(
|
||||||
val groups = filterTags("group", tags = tags)
|
val groups = filterTags("group", tags = tags)
|
||||||
val artists = filterTags("artist", tags = tags)
|
val artists = filterTags("artist", tags = tags)
|
||||||
val publishers = filterTags("publisher", tags = tags)
|
val publishers = filterTags("publisher", tags = tags)
|
||||||
val characters = filterTags("character", tags = tags)
|
|
||||||
val male = filterTags("male", tags = tags)
|
val male = filterTags("male", tags = tags)
|
||||||
val female = filterTags("female", tags = tags)
|
val female = filterTags("female", tags = tags)
|
||||||
val others = filterTags(exclude = listOf("female", "male", "artist", "publisher", "group", "parody"), tags = tags)
|
val others = filterTags(exclude = listOf("female", "male", "artist", "publisher", "group", "parody"), tags = tags)
|
||||||
val parodies = filterTags("parody", tags = tags)
|
val parodies = filterTags("parody", tags = tags)
|
||||||
var appended = false
|
|
||||||
|
|
||||||
url = id.toString()
|
url = id.toString()
|
||||||
title = this@LongArchive.title
|
title = this@LongArchive.title
|
||||||
thumbnail_url = thumbnail
|
thumbnail_url = thumbnail
|
||||||
author = groups ?: artists
|
author = groups.ifEmpty { artists }
|
||||||
artist = artists
|
artist = artists
|
||||||
genre = listOf(male, female, others).joinToString()
|
genre = listOf(male, female, others).joinToString()
|
||||||
description = buildString {
|
description = buildString {
|
||||||
append("Uploader: ", uploader.ifEmpty { "Anonymous" }, "\n")
|
append("Uploader: ", uploader.ifEmpty { "Anonymous" }, "\n")
|
||||||
publishers?.let {
|
publishers.takeIf { it.isNotBlank() }?.let {
|
||||||
append("Publishers: ", it, "\n")
|
append("Publishers: ", it, "\n\n")
|
||||||
}
|
}
|
||||||
append("\n")
|
parodies.takeIf { it.isNotBlank() }?.let {
|
||||||
|
append("Parodies: ", it, "\n\n")
|
||||||
parodies?.let {
|
|
||||||
append("Parodies: ", it, "\n")
|
|
||||||
appended = true
|
|
||||||
}
|
}
|
||||||
characters?.let {
|
male.takeIf { it.isNotBlank() }?.let {
|
||||||
append("Characters: ", it, "\n")
|
|
||||||
appended = true
|
|
||||||
}
|
|
||||||
if (appended) append("\n")
|
|
||||||
|
|
||||||
male?.let {
|
|
||||||
append("Male tags: ", it, "\n\n")
|
append("Male tags: ", it, "\n\n")
|
||||||
}
|
}
|
||||||
female?.let {
|
female.takeIf { it.isNotBlank() }?.let {
|
||||||
append("Female tags: ", it, "\n\n")
|
append("Female tags: ", it, "\n\n")
|
||||||
}
|
}
|
||||||
others?.let {
|
others.takeIf { it.isNotBlank() }?.let {
|
||||||
append("Other tags: ", it, "\n\n")
|
append("Other tags: ", it, "\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
title_jpn?.takeIf { it.isNotEmpty() }?.let { append("Japanese Title: ", it, "\n") }
|
title_jpn?.let { append("Japanese Title: ", it, "\n") }
|
||||||
append("Pages: ", filecount, "\n")
|
append("Pages: ", filecount, "\n")
|
||||||
append("File Size: ", getReadableSize(filesize), "\n")
|
append("File Size: ", getReadableSize(filesize), "\n")
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ fun getFilters(): FilterList {
|
||||||
TextFilter("Female Tags", "female"),
|
TextFilter("Female Tags", "female"),
|
||||||
TextFilter("Artists", "artist"),
|
TextFilter("Artists", "artist"),
|
||||||
TextFilter("Parodies", "parody"),
|
TextFilter("Parodies", "parody"),
|
||||||
TextFilter("Characters", "character"),
|
|
||||||
Filter.Separator(),
|
Filter.Separator(),
|
||||||
TextFilter("Reason", "reason"),
|
TextFilter("Reason", "reason"),
|
||||||
TextFilter("Uploader", "reason"),
|
TextFilter("Uploader", "reason"),
|
||||||
|
@ -53,11 +52,11 @@ private val getTypes = listOf(
|
||||||
|
|
||||||
private val getSortsList: List<Pair<String, String>> = listOf(
|
private val getSortsList: List<Pair<String, String>> = listOf(
|
||||||
Pair("Public Date", "public_date"),
|
Pair("Public Date", "public_date"),
|
||||||
Pair("Posted Date", "posted"),
|
Pair("Posted Date", "posted_date"),
|
||||||
Pair("Title", "title"),
|
Pair("Title", "title"),
|
||||||
Pair("Japanese Title", "title_jpn"),
|
Pair("Japanese Title", "title_jpn"),
|
||||||
Pair("Rating", "rating"),
|
Pair("Rating", "rating"),
|
||||||
Pair("Images", "filecount"),
|
Pair("Images", "images"),
|
||||||
Pair("File Size", "filesize"),
|
Pair("File Size", "size"),
|
||||||
Pair("Category", "category"),
|
Pair("Category", "category"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.pandachaika
|
|
||||||
|
|
||||||
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 PandaChaikaUrlActivity : Activity() {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
val pathSegments = intent?.data?.pathSegments
|
|
||||||
if (pathSegments != null && pathSegments.size > 2) {
|
|
||||||
val id = "${pathSegments[1]}/${pathSegments[2]}"
|
|
||||||
val mainIntent = Intent().apply {
|
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
|
||||||
putExtra("query", "${PandaChaika.PREFIX_ID_SEARCH}$id")
|
|
||||||
putExtra("filter", packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
startActivity(mainIntent)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
Log.e("KoharuUrlActivity", "Could not start activity", e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.e("KoharuUrlActivity", "Could not parse URI from intent $intent")
|
|
||||||
}
|
|
||||||
|
|
||||||
finish()
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Union Mangas'
|
extName = 'Union Mangas'
|
||||||
extClass = '.UnionMangasFactory'
|
extClass = '.UnionMangasFactory'
|
||||||
extVersionCode = 6
|
extVersionCode = 5
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -203,7 +203,7 @@ class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SEARCH_PREFIX = "slug:"
|
const val SEARCH_PREFIX = "slug:"
|
||||||
val apiUrl = "https://api.novelfull.us/api"
|
val apiUrl = "https://app.unionmanga.xyz/api"
|
||||||
val oldApiUrl = "https://api.unionmanga.xyz"
|
val oldApiUrl = "https://api.unionmanga.xyz"
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Hadess'
|
extName = 'Crow Scans'
|
||||||
extClass = '.Hadess'
|
extClass = '.CrowScans'
|
||||||
themePkg = 'madara'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://www.hadess.xyz'
|
baseUrl = 'https://crowscans.com'
|
||||||
overrideVersionCode = 0
|
overrideVersionCode = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 21 KiB |
|
@ -0,0 +1,12 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ar.crowscans
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class CrowScans : MangaThemesia(
|
||||||
|
"Crow Scans",
|
||||||
|
"https://crowscans.com",
|
||||||
|
"ar",
|
||||||
|
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||||
|
)
|
|
@ -1,26 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.crowscans
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class Hadess : Madara(
|
|
||||||
"Hadess",
|
|
||||||
"https://www.hadess.xyz",
|
|
||||||
"ar",
|
|
||||||
dateFormat = SimpleDateFormat("dd MMMM، yyyy", Locale("ar")),
|
|
||||||
) {
|
|
||||||
override val versionId = 2
|
|
||||||
|
|
||||||
override val client = super.client.newBuilder()
|
|
||||||
.rateLimit(3)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override val useNewChapterEndpoint = true
|
|
||||||
|
|
||||||
override val useLoadMoreRequest = LoadMoreStrategy.Always
|
|
||||||
|
|
||||||
override val mangaDetailsSelectorStatus =
|
|
||||||
".summary-heading:contains(الحالة) + ${super.mangaDetailsSelectorStatus}"
|
|
||||||
}
|
|
|
@ -2,9 +2,8 @@ ext {
|
||||||
extName = 'MangaNoon'
|
extName = 'MangaNoon'
|
||||||
extClass = '.MangaNoon'
|
extClass = '.MangaNoon'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://manjanoon.co'
|
baseUrl = 'https://manjanoon.org'
|
||||||
overrideVersionCode = 3
|
overrideVersionCode = 2
|
||||||
isNsfw = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,89 +1,12 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.manganoon
|
package eu.kanade.tachiyomi.extension.ar.manganoon
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import java.text.SimpleDateFormat
|
||||||
import org.jsoup.nodes.Element
|
import java.util.Locale
|
||||||
import java.util.Calendar
|
|
||||||
|
|
||||||
class MangaNoon : MangaThemesia(
|
class MangaNoon : MangaThemesia(
|
||||||
"مانجا نون",
|
"مانجا نون",
|
||||||
"https://manjanoon.co",
|
"https://manjanoon.org",
|
||||||
"ar",
|
"ar",
|
||||||
) {
|
dateFormat = SimpleDateFormat("MMM d, yyy", Locale("ar")),
|
||||||
|
)
|
||||||
override fun chapterFromElement(element: Element): SChapter {
|
|
||||||
return super.chapterFromElement(element).apply {
|
|
||||||
date_upload = element.selectFirst(".chapterdate")?.text().parseChapterDate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// From Galaxy
|
|
||||||
override fun String?.parseChapterDate(): Long {
|
|
||||||
this ?: return 0L
|
|
||||||
|
|
||||||
val number = Regex("""(\d+)""").find(this)?.value?.toIntOrNull() ?: 0
|
|
||||||
val cal = Calendar.getInstance()
|
|
||||||
|
|
||||||
return when {
|
|
||||||
listOf("second", "ثانية").any { contains(it, true) } -> {
|
|
||||||
cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
|
|
||||||
}
|
|
||||||
|
|
||||||
contains("دقيقتين", true) -> {
|
|
||||||
cal.apply { add(Calendar.MINUTE, -2) }.timeInMillis
|
|
||||||
}
|
|
||||||
listOf("minute", "دقائق").any { contains(it, true) } -> {
|
|
||||||
cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
|
||||||
}
|
|
||||||
|
|
||||||
contains("ساعتان", true) -> {
|
|
||||||
cal.apply { add(Calendar.HOUR, -2) }.timeInMillis
|
|
||||||
}
|
|
||||||
listOf("hour", "ساعات").any { contains(it, true) } -> {
|
|
||||||
cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
|
||||||
}
|
|
||||||
|
|
||||||
contains("يوم", true) -> {
|
|
||||||
cal.apply { add(Calendar.DAY_OF_YEAR, -1) }.timeInMillis
|
|
||||||
}
|
|
||||||
contains("يومين", true) -> {
|
|
||||||
cal.apply { add(Calendar.DAY_OF_YEAR, -2) }.timeInMillis
|
|
||||||
}
|
|
||||||
listOf("day", "أيام").any { contains(it, true) } -> {
|
|
||||||
cal.apply { add(Calendar.DAY_OF_YEAR, -number) }.timeInMillis
|
|
||||||
}
|
|
||||||
|
|
||||||
contains("أسبوع", true) -> {
|
|
||||||
cal.apply { add(Calendar.WEEK_OF_YEAR, -1) }.timeInMillis
|
|
||||||
}
|
|
||||||
contains("أسبوعين", true) -> {
|
|
||||||
cal.apply { add(Calendar.WEEK_OF_YEAR, -2) }.timeInMillis
|
|
||||||
}
|
|
||||||
listOf("week", "أسابيع").any { contains(it, true) } -> {
|
|
||||||
cal.apply { add(Calendar.WEEK_OF_YEAR, -number) }.timeInMillis
|
|
||||||
}
|
|
||||||
|
|
||||||
contains("شهر", true) -> {
|
|
||||||
cal.apply { add(Calendar.MONTH, -1) }.timeInMillis
|
|
||||||
}
|
|
||||||
contains("شهرين", true) -> {
|
|
||||||
cal.apply { add(Calendar.MONTH, -2) }.timeInMillis
|
|
||||||
}
|
|
||||||
listOf("month", "أشهر").any { contains(it, true) } -> {
|
|
||||||
cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
|
|
||||||
}
|
|
||||||
|
|
||||||
contains("سنة", true) -> {
|
|
||||||
cal.apply { add(Calendar.YEAR, -1) }.timeInMillis
|
|
||||||
}
|
|
||||||
contains("سنتان", true) -> {
|
|
||||||
cal.apply { add(Calendar.YEAR, -2) }.timeInMillis
|
|
||||||
}
|
|
||||||
listOf("year", "سنوات").any { contains(it, true) } -> {
|
|
||||||
cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> 0L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,8 +2,8 @@ ext {
|
||||||
extName = 'MangaSwat'
|
extName = 'MangaSwat'
|
||||||
extClass = '.MangaSwat'
|
extClass = '.MangaSwat'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://maxlevelteam.com'
|
baseUrl = 'https://t1manga.com'
|
||||||
overrideVersionCode = 20
|
overrideVersionCode = 19
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 23 KiB |
|
@ -24,7 +24,7 @@ import java.util.Locale
|
||||||
class MangaSwat :
|
class MangaSwat :
|
||||||
MangaThemesia(
|
MangaThemesia(
|
||||||
"MangaSwat",
|
"MangaSwat",
|
||||||
"https://maxlevelteam.com",
|
"https://t1manga.com",
|
||||||
"ar",
|
"ar",
|
||||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'NoonScan'
|
|
||||||
extClass = '.NoonScan'
|
|
||||||
themePkg = 'mangathemesia'
|
|
||||||
baseUrl = 'https://noonscan.com'
|
|
||||||
overrideVersionCode = 0
|
|
||||||
isNsfw = false
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 43 KiB |
|
@ -1,12 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.noonscan
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class NoonScan : MangaThemesia(
|
|
||||||
"نون سكان",
|
|
||||||
"https://noonscan.com",
|
|
||||||
"ar",
|
|
||||||
dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale("ar")),
|
|
||||||
)
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Novels Town'
|
||||||
|
extClass = '.NovelsTown'
|
||||||
|
themePkg = 'madara'
|
||||||
|
baseUrl = 'https://novelstown.com'
|
||||||
|
overrideVersionCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 19 KiB |
|
@ -0,0 +1,7 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ar.novelstown
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
|
||||||
|
class NovelsTown : Madara("Novels Town", "https://novelstown.com", "ar") {
|
||||||
|
override val mangaSubString = "الاعمال"
|
||||||
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Vortex Scans'
|
extName = 'Vortex Scans'
|
||||||
extClass = '.VortexScans'
|
extClass = '.VortexScans'
|
||||||
themePkg = 'iken'
|
extVersionCode = 33
|
||||||
overrideVersionCode = 33
|
|
||||||
isNsfw = false
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.multisrc.iken
|
package eu.kanade.tachiyomi.extension.en.arvenscans
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
@ -18,7 +18,7 @@ class SearchResponse(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Manga(
|
class Manga(
|
||||||
private val id: Int,
|
val id: Int,
|
||||||
val slug: String,
|
val slug: String,
|
||||||
private val postTitle: String,
|
private val postTitle: String,
|
||||||
private val postContent: String? = null,
|
private val postContent: String? = null,
|
||||||
|
@ -29,7 +29,7 @@ class Manga(
|
||||||
private val artist: String? = null,
|
private val artist: String? = null,
|
||||||
private val seriesType: String? = null,
|
private val seriesType: String? = null,
|
||||||
private val seriesStatus: String? = null,
|
private val seriesStatus: String? = null,
|
||||||
val genres: List<Genre> = emptyList(),
|
private val genres: List<Name>? = emptyList(),
|
||||||
) {
|
) {
|
||||||
fun toSManga(baseUrl: String) = SManga.create().apply {
|
fun toSManga(baseUrl: String) = SManga.create().apply {
|
||||||
url = "$slug#$id"
|
url = "$slug#$id"
|
||||||
|
@ -70,16 +70,10 @@ class Manga(
|
||||||
"MANHWA" -> add("Manhwa")
|
"MANHWA" -> add("Manhwa")
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
genres.forEach { add(it.name) }
|
genres?.forEach { add(it.name) }
|
||||||
}.distinct().joinToString()
|
}.distinct().joinToString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Genre(
|
|
||||||
val id: Int,
|
|
||||||
val name: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Name(val name: String)
|
class Name(val name: String)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.multisrc.iken
|
package eu.kanade.tachiyomi.extension.en.arvenscans
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
@ -65,8 +65,37 @@ class TypeFilter : SelectFilter(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class GenreFilter(genres: List<Pair<String, String>>) : CheckBoxGroup(
|
class GenreFilter : CheckBoxGroup(
|
||||||
"Genres",
|
"Genres",
|
||||||
"genreIds",
|
"genreIds",
|
||||||
genres,
|
listOf(
|
||||||
|
Pair("Action", "1"),
|
||||||
|
Pair("Adventure", "13"),
|
||||||
|
Pair("Comedy", "7"),
|
||||||
|
Pair("Drama", "2"),
|
||||||
|
Pair("elf", "25"),
|
||||||
|
Pair("Fantas", "28"),
|
||||||
|
Pair("Fantasy", "8"),
|
||||||
|
Pair("Historical", "19"),
|
||||||
|
Pair("Horror", "9"),
|
||||||
|
Pair("Josei", "21"),
|
||||||
|
Pair("Manhwa", "5"),
|
||||||
|
Pair("Martial Arts", "6"),
|
||||||
|
Pair("Mature", "12"),
|
||||||
|
Pair("Monsters", "14"),
|
||||||
|
Pair("Reincarnation", "16"),
|
||||||
|
Pair("Revenge", "17"),
|
||||||
|
Pair("Romance", "20"),
|
||||||
|
Pair("School Life", "23"),
|
||||||
|
Pair("Seinen", "10"),
|
||||||
|
Pair("shojo", "26"),
|
||||||
|
Pair("Shoujo", "22"),
|
||||||
|
Pair("Shounen", "3"),
|
||||||
|
Pair("Slice Of Life", "18"),
|
||||||
|
Pair("Sports", "4"),
|
||||||
|
Pair("Supernatural", "11"),
|
||||||
|
Pair("System", "15"),
|
||||||
|
Pair("terror", "24"),
|
||||||
|
Pair("Video Games", "27"),
|
||||||
|
),
|
||||||
)
|
)
|
|
@ -1,9 +1,145 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.arvenscans
|
package eu.kanade.tachiyomi.extension.en.arvenscans
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.iken.Iken
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
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 eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class VortexScans : Iken(
|
class VortexScans : HttpSource() {
|
||||||
"Vortex Scans",
|
|
||||||
"en",
|
override val name = "Vortex Scans"
|
||||||
"https://vortexscans.org",
|
|
||||||
)
|
override val baseUrl = "https://vortexscans.org"
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
|
private val json by injectLazy<Json>()
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.set("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
private val titleCache by lazy {
|
||||||
|
val response = client.newCall(GET("$baseUrl/api/query?perPage=9999", headers)).execute()
|
||||||
|
val data = response.parseAs<SearchResponse>()
|
||||||
|
|
||||||
|
data.posts
|
||||||
|
.filterNot { it.isNovel }
|
||||||
|
.associateBy { it.slug }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/home", headers)
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val slugs = document.select("div:contains(Popular) + div.swiper div.manga-swipe > a")
|
||||||
|
.map { it.absUrl("href").substringAfterLast("/series/") }
|
||||||
|
|
||||||
|
val entries = slugs.mapNotNull {
|
||||||
|
titleCache[it]?.toSManga(baseUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(entries, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", getFilterList())
|
||||||
|
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = "$baseUrl/api/query".toHttpUrl().newBuilder().apply {
|
||||||
|
addQueryParameter("page", page.toString())
|
||||||
|
addQueryParameter("perPage", perPage.toString())
|
||||||
|
addQueryParameter("searchTerm", query.trim())
|
||||||
|
filters.filterIsInstance<UrlPartFilter>().forEach {
|
||||||
|
it.addUrlParameter(this)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val data = response.parseAs<SearchResponse>()
|
||||||
|
val page = response.request.url.queryParameter("page")!!.toInt()
|
||||||
|
|
||||||
|
val entries = data.posts
|
||||||
|
.filterNot { it.isNovel }
|
||||||
|
.map { it.toSManga(baseUrl) }
|
||||||
|
|
||||||
|
val hasNextPage = data.totalCount > (page * perPage)
|
||||||
|
|
||||||
|
return MangasPage(entries, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
StatusFilter(),
|
||||||
|
TypeFilter(),
|
||||||
|
GenreFilter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
|
val id = manga.url.substringAfterLast("#")
|
||||||
|
val url = "$baseUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid="
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMangaUrl(manga: SManga): String {
|
||||||
|
val slug = manga.url.substringBeforeLast("#")
|
||||||
|
|
||||||
|
return "$baseUrl/series/$slug"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
val data = response.parseAs<Post<Manga>>()
|
||||||
|
|
||||||
|
assert(!data.post.isNovel) { "Novels are unsupported" }
|
||||||
|
|
||||||
|
// genres are only returned in search call
|
||||||
|
// and not when fetching details
|
||||||
|
return data.post.toSManga(baseUrl).apply {
|
||||||
|
genre = titleCache[data.post.slug]?.getGenres()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val data = response.parseAs<Post<ChapterListResponse>>()
|
||||||
|
|
||||||
|
assert(!data.post.isNovel) { "Novels are unsupported" }
|
||||||
|
|
||||||
|
return data.post.chapters
|
||||||
|
.filter { it.isPublic() }
|
||||||
|
.map { it.toSChapter(data.post.slug) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
return document.select("main section > img").mapIndexed { idx, img ->
|
||||||
|
Page(idx, imageUrl = img.absUrl("src"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response) =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T =
|
||||||
|
json.decodeFromString(body.string())
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val perPage = 18
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Asura Scans'
|
extName = 'Asura Scans'
|
||||||
extClass = '.AsuraScans'
|
extClass = '.AsuraScans'
|
||||||
extVersionCode = 36
|
themePkg = 'mangathemesia'
|
||||||
|
baseUrl = 'https://asuracomic.net'
|
||||||
|
overrideVersionCode = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,50 +1,21 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.asurascans
|
package eu.kanade.tachiyomi.extension.en.asurascans
|
||||||
|
|
||||||
import android.app.Application
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesiaAlt
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
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 kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.concurrent.thread
|
|
||||||
|
|
||||||
class AsuraScans : ParsedHttpSource(), ConfigurableSource {
|
|
||||||
|
|
||||||
override val name = "Asura Scans"
|
|
||||||
|
|
||||||
override val baseUrl = "https://asuracomic.net"
|
|
||||||
|
|
||||||
private val apiUrl = "https://gg.asuracomic.net/api"
|
|
||||||
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
private val dateFormat = SimpleDateFormat("MMMM d yyyy", Locale.US)
|
|
||||||
|
|
||||||
private val preferences: SharedPreferences =
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
|
|
||||||
|
class AsuraScans : MangaThemesiaAlt(
|
||||||
|
"Asura Scans",
|
||||||
|
"https://asuracomic.net",
|
||||||
|
"en",
|
||||||
|
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US),
|
||||||
|
randomUrlPrefKey = "pref_permanent_manga_url_2_en",
|
||||||
|
) {
|
||||||
init {
|
init {
|
||||||
// remove legacy preferences
|
// remove legacy preferences
|
||||||
preferences.run {
|
preferences.run {
|
||||||
|
@ -54,256 +25,47 @@ class AsuraScans : ParsedHttpSource(), ConfigurableSource {
|
||||||
if (contains("pref_base_url_host")) {
|
if (contains("pref_base_url_host")) {
|
||||||
edit().remove("pref_base_url_host").apply()
|
edit().remove("pref_base_url_host").apply()
|
||||||
}
|
}
|
||||||
if (contains("pref_permanent_manga_url_2_en")) {
|
|
||||||
edit().remove("pref_permanent_manga_url_2_en").apply()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
override val client = super.client.newBuilder()
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
|
||||||
.rateLimit(1, 3)
|
.rateLimit(1, 3)
|
||||||
|
.apply {
|
||||||
|
val interceptors = interceptors()
|
||||||
|
val index = interceptors.indexOfFirst { "Brotli" in it.javaClass.simpleName }
|
||||||
|
if (index >= 0) {
|
||||||
|
interceptors.add(interceptors.removeAt(index))
|
||||||
|
}
|
||||||
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override val seriesDescriptionSelector = "div.desc p, div.entry-content p, div[itemprop=description]:not(:has(p))"
|
||||||
.add("Referer", "$baseUrl/")
|
override val seriesArtistSelector = ".fmed b:contains(artist)+span, .infox span:contains(artist)"
|
||||||
|
override val seriesAuthorSelector = ".fmed b:contains(author)+span, .infox span:contains(author)"
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request =
|
override val pageSelector = "div.rdminimal > img, div.rdminimal > p > img, div.rdminimal > a > img, div.rdminimal > p > a > img, " +
|
||||||
GET("$baseUrl/series?genres=&status=-1&types=-1&order=rating&page=$page", headers)
|
"div.rdminimal > noscript > img, div.rdminimal > p > noscript > img, div.rdminimal > a > noscript > img, div.rdminimal > p > a > noscript > img"
|
||||||
|
|
||||||
override fun popularMangaSelector() = searchMangaSelector()
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element) = searchMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request =
|
|
||||||
GET("$baseUrl/series?genres=&status=-1&types=-1&order=update&page=$page", headers)
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = searchMangaSelector()
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = "$baseUrl/series".toHttpUrl().newBuilder()
|
val request = super.searchMangaRequest(page, query, filters)
|
||||||
|
if (query.isBlank()) return request
|
||||||
|
|
||||||
url.addQueryParameter("page", page.toString())
|
val url = request.url.newBuilder()
|
||||||
|
.addPathSegment("page/$page/")
|
||||||
|
.removeAllQueryParameters("page")
|
||||||
|
.removeAllQueryParameters("title")
|
||||||
|
.addQueryParameter("s", query)
|
||||||
|
.build()
|
||||||
|
|
||||||
if (query.isNotBlank()) {
|
return request.newBuilder()
|
||||||
url.addQueryParameter("name", query)
|
.url(url)
|
||||||
}
|
.build()
|
||||||
|
|
||||||
val genres = filters.firstInstanceOrNull<GenreFilter>()?.state.orEmpty()
|
|
||||||
.filter(Genre::state)
|
|
||||||
.map(Genre::id)
|
|
||||||
.joinToString(",")
|
|
||||||
|
|
||||||
val status = filters.firstInstanceOrNull<StatusFilter>()?.toUriPart() ?: "-1"
|
|
||||||
val types = filters.firstInstanceOrNull<TypeFilter>()?.toUriPart() ?: "-1"
|
|
||||||
val order = filters.firstInstanceOrNull<OrderFilter>()?.toUriPart() ?: "rating"
|
|
||||||
|
|
||||||
url.addQueryParameter("genres", genres)
|
|
||||||
url.addQueryParameter("status", status)
|
|
||||||
url.addQueryParameter("types", types)
|
|
||||||
url.addQueryParameter("order", order)
|
|
||||||
|
|
||||||
return GET(url.build(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = "div.grid > a[href]"
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
|
||||||
setUrlWithoutDomain(element.attr("abs:href").toPermSlugIfNeeded())
|
|
||||||
title = element.selectFirst("div.block > span.block")!!.ownText()
|
|
||||||
thumbnail_url = element.selectFirst("img")?.attr("abs:src")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "div.flex > a.flex.bg-themecolor:contains(Next)"
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList {
|
|
||||||
fetchFilters()
|
|
||||||
val filters = mutableListOf<Filter<*>>()
|
|
||||||
if (filtersState == FiltersState.FETCHED) {
|
|
||||||
filters += listOf(
|
|
||||||
GenreFilter("Genres", getGenreFilters()),
|
|
||||||
StatusFilter("Status", getStatusFilters()),
|
|
||||||
TypeFilter("Types", getTypeFilters()),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
filters += Filter.Header("Press 'Reset' to attempt to fetch the filters")
|
|
||||||
}
|
|
||||||
|
|
||||||
filters += OrderFilter(
|
|
||||||
"Order by",
|
|
||||||
listOf(
|
|
||||||
Pair("Rating", "rating"),
|
|
||||||
Pair("Update", "update"),
|
|
||||||
Pair("Latest", "latest"),
|
|
||||||
Pair("Z-A", "desc"),
|
|
||||||
Pair("A-Z", "asc"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return FilterList(filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getGenreFilters(): List<Genre> = genresList.map { Genre(it.first, it.second) }
|
|
||||||
private fun getStatusFilters(): List<Pair<String, String>> = statusesList.map { it.first to it.second.toString() }
|
|
||||||
private fun getTypeFilters(): List<Pair<String, String>> = typesList.map { it.first to it.second.toString() }
|
|
||||||
|
|
||||||
private var genresList: List<Pair<String, Int>> = emptyList()
|
|
||||||
private var statusesList: List<Pair<String, Int>> = emptyList()
|
|
||||||
private var typesList: List<Pair<String, Int>> = emptyList()
|
|
||||||
|
|
||||||
private var fetchFiltersAttempts = 0
|
|
||||||
private var filtersState = FiltersState.NOT_FETCHED
|
|
||||||
|
|
||||||
private fun fetchFilters() {
|
|
||||||
if (filtersState != FiltersState.NOT_FETCHED || fetchFiltersAttempts >= 3) return
|
|
||||||
filtersState = FiltersState.FETCHING
|
|
||||||
fetchFiltersAttempts++
|
|
||||||
thread {
|
|
||||||
try {
|
|
||||||
val response = client.newCall(GET("$apiUrl/series/filters", headers)).execute()
|
|
||||||
val filters = json.decodeFromString<FiltersDto>(response.body.string())
|
|
||||||
|
|
||||||
genresList = filters.genres.filter { it.id > 0 }.map { it.name.trim() to it.id }
|
|
||||||
statusesList = filters.statuses.map { it.name.trim() to it.id }
|
|
||||||
typesList = filters.types.map { it.name.trim() to it.id }
|
|
||||||
|
|
||||||
filtersState = FiltersState.FETCHED
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
filtersState = FiltersState.NOT_FETCHED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
|
||||||
if (!preferences.dynamicUrl()) return super.mangaDetailsRequest(manga)
|
|
||||||
val match = OLD_FORMAT_MANGA_REGEX.find(manga.url)?.groupValues?.get(2)
|
|
||||||
val slug = match ?: manga.url.substringAfter("/series/").substringBefore("/")
|
|
||||||
val savedSlug = preferences.slugMap[slug] ?: "$slug-"
|
|
||||||
return GET("$baseUrl/series/$savedSlug", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
|
||||||
if (preferences.dynamicUrl()) {
|
|
||||||
val url = response.request.url.toString()
|
|
||||||
val newSlug = url.substringAfter("/series/").substringBefore("/")
|
|
||||||
val absSlug = newSlug.substringBeforeLast("-")
|
|
||||||
preferences.slugMap = preferences.slugMap.apply { put(absSlug, newSlug) }
|
|
||||||
}
|
|
||||||
return super.mangaDetailsParse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
|
||||||
title = document.selectFirst("span.text-xl.font-bold")!!.ownText()
|
|
||||||
thumbnail_url = document.selectFirst("img[alt=poster]")?.attr("abs:src")
|
|
||||||
description = document.selectFirst("span.font-medium.text-sm")?.text()
|
|
||||||
author = document.selectFirst("div.grid > div:has(h3:eq(0):containsOwn(Author)) > h3:eq(1)")?.ownText()
|
|
||||||
artist = document.selectFirst("div.grid > div:has(h3:eq(0):containsOwn(Artist)) > h3:eq(1)")?.ownText()
|
|
||||||
genre = document.select("div[class^=space] > div.flex > button.text-white").joinToString { it.ownText() }
|
|
||||||
status = parseStatus(document.selectFirst("div.flex:has(h3:eq(0):containsOwn(Status)) > h3:eq(1)")?.ownText())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(status: String?) = when (status) {
|
|
||||||
"Ongoing", "Season End" -> SManga.ONGOING
|
|
||||||
"Hiatus" -> SManga.ON_HIATUS
|
|
||||||
"Completed" -> SManga.COMPLETED
|
|
||||||
"Dropped" -> SManga.CANCELLED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
if (preferences.dynamicUrl()) {
|
|
||||||
val url = response.request.url.toString()
|
|
||||||
val newSlug = url.substringAfter("/series/").substringBefore("/")
|
|
||||||
val absSlug = newSlug.substringBeforeLast("-")
|
|
||||||
preferences.slugMap = preferences.slugMap.apply { put(absSlug, newSlug) }
|
|
||||||
}
|
|
||||||
return super.chapterListParse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "div.scrollbar-thumb-themecolor > a.block"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
|
||||||
setUrlWithoutDomain(element.attr("abs:href").toPermSlugIfNeeded())
|
|
||||||
name = element.selectFirst("h3:eq(0)")!!.text()
|
|
||||||
date_upload = try {
|
|
||||||
val text = element.selectFirst("h3:eq(1)")!!.ownText()
|
|
||||||
val cleanText = text.replace(CLEAN_DATE_REGEX, "$1")
|
|
||||||
dateFormat.parse(cleanText)?.time ?: 0
|
|
||||||
} catch (_: Exception) {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
if (!preferences.dynamicUrl()) return super.pageListRequest(chapter)
|
|
||||||
val match = OLD_FORMAT_CHAPTER_REGEX.containsMatchIn(chapter.url)
|
|
||||||
if (match) throw Exception("Please refresh the chapter list before reading.")
|
|
||||||
val slug = chapter.url.substringAfter("/series/").substringBefore("/")
|
|
||||||
val savedSlug = preferences.slugMap[slug] ?: "$slug-"
|
|
||||||
return GET(baseUrl + chapter.url.replace(slug, savedSlug), headers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip scriptPages
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
return document.select("div > img[alt=chapter]").mapIndexed { i, element ->
|
return document.select(pageSelector)
|
||||||
Page(i, imageUrl = element.attr("abs:src"))
|
.filterNot { it.attr("src").isNullOrEmpty() }
|
||||||
}
|
.mapIndexed { i, img -> Page(i, document.location(), img.attr("abs:src")) }
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
private enum class FiltersState { NOT_FETCHED, FETCHING, FETCHED }
|
|
||||||
|
|
||||||
private inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
|
|
||||||
filterIsInstance<R>().firstOrNull()
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = PREF_DYNAMIC_URL
|
|
||||||
title = "Automatically update dynamic URLs"
|
|
||||||
summary = "Automatically update random numbers in manga URLs.\nHelps mitigating HTTP 404 errors during update and \"in library\" marks when browsing.\nNote: This setting may require clearing database in advanced settings and migrating all manga to the same source."
|
|
||||||
setDefaultValue(true)
|
|
||||||
}.let(screen::addPreference)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var SharedPreferences.slugMap: MutableMap<String, String>
|
|
||||||
get() {
|
|
||||||
val jsonMap = getString(PREF_SLUG_MAP, "{}")!!
|
|
||||||
return try {
|
|
||||||
json.decodeFromString<Map<String, String>>(jsonMap).toMutableMap()
|
|
||||||
} catch (_: Exception) {
|
|
||||||
mutableMapOf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set(newSlugMap) {
|
|
||||||
edit()
|
|
||||||
.putString(PREF_SLUG_MAP, json.encodeToString(newSlugMap))
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun SharedPreferences.dynamicUrl(): Boolean = getBoolean(PREF_DYNAMIC_URL, true)
|
|
||||||
|
|
||||||
private fun String.toPermSlugIfNeeded(): String {
|
|
||||||
if (!preferences.dynamicUrl()) return this
|
|
||||||
val slug = this.substringAfter("/series/").substringBefore("/")
|
|
||||||
val absSlug = slug.substringBeforeLast("-")
|
|
||||||
preferences.slugMap = preferences.slugMap.apply { put(absSlug, slug) }
|
|
||||||
return this.replace(slug, absSlug)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val CLEAN_DATE_REGEX = """(\d+)(st|nd|rd|th)""".toRegex()
|
|
||||||
private val OLD_FORMAT_MANGA_REGEX = """^/manga/(\d+-)?([^/]+)/?$""".toRegex()
|
|
||||||
private val OLD_FORMAT_CHAPTER_REGEX = """^/(\d+-)?[^/]*-chapter-\d+(-\d+)*/?$""".toRegex()
|
|
||||||
private const val PREF_SLUG_MAP = "pref_slug_map"
|
|
||||||
private const val PREF_DYNAMIC_URL = "pref_dynamic_url"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.asurascans
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class FiltersDto(
|
|
||||||
val genres: List<FilterItemDto>,
|
|
||||||
val statuses: List<FilterItemDto>,
|
|
||||||
val types: List<FilterItemDto>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class FilterItemDto(
|
|
||||||
val id: Int,
|
|
||||||
val name: String,
|
|
||||||
)
|
|
|
@ -1,17 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.asurascans
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
|
|
||||||
class Genre(title: String, val id: Int) : Filter.CheckBox(title)
|
|
||||||
class GenreFilter(title: String, genres: List<Genre>) : Filter.Group<Genre>(title, genres)
|
|
||||||
|
|
||||||
class StatusFilter(title: String, statuses: List<Pair<String, String>>) : UriPartFilter(title, statuses)
|
|
||||||
|
|
||||||
class TypeFilter(title: String, types: List<Pair<String, String>>) : UriPartFilter(title, types)
|
|
||||||
|
|
||||||
class OrderFilter(title: String, orders: List<Pair<String, String>>) : UriPartFilter(title, orders)
|
|
||||||
|
|
||||||
open class UriPartFilter(displayName: String, val vals: List<Pair<String, String>>) :
|
|
||||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
|
||||||
fun toUriPart() = vals[state].second
|
|
||||||
}
|
|
|
@ -1,10 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Fury Manga'
|
extName = 'Blazescans'
|
||||||
extClass = '.FuryManga'
|
extClass = '.Blazescans'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://furymanga.com'
|
baseUrl = 'https://blazetoon.com'
|
||||||
overrideVersionCode = 2
|
overrideVersionCode = 1
|
||||||
isNsfw = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 19 KiB |
|
@ -4,14 +4,7 @@ import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class FuryManga : MangaThemesia(
|
class Blazescans : MangaThemesia("Blazescans", "https://blazetoon.com", "en") {
|
||||||
"Fury Manga",
|
|
||||||
"https://furymanga.com",
|
|
||||||
"en",
|
|
||||||
"/comics",
|
|
||||||
) {
|
|
||||||
override val id = 3912200442923601567
|
|
||||||
|
|
||||||
override val client = super.client.newBuilder()
|
override val client = super.client.newBuilder()
|
||||||
.rateLimit(1, 2, TimeUnit.SECONDS)
|
.rateLimit(1, 2, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'ComicExtra'
|
extName = 'ComicExtra'
|
||||||
extClass = '.ComicExtra'
|
extClass = '.ComicExtra'
|
||||||
extVersionCode = 16
|
extVersionCode = 15
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -23,7 +23,7 @@ class ComicExtra : ParsedHttpSource() {
|
||||||
|
|
||||||
override val name = "ComicExtra"
|
override val name = "ComicExtra"
|
||||||
|
|
||||||
override val baseUrl = "https://comixextra.com"
|
override val baseUrl = "https://comicextra.org"
|
||||||
|
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'Darths & Droids'
|
|
||||||
extClass = '.DarthsDroids'
|
|
||||||
extVersionCode = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 51 KiB |
|
@ -1,247 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.darthsdroids
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
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.model.UpdateStrategy
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import rx.Observable
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
// Dear Darths & Droids creators:
|
|
||||||
// I’m sorry if this extension causes too much traffic for your site.
|
|
||||||
// Unfortunately we can’t just download and use your Zip downloads.
|
|
||||||
// Shall problems arise, we’ll reduce the rate limit.
|
|
||||||
class DarthsDroids : HttpSource() {
|
|
||||||
override val name = "Darths & Droids"
|
|
||||||
override val baseUrl = "https://www.darthsanddroids.net"
|
|
||||||
override val lang = "en"
|
|
||||||
override val supportsLatest = false
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
|
||||||
.rateLimitHost(baseUrl.toHttpUrl(), 10, 1, TimeUnit.SECONDS)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
// Picks a thumbnail from the profile pictures of the »cast« pages:
|
|
||||||
// https://www.darthsanddroids.net/cast/
|
|
||||||
//
|
|
||||||
// Where possible, pick a thumbnail from the corresponding book’s
|
|
||||||
// cast page. Try to avoid having a character appear more than once
|
|
||||||
// as thumbnail, giving all main characters equal amounts of spotlight.
|
|
||||||
// Pick a character people would intuïtively associate with the
|
|
||||||
// corresponding film, like Qui-Gon for Phantom Menace or Leia for
|
|
||||||
// A New Hope.
|
|
||||||
//
|
|
||||||
// If a book doesn’t have its own cast page, try source a fitting
|
|
||||||
// profile picture from a different page. Avoid sourcing thumbnails
|
|
||||||
// from a different website.
|
|
||||||
private fun dndThumbnailUrlForTitle(nthManga: Int): String = when (nthManga) {
|
|
||||||
// The numbers are assigned in order of appearance of a book on the archive page.
|
|
||||||
0 -> "$baseUrl/cast/QuiGon.jpg" // D&D1
|
|
||||||
1 -> "$baseUrl/cast/Anakin2.jpg" // D&D2
|
|
||||||
2 -> "$baseUrl/cast/ObiWan3.jpg" // D&D3
|
|
||||||
3 -> "$baseUrl/cast/JarJar2.jpg" // JJ
|
|
||||||
4 -> "$baseUrl/cast/Leia4.jpg" // D&D4
|
|
||||||
5 -> "$baseUrl/cast/Han5.jpg" // D&D5
|
|
||||||
6 -> "$baseUrl/cast/Luke6.jpg" // D&D6
|
|
||||||
7 -> "$baseUrl/cast/Cassian.jpg" // R1
|
|
||||||
8 -> "$baseUrl/cast/C3PO4.jpg" // Muppets
|
|
||||||
9 -> "$baseUrl/cast/Finn7.jpg" // D&D7
|
|
||||||
10 -> "$baseUrl/cast/Han4.jpg" // Solo
|
|
||||||
11 -> "$baseUrl/cast/Hux8.jpg" // D&D8
|
|
||||||
// Just some nonsense fallback that screams »Star Wars« but is also so recognisably
|
|
||||||
// OT that one can understand it’s a mere fallback. Better thumbnails require an
|
|
||||||
// extension update.
|
|
||||||
else -> "$baseUrl/cast/Vader4.jpg"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun dndManga(archiveUrl: String, mangaTitle: String, mangaStatus: Int, nthManga: Int): SManga = SManga.create().apply {
|
|
||||||
setUrlWithoutDomain(archiveUrl)
|
|
||||||
thumbnail_url = dndThumbnailUrlForTitle(nthManga)
|
|
||||||
title = mangaTitle
|
|
||||||
author = "David Morgan-Mar & Co."
|
|
||||||
artist = "David Morgan-Mar & Co."
|
|
||||||
description = """What if Star Wars as we know it didn't exist, but instead the
|
|
||||||
|plot of the movies was being made up on the spot by players of
|
|
||||||
|a Tabletop Game?
|
|
||||||
|
|
|
||||||
|Well, for one, the results might actually make a lot more sense,
|
|
||||||
|from an out-of-story point of view…
|
|
||||||
""".trimMargin()
|
|
||||||
genre = "Campaign Comic, Comedy, Space Opera, Science Fiction"
|
|
||||||
status = mangaStatus
|
|
||||||
update_strategy = when (mangaStatus) {
|
|
||||||
SManga.COMPLETED -> UpdateStrategy.ONLY_FETCH_ONCE
|
|
||||||
else -> UpdateStrategy.ALWAYS_UPDATE
|
|
||||||
}
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request =
|
|
||||||
GET("$baseUrl/archive.html", headers)
|
|
||||||
|
|
||||||
// The book and page archive feeds are rather special for this webcomic.
|
|
||||||
// The main archive page `/archive.html` is a combined feed for both,
|
|
||||||
// all previous and finished books, as well as all pages of the book that
|
|
||||||
// is currently releasing. Every finished book gets its own archive page
|
|
||||||
// like `/archive4.html` or `/archiveJJ.html` into which all page links
|
|
||||||
// are moved. So whatever book is currently releasing in `/archive.html`
|
|
||||||
// will eventually be moved into its own archive, and it’ll instead
|
|
||||||
// appear as a book-archive link in `/archive.html`.
|
|
||||||
//
|
|
||||||
// This means a few things:
|
|
||||||
// • The currently releasing book eventually changes its `url`!
|
|
||||||
// • The URL of the currently releasing book will be taken over by
|
|
||||||
// whichever new book comes next.
|
|
||||||
// • There is no deterministic way of guessing a book’s future
|
|
||||||
// archive name.
|
|
||||||
// ◦ This is especially apparent with the »Solo« book, which’s
|
|
||||||
// archive page is `/solo/`, while all others are `/archiveX.html`.
|
|
||||||
//
|
|
||||||
// So eventually, Tachiyomi & Co. will glitch out once a currently
|
|
||||||
// releasing book finishes. People will find the current book’s page
|
|
||||||
// feed to be empty. Even worse, they may find it starting anew with
|
|
||||||
// different pages. A manual refresh *should* change the book’s `url`
|
|
||||||
// to its new archive page, and all reading progress should be preserved.
|
|
||||||
// Then the user will have to manually add the new book to their library.
|
|
||||||
//
|
|
||||||
// The alternative would be to have a pseudo book »<Title> (ongoing)«
|
|
||||||
// that just disappears, being replaced by »<Title>«. But i think that’s
|
|
||||||
// even worse in terms of user experience. Maybe one day we’ll have new
|
|
||||||
// extension APIs for dealing with unique webcomic weirdnesses. ’cause
|
|
||||||
// trust me, there’s worse.
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
|
||||||
val mainArchive = response.asJsoup()
|
|
||||||
val archiveData = mainArchive.select("div.text > table.text > tbody > tr")
|
|
||||||
|
|
||||||
val mangas = mutableListOf<SManga>()
|
|
||||||
var nextMangaTitle = name
|
|
||||||
var nthManga = 0
|
|
||||||
|
|
||||||
run stop@{
|
|
||||||
archiveData.forEach {
|
|
||||||
val maybeTitle = it.selectFirst("th")?.text()
|
|
||||||
if (maybeTitle != null) {
|
|
||||||
nextMangaTitle = "$name $maybeTitle"
|
|
||||||
} else {
|
|
||||||
val maybeArchive = it.selectFirst("""td[colspan="3"] > a""")?.absUrl("href")
|
|
||||||
if (maybeArchive != null) {
|
|
||||||
mangas.add(dndManga(maybeArchive, nextMangaTitle, SManga.COMPLETED, nthManga++))
|
|
||||||
} else {
|
|
||||||
// We reached the end, assuming the page layout stays consistent beyond D&D8.
|
|
||||||
// Thus, we append our final manga with this current page as its archive.
|
|
||||||
// Unfortunately this means we will needlessly fetch this page twice.
|
|
||||||
mangas.add(dndManga("/archive.html", nextMangaTitle, SManga.ONGOING, nthManga))
|
|
||||||
return@stop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MangasPage(mangas, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not efficient, but the simplest way for me to refresh.
|
|
||||||
// We also can’t really use the `mangaDetailsRequest + mangaDetailsParse`
|
|
||||||
// approach, for we actually expect one of the books’ `url`s to change.
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
|
||||||
fetchPopularManga(0)
|
|
||||||
.map { mangasPage ->
|
|
||||||
mangasPage
|
|
||||||
.mangas
|
|
||||||
// Do not test for URL-equality, for the last book will always
|
|
||||||
// eventually migrate its archive page from `/archive.html` to
|
|
||||||
// its own page.
|
|
||||||
.first { it.title == manga.title }
|
|
||||||
}
|
|
||||||
|
|
||||||
// This implementation here is needlessly complicated, for it has to automatically detect
|
|
||||||
// whether we’re in a date-annotated archive, the main archive, or a dateless archive.
|
|
||||||
// All three are largely similar, there are just *some* (annoying) differences we have to
|
|
||||||
// deal with.
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val archivePages = response.asJsoup()
|
|
||||||
|
|
||||||
// For books where all pages released the same day, there is no page date column,
|
|
||||||
// so instead we grab the release date of the archive page itself from its footer.
|
|
||||||
val pageDate = archivePages
|
|
||||||
.select("""br + i""")
|
|
||||||
.mapNotNull { EXTR_PAGE_DATE.find(it.text())?.groupValues?.getOrNull(1) }
|
|
||||||
.map { PAGE_DATE_FMT.parse(it)?.time }
|
|
||||||
.firstOrNull()
|
|
||||||
?: 0L
|
|
||||||
var i = 0
|
|
||||||
|
|
||||||
return archivePages
|
|
||||||
.select("""div.text > table.text > tbody > tr""")
|
|
||||||
.mapNotNull {
|
|
||||||
val pageData = it.select("""td""")
|
|
||||||
var pageAnchor = pageData.getOrNull(2)?.selectFirst("a")
|
|
||||||
// null for »Intermission«, main archive, dateless archive,…
|
|
||||||
if (pageAnchor != null) {
|
|
||||||
SChapter.create().apply {
|
|
||||||
name = pageAnchor!!.text()
|
|
||||||
chapter_number = (i++).toFloat()
|
|
||||||
date_upload = runCatching {
|
|
||||||
DATE_FMT.parse(pageData[0].text())!!.time
|
|
||||||
}.getOrDefault(0L)
|
|
||||||
setUrlWithoutDomain(pageAnchor!!.absUrl("href"))
|
|
||||||
}
|
|
||||||
} else if (!pageData.hasAttr("colspan")) {
|
|
||||||
// Are we in a dateless archive?
|
|
||||||
pageAnchor = pageData.getOrNull(0)?.selectFirst("a")
|
|
||||||
if (pageAnchor != null) {
|
|
||||||
SChapter.create().apply {
|
|
||||||
name = pageAnchor.text()
|
|
||||||
chapter_number = (i++).toFloat()
|
|
||||||
date_upload = pageDate
|
|
||||||
setUrlWithoutDomain(pageAnchor.absUrl("href"))
|
|
||||||
}
|
|
||||||
} else { null }
|
|
||||||
} else { null }
|
|
||||||
}
|
|
||||||
.reversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> =
|
|
||||||
// Careful. For almost all images it’s `div.center>p>img`, except for pages released on
|
|
||||||
// April’s Fools day, when it’s `div.center>p>a>img`. We could still add the `p` in
|
|
||||||
// between, but it was decided to leave it out, in case yet another *almost* same
|
|
||||||
// page layout pops up in the future.
|
|
||||||
//
|
|
||||||
// For example, this episode was released during April’s Fools day.
|
|
||||||
// https://www.darthsanddroids.net/episodes/0082.html
|
|
||||||
response
|
|
||||||
.asJsoup()
|
|
||||||
.select("""div.center img""")
|
|
||||||
.mapIndexed { i, img ->
|
|
||||||
Page(
|
|
||||||
index = i,
|
|
||||||
imageUrl = img.absUrl("src"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = throw UnsupportedOperationException()
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException()
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
|
||||||
override fun imageUrlRequest(page: Page): Request = throw UnsupportedOperationException()
|
|
||||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val DATE_FMT = SimpleDateFormat("EEE d MMM, yyyy", Locale.US)
|
|
||||||
private val EXTR_PAGE_DATE = """Published\:\s+(\w+,\s+\d+\s+\w+,\s+\d+\;\s+\d+\:\d+\:\d+\s+\w+)""".toRegex()
|
|
||||||
private val PAGE_DATE_FMT = SimpleDateFormat("EEEEE, d MMMMM, yyyy; HH:mm:ss zzz", Locale.US)
|
|
||||||
}
|
|
||||||
}
|
|