Compare commits
No commits in common. "ac57f5e3dde6197b96a3ae66e311c3dfeac4ec1d" and "01e67374ec95300fa6993c1df8302da0b3f4682a" have entirely different histories.
ac57f5e3dd
...
01e67374ec
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 4
|
||||
baseVersionCode = 3
|
||||
|
|
|
@ -130,8 +130,8 @@ abstract class GravureBlogger(
|
|||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
return document.select("div.post-body a:has(> img)").mapIndexed { i, it ->
|
||||
Page(i, imageUrl = it.absUrl("href"))
|
||||
return document.select("div.post-body img").mapIndexed { i, it ->
|
||||
Page(i, imageUrl = it.absUrl("src"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 25
|
||||
baseVersionCode = 24
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
|
|
@ -225,13 +225,13 @@ abstract class HeanCms(
|
|||
throw Exception(intl.format("url_changed_error", name, name))
|
||||
}
|
||||
|
||||
val seriesSlug = manga.url.substringAfterLast("/").substringBefore("#")
|
||||
val seriesId = manga.url.substringAfterLast("#")
|
||||
|
||||
val apiHeaders = headersBuilder()
|
||||
.add("Accept", ACCEPT_JSON)
|
||||
.build()
|
||||
|
||||
return GET("$apiUrl/series/$seriesSlug", apiHeaders)
|
||||
return GET("$apiUrl/series/id/$seriesId", apiHeaders)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
baseVersionCode = 30
|
||||
baseVersionCode = 28
|
||||
|
|
|
@ -188,11 +188,7 @@ abstract class LibGroup(
|
|||
} else {
|
||||
it.replace("\\", "")
|
||||
}
|
||||
str.parseAs<AuthToken>().let { auth ->
|
||||
if (auth.isValid()) {
|
||||
returnValue = auth
|
||||
}
|
||||
}
|
||||
returnValue = str.parseAs<AuthToken>()
|
||||
}
|
||||
latch.countDown()
|
||||
}
|
||||
|
@ -328,7 +324,7 @@ abstract class LibGroup(
|
|||
if (currentBranch.value.branchId == defaultBranchId && sortingList == "ms_mixing") { // ms_mixing with default branch from api
|
||||
chapters.add(it.value.toSChapter(slugUrl, defaultBranchId, isScanUser()))
|
||||
} 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()))
|
||||
}
|
||||
} else if (sortingList == "ms_combining") { // ms_combining
|
||||
|
|
|
@ -202,6 +202,7 @@ class Chapter(
|
|||
val name: String?,
|
||||
val number: String,
|
||||
val volume: String,
|
||||
@SerialName("item_number") val itemNumber: Float?,
|
||||
) {
|
||||
@Serializable
|
||||
class Branch(
|
||||
|
@ -240,7 +241,7 @@ class Chapter(
|
|||
url = "/$slugUrl/chapter?$branchStr&volume=$volume&number=$number"
|
||||
scanlator = getTeamName(branchId) ?: if (isScanUser) getUserName(branchId) else null
|
||||
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
|
||||
class AuthToken(
|
||||
private val auth: Auth?,
|
||||
private val token: Token?,
|
||||
private val auth: Auth,
|
||||
private val token: Token,
|
||||
) {
|
||||
@Serializable
|
||||
class Auth(
|
||||
|
@ -282,15 +283,13 @@ class AuthToken(
|
|||
@SerialName("access_token") val accessToken: String,
|
||||
)
|
||||
|
||||
fun isValid(): Boolean = auth != null && token != null
|
||||
|
||||
fun isExpired(): Boolean {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val expiresIn = token!!.timestamp + (token.expiresIn * 1000)
|
||||
val expiresIn = token.timestamp + (token.expiresIn * 1000)
|
||||
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.asObservableSuccess
|
||||
|
@ -10,10 +10,8 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
|
@ -21,13 +19,11 @@ import org.jsoup.nodes.Element
|
|||
import org.jsoup.select.Elements
|
||||
import rx.Observable
|
||||
|
||||
class ReadAllComics : ParsedHttpSource() {
|
||||
|
||||
override val name = "ReadAllComics"
|
||||
|
||||
override val baseUrl = "https://readallcomics.com"
|
||||
|
||||
override val lang = "en"
|
||||
abstract class ReadAllComics(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
|
@ -38,7 +34,7 @@ class ReadAllComics : ParsedHttpSource() {
|
|||
.rateLimit(2)
|
||||
.build()
|
||||
|
||||
private fun archivedCategoryInterceptor(chain: Interceptor.Chain): Response {
|
||||
protected open fun archivedCategoryInterceptor(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
|
||||
|
@ -47,7 +43,7 @@ class ReadAllComics : ParsedHttpSource() {
|
|||
request.url.toString(),
|
||||
)
|
||||
|
||||
val newUrl = document.selectFirst(".description-archive > p > span > a")
|
||||
val newUrl = document.selectFirst(archivedCategorySelector())
|
||||
?.attr("href")?.toHttpUrlOrNull()
|
||||
?: return response
|
||||
|
||||
|
@ -64,18 +60,33 @@ class ReadAllComics : ParsedHttpSource() {
|
|||
return response
|
||||
}
|
||||
|
||||
protected open fun archivedCategorySelector() = ".description-archive > p > span > a"
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
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 category = element.classNames()
|
||||
.firstOrNull { it.startsWith("category-") }!!
|
||||
.substringAfter("category-")
|
||||
.firstOrNull { it.startsWith("category-") }
|
||||
?.substringAfter("category-")
|
||||
?: return null
|
||||
|
||||
url = "/category/$category/"
|
||||
title = category.replace("-", " ").capitalizeEachWord()
|
||||
thumbnail_url = element.select("img").attr("abs:src")
|
||||
title = element.select(popularMangaTitleSelector()).text()
|
||||
thumbnail_url = element.select(popularMangaThumbnailSelector()).attr("abs:src")
|
||||
}
|
||||
|
||||
return manga
|
||||
|
@ -83,6 +94,8 @@ class ReadAllComics : ParsedHttpSource() {
|
|||
|
||||
override fun popularMangaSelector() = "#post-area > div"
|
||||
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> {
|
||||
return if (page == 1) {
|
||||
|
@ -94,15 +107,10 @@ class ReadAllComics : ParsedHttpSource() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("story", query)
|
||||
addQueryParameter("s", "")
|
||||
addQueryParameter("type", "comic")
|
||||
}.build()
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||
GET("$baseUrl/?story=${query.trim()}&s=&type=${searchType()}", headers)
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
protected open fun searchType() = "comic"
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
searchPageElements = response.asJsoup().select(searchMangaSelector())
|
||||
|
@ -125,32 +133,27 @@ class ReadAllComics : ParsedHttpSource() {
|
|||
|
||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
title = element.text()
|
||||
thumbnail_url = "https://fakeimg.pl/200x300/?text=No%20Cover%0AOn%20Search&font_size=62"
|
||||
title = element.text().trim()
|
||||
thumbnail_url = searchCover
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = ".categories a"
|
||||
override fun searchMangaNextPageSelector() = null
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
title = document.selectFirst("h1")!!.text()
|
||||
genre = document.select("p strong").joinToString { it.text() }
|
||||
author = document.select("p > strong").last()?.text()
|
||||
description = buildString {
|
||||
document.select(".b > strong").forEach { element ->
|
||||
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")
|
||||
title = document.select(mangaDetailsTitleSelector()).text().trim()
|
||||
genre = document.select(mangaDetailsGenreSelector()).joinToString { it.text().trim() }
|
||||
author = document.select(mangaDetailsAuthorSelector()).last()?.text()?.trim()
|
||||
description = document.select(mangaDetailsDescriptionSelector()).text().trim()
|
||||
thumbnail_url = document.select(mangaDetailsThumbnailSelector()).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 chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
|
@ -159,25 +162,15 @@ class ReadAllComics : ParsedHttpSource() {
|
|||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.capitalizeEachWord(): String {
|
||||
val result = StringBuilder(length)
|
||||
var capitalize = true
|
||||
for (char in this) {
|
||||
result.append(
|
||||
if (capitalize) {
|
||||
char.uppercase()
|
||||
} else {
|
||||
char.lowercase()
|
||||
},
|
||||
)
|
||||
capitalize = char.isWhitespace()
|
||||
}
|
||||
return result.toString()
|
||||
protected open fun pageListSelector() = "body > div img"
|
||||
|
||||
companion object {
|
||||
private const val searchCover = "https://fakeimg.pl/200x300/?text=No%20Cover%0AOn%20Search&font_size=62"
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) =
|
||||
|
@ -190,4 +183,6 @@ class ReadAllComics : ParsedHttpSource() {
|
|||
throw UnsupportedOperationException()
|
||||
override fun latestUpdatesNextPageSelector() =
|
||||
throw UnsupportedOperationException()
|
||||
override fun popularMangaFromElement(element: Element) =
|
||||
throw UnsupportedOperationException()
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
ext {
|
||||
extName = 'Galaxy'
|
||||
extClass = '.GalaxyFactory'
|
||||
extVersionCode = 4
|
||||
isNsfw = false
|
||||
extVersionCode = 2
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
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 uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class GalaxyFactory : SourceFactory {
|
||||
|
||||
|
@ -15,53 +8,8 @@ class GalaxyFactory : SourceFactory {
|
|||
override val id = 2602904659965278831
|
||||
}
|
||||
|
||||
class GalaxyManga :
|
||||
Galaxy("Galaxy Manga", "https://galaxymanga.net", "ar"),
|
||||
ConfigurableSource {
|
||||
class GalaxyManga : Galaxy("Galaxy Manga", "https://ayoub-zrr.xyz", "ar") {
|
||||
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(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Hitomi'
|
||||
extClass = '.HitomiFactory'
|
||||
extVersionCode = 32
|
||||
extVersionCode = 31
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
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.await
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
|
@ -24,14 +19,9 @@ import kotlinx.serialization.decodeFromString
|
|||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Call
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
@ -40,14 +30,13 @@ import java.text.ParseException
|
|||
import java.text.SimpleDateFormat
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
class Hitomi(
|
||||
override val lang: String,
|
||||
private val nozomiLang: String,
|
||||
) : HttpSource(), ConfigurableSource {
|
||||
) : HttpSource() {
|
||||
|
||||
override val name = "Hitomi"
|
||||
|
||||
|
@ -61,14 +50,7 @@ class Hitomi(
|
|||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.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 val client = network.cloudflareClient
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.set("referer", "$baseUrl/")
|
||||
|
@ -506,7 +488,7 @@ class Hitomi(
|
|||
private suspend fun Gallery.toSManga() = SManga.create().apply {
|
||||
title = this@toSManga.title
|
||||
url = galleryurl
|
||||
author = groups?.joinToString { it.formatted } ?: artists?.joinToString { it.formatted }
|
||||
author = groups?.joinToString { it.formatted }
|
||||
artist = artists?.joinToString { it.formatted }
|
||||
genre = tags?.joinToString { it.formatted }
|
||||
thumbnail_url = files.first().let {
|
||||
|
@ -585,25 +567,14 @@ class Hitomi(
|
|||
|
||||
gallery.files.mapIndexed { idx, img ->
|
||||
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 imageId = imageIdFromHash(hash)
|
||||
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(
|
||||
idx,
|
||||
"$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")
|
||||
}
|
||||
|
||||
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 popularMangaRequest(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 searchMangaParse(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
|
||||
class ImageFile(
|
||||
val hash: String,
|
||||
val haswebp: Int,
|
||||
val hasavif: Int,
|
||||
val hasjxl: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".all.ninenineninehentai.AnimeHUrlActivity"
|
||||
android:name=".all.ninenineninehentai.NineNineNineHentaiUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
|
@ -12,7 +12,7 @@
|
|||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="animeh.to"/>
|
||||
<data android:host="999hentai.net"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:pathPattern="/hchapter/..*"/>
|
||||
</intent-filter>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeH'
|
||||
extClass = '.AnimeHFactory'
|
||||
extVersionCode = 7
|
||||
extName = '999Hentai'
|
||||
extClass = '.NineNineNineHentaiFactory'
|
||||
extVersionCode = 6
|
||||
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.util.Locale
|
||||
|
||||
open class AnimeH(
|
||||
open class NineNineNineHentai(
|
||||
final override val lang: String,
|
||||
private val siteLang: String = lang,
|
||||
) : 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
|
||||
|
|
@ -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 kotlin.system.exitProcess
|
||||
|
||||
class AnimeHUrlActivity : Activity() {
|
||||
class NineNineNineHentaiUrlActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
|
@ -15,17 +15,17 @@ class AnimeHUrlActivity : Activity() {
|
|||
val id = pathSegments[1]
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "${AnimeH.SEARCH_PREFIX}$id")
|
||||
putExtra("query", "${NineNineNineHentai.SEARCH_PREFIX}$id")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("AnimeHUrlActivity", e.toString())
|
||||
Log.e("999HentaiUrlActivity", e.toString())
|
||||
}
|
||||
} else {
|
||||
Log.e("AnimeHUrlActivity", "could not parse uri from intent $intent")
|
||||
Log.e("999HentaiUrlActivity", "could not parse uri from intent $intent")
|
||||
}
|
||||
|
||||
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 {
|
||||
extName = 'PandaChaika'
|
||||
extClass = '.PandaChaikaFactory'
|
||||
extVersionCode = 2
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package eu.kanade.tachiyomi.extension.all.pandachaika
|
||||
|
||||
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.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
|
@ -44,9 +42,6 @@ class PandaChaika(
|
|||
|
||||
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
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
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 {
|
||||
val library = response.parseAs<ArchiveResponse>()
|
||||
|
||||
|
@ -325,11 +250,4 @@ class PandaChaika(
|
|||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
override fun pageListParse(response: Response): List<Page> = 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
|
||||
|
||||
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:") } }
|
||||
.joinToString {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}.takeIf { it.isNotBlank() }
|
||||
}
|
||||
}
|
||||
fun getReadableSize(bytes: Double): String {
|
||||
return when {
|
||||
bytes >= 300 * 1000 * 1000 -> "${"%.2f".format(bytes / (1000.0 * 1000.0 * 1000.0))} GB"
|
||||
bytes >= 100 * 1000 -> "${"%.2f".format(bytes / (1000.0 * 1000.0))} MB"
|
||||
bytes >= 1000 -> "${"%.2f".format(bytes / (1000.0))} kB"
|
||||
bytes >= 300 * 1024 * 1024 -> "${"%.2f".format(bytes / (1024.0 * 1024.0 * 1024.0))} GB"
|
||||
bytes >= 100 * 1024 -> "${"%.2f".format(bytes / (1024.0 * 1024.0))} MB"
|
||||
bytes >= 1024 -> "${"%.2f".format(bytes / (1024.0))} KB"
|
||||
else -> "$bytes B"
|
||||
}
|
||||
}
|
||||
|
@ -31,14 +31,13 @@ fun getReadableSize(bytes: Double): String {
|
|||
class Archive(
|
||||
val download: String,
|
||||
val posted: Long,
|
||||
val title: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class LongArchive(
|
||||
private val thumbnail: String,
|
||||
private val title: String,
|
||||
val id: Int,
|
||||
private val id: Int,
|
||||
private val posted: Long?,
|
||||
private val public_date: Long?,
|
||||
private val filecount: Int,
|
||||
|
@ -51,47 +50,35 @@ class LongArchive(
|
|||
val groups = filterTags("group", tags = tags)
|
||||
val artists = filterTags("artist", tags = tags)
|
||||
val publishers = filterTags("publisher", tags = tags)
|
||||
val characters = filterTags("character", tags = tags)
|
||||
val male = filterTags("male", tags = tags)
|
||||
val female = filterTags("female", tags = tags)
|
||||
val others = filterTags(exclude = listOf("female", "male", "artist", "publisher", "group", "parody"), tags = tags)
|
||||
val parodies = filterTags("parody", tags = tags)
|
||||
var appended = false
|
||||
|
||||
url = id.toString()
|
||||
title = this@LongArchive.title
|
||||
thumbnail_url = thumbnail
|
||||
author = groups ?: artists
|
||||
author = groups.ifEmpty { artists }
|
||||
artist = artists
|
||||
genre = listOf(male, female, others).joinToString()
|
||||
description = buildString {
|
||||
append("Uploader: ", uploader.ifEmpty { "Anonymous" }, "\n")
|
||||
publishers?.let {
|
||||
append("Publishers: ", it, "\n")
|
||||
publishers.takeIf { it.isNotBlank() }?.let {
|
||||
append("Publishers: ", it, "\n\n")
|
||||
}
|
||||
append("\n")
|
||||
|
||||
parodies?.let {
|
||||
append("Parodies: ", it, "\n")
|
||||
appended = true
|
||||
parodies.takeIf { it.isNotBlank() }?.let {
|
||||
append("Parodies: ", it, "\n\n")
|
||||
}
|
||||
characters?.let {
|
||||
append("Characters: ", it, "\n")
|
||||
appended = true
|
||||
}
|
||||
if (appended) append("\n")
|
||||
|
||||
male?.let {
|
||||
male.takeIf { it.isNotBlank() }?.let {
|
||||
append("Male tags: ", it, "\n\n")
|
||||
}
|
||||
female?.let {
|
||||
female.takeIf { it.isNotBlank() }?.let {
|
||||
append("Female tags: ", it, "\n\n")
|
||||
}
|
||||
others?.let {
|
||||
others.takeIf { it.isNotBlank() }?.let {
|
||||
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("File Size: ", getReadableSize(filesize), "\n")
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ fun getFilters(): FilterList {
|
|||
TextFilter("Female Tags", "female"),
|
||||
TextFilter("Artists", "artist"),
|
||||
TextFilter("Parodies", "parody"),
|
||||
TextFilter("Characters", "character"),
|
||||
Filter.Separator(),
|
||||
TextFilter("Reason", "reason"),
|
||||
TextFilter("Uploader", "reason"),
|
||||
|
@ -53,11 +52,11 @@ private val getTypes = listOf(
|
|||
|
||||
private val getSortsList: List<Pair<String, String>> = listOf(
|
||||
Pair("Public Date", "public_date"),
|
||||
Pair("Posted Date", "posted"),
|
||||
Pair("Posted Date", "posted_date"),
|
||||
Pair("Title", "title"),
|
||||
Pair("Japanese Title", "title_jpn"),
|
||||
Pair("Rating", "rating"),
|
||||
Pair("Images", "filecount"),
|
||||
Pair("File Size", "filesize"),
|
||||
Pair("Images", "images"),
|
||||
Pair("File Size", "size"),
|
||||
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 {
|
||||
extName = 'Union Mangas'
|
||||
extClass = '.UnionMangasFactory'
|
||||
extVersionCode = 6
|
||||
extVersionCode = 5
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -203,7 +203,7 @@ class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
|
|||
|
||||
companion object {
|
||||
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 dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
ext {
|
||||
extName = 'Hadess'
|
||||
extClass = '.Hadess'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://www.hadess.xyz'
|
||||
extName = 'Crow Scans'
|
||||
extClass = '.CrowScans'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://crowscans.com'
|
||||
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'
|
||||
extClass = '.MangaNoon'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://manjanoon.co'
|
||||
overrideVersionCode = 3
|
||||
isNsfw = false
|
||||
baseUrl = 'https://manjanoon.org'
|
||||
overrideVersionCode = 2
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,89 +1,12 @@
|
|||
package eu.kanade.tachiyomi.extension.ar.manganoon
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import org.jsoup.nodes.Element
|
||||
import java.util.Calendar
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class MangaNoon : MangaThemesia(
|
||||
"مانجا نون",
|
||||
"https://manjanoon.co",
|
||||
"https://manjanoon.org",
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
dateFormat = SimpleDateFormat("MMM d, yyy", Locale("ar")),
|
||||
)
|
||||
|
|
|
@ -2,8 +2,8 @@ ext {
|
|||
extName = 'MangaSwat'
|
||||
extClass = '.MangaSwat'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://maxlevelteam.com'
|
||||
overrideVersionCode = 20
|
||||
baseUrl = 'https://t1manga.com'
|
||||
overrideVersionCode = 19
|
||||
}
|
||||
|
||||
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 :
|
||||
MangaThemesia(
|
||||
"MangaSwat",
|
||||
"https://maxlevelteam.com",
|
||||
"https://t1manga.com",
|
||||
"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 {
|
||||
extName = 'Vortex Scans'
|
||||
extClass = '.VortexScans'
|
||||
themePkg = 'iken'
|
||||
overrideVersionCode = 33
|
||||
extVersionCode = 33
|
||||
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.SManga
|
||||
|
@ -18,7 +18,7 @@ class SearchResponse(
|
|||
|
||||
@Serializable
|
||||
class Manga(
|
||||
private val id: Int,
|
||||
val id: Int,
|
||||
val slug: String,
|
||||
private val postTitle: String,
|
||||
private val postContent: String? = null,
|
||||
|
@ -29,7 +29,7 @@ class Manga(
|
|||
private val artist: String? = null,
|
||||
private val seriesType: 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 {
|
||||
url = "$slug#$id"
|
||||
|
@ -70,16 +70,10 @@ class Manga(
|
|||
"MANHWA" -> add("Manhwa")
|
||||
else -> {}
|
||||
}
|
||||
genres.forEach { add(it.name) }
|
||||
genres?.forEach { add(it.name) }
|
||||
}.distinct().joinToString()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class Genre(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
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 okhttp3.HttpUrl
|
||||
|
@ -65,8 +65,37 @@ class TypeFilter : SelectFilter(
|
|||
),
|
||||
)
|
||||
|
||||
class GenreFilter(genres: List<Pair<String, String>>) : CheckBoxGroup(
|
||||
class GenreFilter : CheckBoxGroup(
|
||||
"Genres",
|
||||
"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
|
||||
|
||||
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(
|
||||
"Vortex Scans",
|
||||
"en",
|
||||
"https://vortexscans.org",
|
||||
)
|
||||
class VortexScans : HttpSource() {
|
||||
|
||||
override val name = "Vortex Scans"
|
||||
|
||||
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 {
|
||||
extName = 'Asura Scans'
|
||||
extClass = '.AsuraScans'
|
||||
extVersionCode = 36
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://asuracomic.net'
|
||||
overrideVersionCode = 4
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,50 +1,21 @@
|
|||
package eu.kanade.tachiyomi.extension.en.asurascans
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesiaAlt
|
||||
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.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.Response
|
||||
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.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 {
|
||||
// remove legacy preferences
|
||||
preferences.run {
|
||||
|
@ -54,256 +25,47 @@ class AsuraScans : ParsedHttpSource(), ConfigurableSource {
|
|||
if (contains("pref_base_url_host")) {
|
||||
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 = network.cloudflareClient.newBuilder()
|
||||
override val client = super.client.newBuilder()
|
||||
.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()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
override val seriesDescriptionSelector = "div.desc p, div.entry-content p, div[itemprop=description]:not(:has(p))"
|
||||
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 =
|
||||
GET("$baseUrl/series?genres=&status=-1&types=-1&order=rating&page=$page", headers)
|
||||
|
||||
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 val pageSelector = "div.rdminimal > img, div.rdminimal > p > img, div.rdminimal > a > img, div.rdminimal > p > a > img, " +
|
||||
"div.rdminimal > noscript > img, div.rdminimal > p > noscript > img, div.rdminimal > a > noscript > img, div.rdminimal > p > a > noscript > img"
|
||||
|
||||
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()) {
|
||||
url.addQueryParameter("name", query)
|
||||
}
|
||||
|
||||
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)
|
||||
return request.newBuilder()
|
||||
.url(url)
|
||||
.build()
|
||||
}
|
||||
|
||||
// Skip scriptPages
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("div > img[alt=chapter]").mapIndexed { i, element ->
|
||||
Page(i, imageUrl = element.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"
|
||||
return document.select(pageSelector)
|
||||
.filterNot { it.attr("src").isNullOrEmpty() }
|
||||
.mapIndexed { i, img -> Page(i, document.location(), img.attr("abs:src")) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
extName = 'Fury Manga'
|
||||
extClass = '.FuryManga'
|
||||
extName = 'Blazescans'
|
||||
extClass = '.Blazescans'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://furymanga.com'
|
||||
overrideVersionCode = 2
|
||||
isNsfw = true
|
||||
baseUrl = 'https://blazetoon.com'
|
||||
overrideVersionCode = 1
|
||||
}
|
||||
|
||||
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 java.util.concurrent.TimeUnit
|
||||
|
||||
class FuryManga : MangaThemesia(
|
||||
"Fury Manga",
|
||||
"https://furymanga.com",
|
||||
"en",
|
||||
"/comics",
|
||||
) {
|
||||
override val id = 3912200442923601567
|
||||
|
||||
class Blazescans : MangaThemesia("Blazescans", "https://blazetoon.com", "en") {
|
||||
override val client = super.client.newBuilder()
|
||||
.rateLimit(1, 2, TimeUnit.SECONDS)
|
||||
.build()
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'ComicExtra'
|
||||
extClass = '.ComicExtra'
|
||||
extVersionCode = 16
|
||||
extVersionCode = 15
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -23,7 +23,7 @@ class ComicExtra : ParsedHttpSource() {
|
|||
|
||||
override val name = "ComicExtra"
|
||||
|
||||
override val baseUrl = "https://comixextra.com"
|
||||
override val baseUrl = "https://comicextra.org"
|
||||
|
||||
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)
|
||||
}
|
||||
}
|