Compare commits

..

No commits in common. "ac57f5e3dde6197b96a3ae66e311c3dfeac4ec1d" and "01e67374ec95300fa6993c1df8302da0b3f4682a" have entirely different histories.

366 changed files with 1526 additions and 3444 deletions

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 4 baseVersionCode = 3

View File

@ -130,8 +130,8 @@ abstract class GravureBlogger(
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup() val document = response.asJsoup()
return document.select("div.post-body a:has(> img)").mapIndexed { i, it -> return document.select("div.post-body img").mapIndexed { i, it ->
Page(i, imageUrl = it.absUrl("href")) Page(i, imageUrl = it.absUrl("src"))
} }
} }

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 25 baseVersionCode = 24
dependencies { dependencies {
api(project(":lib:i18n")) api(project(":lib:i18n"))

View File

@ -225,13 +225,13 @@ abstract class HeanCms(
throw Exception(intl.format("url_changed_error", name, name)) throw Exception(intl.format("url_changed_error", name, name))
} }
val seriesSlug = manga.url.substringAfterLast("/").substringBefore("#") val seriesId = manga.url.substringAfterLast("#")
val apiHeaders = headersBuilder() val apiHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON) .add("Accept", ACCEPT_JSON)
.build() .build()
return GET("$apiUrl/series/$seriesSlug", apiHeaders) return GET("$apiUrl/series/id/$seriesId", apiHeaders)
} }
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {

View File

@ -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

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 30 baseVersionCode = 28

View File

@ -188,11 +188,7 @@ abstract class LibGroup(
} else { } else {
it.replace("\\", "") it.replace("\\", "")
} }
str.parseAs<AuthToken>().let { auth -> returnValue = str.parseAs<AuthToken>()
if (auth.isValid()) {
returnValue = auth
}
}
} }
latch.countDown() latch.countDown()
} }
@ -328,7 +324,7 @@ abstract class LibGroup(
if (currentBranch.value.branchId == defaultBranchId && sortingList == "ms_mixing") { // ms_mixing with default branch from api if (currentBranch.value.branchId == defaultBranchId && sortingList == "ms_mixing") { // ms_mixing with default branch from api
chapters.add(it.value.toSChapter(slugUrl, defaultBranchId, isScanUser())) chapters.add(it.value.toSChapter(slugUrl, defaultBranchId, isScanUser()))
} else if (defaultBranchId == null && sortingList == "ms_mixing") { // ms_mixing with first branch in chapter } else if (defaultBranchId == null && sortingList == "ms_mixing") { // ms_mixing with first branch in chapter
if (chapters.any { chpIt -> chpIt.chapter_number == it.value.number.toFloat() }) { if (chapters.any { chpIt -> chpIt.chapter_number == it.value.itemNumber }) {
chapters.add(it.value.toSChapter(slugUrl, currentBranch.value.branchId, isScanUser())) chapters.add(it.value.toSChapter(slugUrl, currentBranch.value.branchId, isScanUser()))
} }
} else if (sortingList == "ms_combining") { // ms_combining } else if (sortingList == "ms_combining") { // ms_combining

View File

@ -202,6 +202,7 @@ class Chapter(
val name: String?, val name: String?,
val number: String, val number: String,
val volume: String, val volume: String,
@SerialName("item_number") val itemNumber: Float?,
) { ) {
@Serializable @Serializable
class Branch( class Branch(
@ -240,7 +241,7 @@ class Chapter(
url = "/$slugUrl/chapter?$branchStr&volume=$volume&number=$number" url = "/$slugUrl/chapter?$branchStr&volume=$volume&number=$number"
scanlator = getTeamName(branchId) ?: if (isScanUser) getUserName(branchId) else null scanlator = getTeamName(branchId) ?: if (isScanUser) getUserName(branchId) else null
date_upload = runCatching { LibGroup.simpleDateFormat.parse(first(branchId)!!.createdAt)!!.time }.getOrDefault(0L) date_upload = runCatching { LibGroup.simpleDateFormat.parse(first(branchId)!!.createdAt)!!.time }.getOrDefault(0L)
chapter_number = number.toFloat() chapter_number = itemNumber ?: -1f
} }
} }
@ -266,8 +267,8 @@ class Pages(
@Serializable @Serializable
class AuthToken( class AuthToken(
private val auth: Auth?, private val auth: Auth,
private val token: Token?, private val token: Token,
) { ) {
@Serializable @Serializable
class Auth( class Auth(
@ -282,15 +283,13 @@ class AuthToken(
@SerialName("access_token") val accessToken: String, @SerialName("access_token") val accessToken: String,
) )
fun isValid(): Boolean = auth != null && token != null
fun isExpired(): Boolean { fun isExpired(): Boolean {
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
val expiresIn = token!!.timestamp + (token.expiresIn * 1000) val expiresIn = token.timestamp + (token.expiresIn * 1000)
return expiresIn < currentTime return expiresIn < currentTime
} }
fun getToken(): String = "${token!!.tokenType} ${token.accessToken}" fun getToken(): String = "${token.tokenType} ${token.accessToken}"
fun getUserId(): Int = auth!!.id fun getUserId(): Int = auth.id
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.en.readallcomicscom package eu.kanade.tachiyomi.multisrc.readallcomics
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
@ -10,10 +10,8 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
@ -21,13 +19,11 @@ import org.jsoup.nodes.Element
import org.jsoup.select.Elements import org.jsoup.select.Elements
import rx.Observable import rx.Observable
class ReadAllComics : ParsedHttpSource() { abstract class ReadAllComics(
override val name: String,
override val name = "ReadAllComics" override val baseUrl: String,
override val lang: String,
override val baseUrl = "https://readallcomics.com" ) : ParsedHttpSource() {
override val lang = "en"
override val supportsLatest = false override val supportsLatest = false
@ -38,7 +34,7 @@ class ReadAllComics : ParsedHttpSource() {
.rateLimit(2) .rateLimit(2)
.build() .build()
private fun archivedCategoryInterceptor(chain: Interceptor.Chain): Response { protected open fun archivedCategoryInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request() val request = chain.request()
val response = chain.proceed(request) val response = chain.proceed(request)
@ -47,7 +43,7 @@ class ReadAllComics : ParsedHttpSource() {
request.url.toString(), request.url.toString(),
) )
val newUrl = document.selectFirst(".description-archive > p > span > a") val newUrl = document.selectFirst(archivedCategorySelector())
?.attr("href")?.toHttpUrlOrNull() ?.attr("href")?.toHttpUrlOrNull()
?: return response ?: return response
@ -64,18 +60,33 @@ class ReadAllComics : ParsedHttpSource() {
return response return response
} }
protected open fun archivedCategorySelector() = ".description-archive > p > span > a"
override fun popularMangaRequest(page: Int) = override fun popularMangaRequest(page: Int) =
GET("$baseUrl${if (page > 1)"/page/$page/" else ""}", headers) GET("$baseUrl${if (page > 1)"/page/$page/" else ""}", headers)
override fun popularMangaFromElement(element: Element): SManga { override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(popularMangaSelector()).mapNotNull {
nullablePopularManga(it)
}
val hasNextPage = document.select(popularMangaNextPageSelector()).first() != null
return MangasPage(mangas, hasNextPage)
}
protected open fun nullablePopularManga(element: Element): SManga? {
val manga = SManga.create().apply { val manga = SManga.create().apply {
val category = element.classNames() val category = element.classNames()
.firstOrNull { it.startsWith("category-") }!! .firstOrNull { it.startsWith("category-") }
.substringAfter("category-") ?.substringAfter("category-")
?: return null
url = "/category/$category/" url = "/category/$category/"
title = category.replace("-", " ").capitalizeEachWord() title = element.select(popularMangaTitleSelector()).text()
thumbnail_url = element.select("img").attr("abs:src") thumbnail_url = element.select(popularMangaThumbnailSelector()).attr("abs:src")
} }
return manga return manga
@ -83,6 +94,8 @@ class ReadAllComics : ParsedHttpSource() {
override fun popularMangaSelector() = "#post-area > div" override fun popularMangaSelector() = "#post-area > div"
override fun popularMangaNextPageSelector() = "div.pagenavi > a.next" override fun popularMangaNextPageSelector() = "div.pagenavi > a.next"
protected open fun popularMangaTitleSelector() = "h2"
protected open fun popularMangaThumbnailSelector() = "img"
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (page == 1) { return if (page == 1) {
@ -94,15 +107,10 @@ class ReadAllComics : ParsedHttpSource() {
} }
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
val url = baseUrl.toHttpUrl().newBuilder().apply { GET("$baseUrl/?story=${query.trim()}&s=&type=${searchType()}", headers)
addQueryParameter("story", query)
addQueryParameter("s", "")
addQueryParameter("type", "comic")
}.build()
return GET(url, headers) protected open fun searchType() = "comic"
}
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
searchPageElements = response.asJsoup().select(searchMangaSelector()) searchPageElements = response.asJsoup().select(searchMangaSelector())
@ -125,32 +133,27 @@ class ReadAllComics : ParsedHttpSource() {
override fun searchMangaFromElement(element: Element) = SManga.create().apply { override fun searchMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.attr("href")) setUrlWithoutDomain(element.attr("href"))
title = element.text() title = element.text().trim()
thumbnail_url = "https://fakeimg.pl/200x300/?text=No%20Cover%0AOn%20Search&font_size=62" thumbnail_url = searchCover
} }
override fun searchMangaSelector() = ".categories a" override fun searchMangaSelector() = ".categories a"
override fun searchMangaNextPageSelector() = null override fun searchMangaNextPageSelector() = null
override fun mangaDetailsParse(document: Document) = SManga.create().apply { override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst("h1")!!.text() title = document.select(mangaDetailsTitleSelector()).text().trim()
genre = document.select("p strong").joinToString { it.text() } genre = document.select(mangaDetailsGenreSelector()).joinToString { it.text().trim() }
author = document.select("p > strong").last()?.text() author = document.select(mangaDetailsAuthorSelector()).last()?.text()?.trim()
description = buildString { description = document.select(mangaDetailsDescriptionSelector()).text().trim()
document.select(".b > strong").forEach { element -> thumbnail_url = document.select(mangaDetailsThumbnailSelector()).attr("abs:src")
val vol = element.previousElementSibling()
if (isNotBlank()) {
append("\n\n")
}
if (vol?.tagName() == "span") {
append(vol.text(), "\n")
}
append(element.text())
}
}
thumbnail_url = document.select("p img").attr("abs:src")
} }
protected open fun mangaDetailsTitleSelector() = "h1"
protected open fun mangaDetailsGenreSelector() = "p strong"
protected open fun mangaDetailsAuthorSelector() = "p > strong"
protected open fun mangaDetailsDescriptionSelector() = ".b > strong"
protected open fun mangaDetailsThumbnailSelector() = "p img"
override fun chapterListSelector() = ".list-story a" override fun chapterListSelector() = ".list-story a"
override fun chapterFromElement(element: Element) = SChapter.create().apply { override fun chapterFromElement(element: Element) = SChapter.create().apply {
@ -159,25 +162,15 @@ class ReadAllComics : ParsedHttpSource() {
} }
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
return document.select("body img:not(body div[id=\"logo\"] img)").mapIndexed { idx, element -> return document.select(pageListSelector()).mapIndexed { idx, element ->
Page(idx, "", element.attr("abs:src")) Page(idx, "", element.attr("abs:src"))
} }
} }
private fun String.capitalizeEachWord(): String { protected open fun pageListSelector() = "body > div img"
val result = StringBuilder(length)
var capitalize = true companion object {
for (char in this) { private const val searchCover = "https://fakeimg.pl/200x300/?text=No%20Cover%0AOn%20Search&font_size=62"
result.append(
if (capitalize) {
char.uppercase()
} else {
char.lowercase()
},
)
capitalize = char.isWhitespace()
}
return result.toString()
} }
override fun imageUrlParse(document: Document) = override fun imageUrlParse(document: Document) =
@ -190,4 +183,6 @@ class ReadAllComics : ParsedHttpSource() {
throw UnsupportedOperationException() throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector() = override fun latestUpdatesNextPageSelector() =
throw UnsupportedOperationException() throw UnsupportedOperationException()
override fun popularMangaFromElement(element: Element) =
throw UnsupportedOperationException()
} }

View File

@ -1,8 +1,7 @@
ext { ext {
extName = 'Galaxy' extName = 'Galaxy'
extClass = '.GalaxyFactory' extClass = '.GalaxyFactory'
extVersionCode = 4 extVersionCode = 2
isNsfw = false
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,13 +1,6 @@
package eu.kanade.tachiyomi.extension.all.galaxy package eu.kanade.tachiyomi.extension.all.galaxy
import android.app.Application
import android.content.SharedPreferences
import android.widget.Toast
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class GalaxyFactory : SourceFactory { class GalaxyFactory : SourceFactory {
@ -15,53 +8,8 @@ class GalaxyFactory : SourceFactory {
override val id = 2602904659965278831 override val id = 2602904659965278831
} }
class GalaxyManga : class GalaxyManga : Galaxy("Galaxy Manga", "https://ayoub-zrr.xyz", "ar") {
Galaxy("Galaxy Manga", "https://galaxymanga.net", "ar"),
ConfigurableSource {
override val id = 2729515745226258240 override val id = 2729515745226258240
override val baseUrl by lazy { getPrefBaseUrl() }
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
companion object {
private const val RESTART_APP = ".لتطبيق الإعدادات الجديدة أعد تشغيل التطبيق"
private const val BASE_URL_PREF_TITLE = "تعديل الرابط"
private const val BASE_URL_PREF = "overrideBaseUrl"
private const val BASE_URL_PREF_SUMMARY = ".للاستخدام المؤقت. تحديث التطبيق سيؤدي الى حذف الإعدادات"
private const val DEFAULT_BASE_URL_PREF = "defaultBaseUrl"
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val baseUrlPref = androidx.preference.EditTextPreference(screen.context).apply {
key = BASE_URL_PREF
title = BASE_URL_PREF_TITLE
summary = BASE_URL_PREF_SUMMARY
this.setDefaultValue(super.baseUrl)
dialogTitle = BASE_URL_PREF_TITLE
dialogMessage = "Default: ${super.baseUrl}"
setOnPreferenceChangeListener { _, _ ->
Toast.makeText(screen.context, RESTART_APP, Toast.LENGTH_LONG).show()
true
}
}
screen.addPreference(baseUrlPref)
}
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, super.baseUrl)!!
init {
preferences.getString(DEFAULT_BASE_URL_PREF, null).let { prefDefaultBaseUrl ->
if (prefDefaultBaseUrl != super.baseUrl) {
preferences.edit()
.putString(BASE_URL_PREF, super.baseUrl)
.putString(DEFAULT_BASE_URL_PREF, super.baseUrl)
.apply()
}
}
}
} }
override fun createSources() = listOf( override fun createSources() = listOf(

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Hitomi' extName = 'Hitomi'
extClass = '.HitomiFactory' extClass = '.HitomiFactory'
extVersionCode = 32 extVersionCode = 31
isNsfw = true isNsfw = true
} }

View File

@ -1,12 +1,7 @@
package eu.kanade.tachiyomi.extension.all.hitomi package eu.kanade.tachiyomi.extension.all.hitomi
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -24,14 +19,9 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.Call import okhttp3.Call
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
@ -40,14 +30,13 @@ import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.LinkedList import java.util.LinkedList
import java.util.Locale import java.util.Locale
import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
class Hitomi( class Hitomi(
override val lang: String, override val lang: String,
private val nozomiLang: String, private val nozomiLang: String,
) : HttpSource(), ConfigurableSource { ) : HttpSource() {
override val name = "Hitomi" override val name = "Hitomi"
@ -61,14 +50,7 @@ class Hitomi(
private val json: Json by injectLazy() private val json: Json by injectLazy()
override val client = network.cloudflareClient.newBuilder() override val client = network.cloudflareClient
.addInterceptor(::Intercept)
.build()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private fun imageType() = preferences.getString(PREF_IMAGETYPE, "webp")!!
override fun headersBuilder() = super.headersBuilder() override fun headersBuilder() = super.headersBuilder()
.set("referer", "$baseUrl/") .set("referer", "$baseUrl/")
@ -506,7 +488,7 @@ class Hitomi(
private suspend fun Gallery.toSManga() = SManga.create().apply { private suspend fun Gallery.toSManga() = SManga.create().apply {
title = this@toSManga.title title = this@toSManga.title
url = galleryurl url = galleryurl
author = groups?.joinToString { it.formatted } ?: artists?.joinToString { it.formatted } author = groups?.joinToString { it.formatted }
artist = artists?.joinToString { it.formatted } artist = artists?.joinToString { it.formatted }
genre = tags?.joinToString { it.formatted } genre = tags?.joinToString { it.formatted }
thumbnail_url = files.first().let { thumbnail_url = files.first().let {
@ -585,25 +567,14 @@ class Hitomi(
gallery.files.mapIndexed { idx, img -> gallery.files.mapIndexed { idx, img ->
val hash = img.hash val hash = img.hash
val typePref = imageType()
val avif = img.hasavif == 1 && typePref == "avif"
val jxl = img.hasjxl == 1 && typePref == "jxl"
val commonId = commonImageId() val commonId = commonImageId()
val imageId = imageIdFromHash(hash) val imageId = imageIdFromHash(hash)
val subDomain = 'a' + subdomainOffset(imageId) val subDomain = 'a' + subdomainOffset(imageId)
val imageUrl = when {
jxl -> "https://${subDomain}a.$domain/jxl/$commonId$imageId/$hash.jxl"
avif -> "https://${subDomain}a.$domain/avif/$commonId$imageId/$hash.avif"
else -> "https://${subDomain}a.$domain/webp/$commonId$imageId/$hash.webp"
}
Page( Page(
idx, idx,
"$baseUrl/reader/$id.html", "$baseUrl/reader/$id.html",
imageUrl, "https://${subDomain}a.$domain/webp/$commonId$imageId/$hash.webp",
) )
} }
} }
@ -686,45 +657,6 @@ class Hitomi(
return hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1") return hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1")
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_IMAGETYPE
title = "Images Type"
entries = arrayOf("webp", "avif", "jxl")
entryValues = arrayOf("webp", "avif", "jxl")
summary = "Clear chapter cache to apply changes"
setDefaultValue("webp")
}.also(screen::addPreference)
}
private fun List<Int>.toBytesList(): ByteArray = this.map { it.toByte() }.toByteArray()
private val signatureOne = listOf(0xFF, 0x0A).toBytesList()
private val signatureTwo = listOf(0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A).toBytesList()
fun ByteArray.startsWith(byteArray: ByteArray): Boolean {
if (this.size < byteArray.size) return false
return this.sliceArray(byteArray.indices).contentEquals(byteArray)
}
private fun Intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (response.headers["Content-Type"] != "application/octet-stream") {
return response
}
val bytesPeek = max(signatureOne.size, signatureTwo.size).toLong()
val bytesArray = response.peekBody(bytesPeek).bytes()
if (!(bytesArray.startsWith(signatureOne) || bytesArray.startsWith(signatureTwo))) {
return response
}
val type = "image/jxl"
val body = response.body.bytes().toResponseBody(type.toMediaType())
return response.newBuilder()
.body(body)
.header("Content-Type", type)
.build()
}
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException() override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException() override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
@ -732,8 +664,4 @@ class Hitomi(
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException() override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException() override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
companion object {
const val PREF_IMAGETYPE = "pref_image_type"
}
} }

View File

@ -21,9 +21,6 @@ class Gallery(
@Serializable @Serializable
class ImageFile( class ImageFile(
val hash: String, val hash: String,
val haswebp: Int,
val hasavif: Int,
val hasjxl: Int,
) )
@Serializable @Serializable

View File

@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:icon="@mipmap/ic_launcher"> <application android:icon="@mipmap/ic_launcher">
<activity <activity
android:name=".all.ninenineninehentai.AnimeHUrlActivity" android:name=".all.ninenineninehentai.NineNineNineHentaiUrlActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
android:theme="@android:style/Theme.NoDisplay"> android:theme="@android:style/Theme.NoDisplay">
@ -12,7 +12,7 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:host="animeh.to"/> <data android:host="999hentai.net"/>
<data android:scheme="https"/> <data android:scheme="https"/>
<data android:pathPattern="/hchapter/..*"/> <data android:pathPattern="/hchapter/..*"/>
</intent-filter> </intent-filter>

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'AnimeH' extName = '999Hentai'
extClass = '.AnimeHFactory' extClass = '.NineNineNineHentaiFactory'
extVersionCode = 7 extVersionCode = 6
isNsfw = true isNsfw = true
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -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 }

View File

@ -35,16 +35,16 @@ import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
open class AnimeH( open class NineNineNineHentai(
final override val lang: String, final override val lang: String,
private val siteLang: String = lang, private val siteLang: String = lang,
) : HttpSource(), ConfigurableSource { ) : HttpSource(), ConfigurableSource {
override val name = "AnimeH" override val name = "999Hentai"
override val baseUrl = "https://animeh.to" override val baseUrl = "https://999hentai.net"
private val apiUrl = "https://api.animeh.to/api" private val apiUrl = "https://hapi.999hentai.net/api"
override val supportsLatest = true override val supportsLatest = true

View File

@ -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"),
)
}

View File

@ -7,7 +7,7 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import kotlin.system.exitProcess import kotlin.system.exitProcess
class AnimeHUrlActivity : Activity() { class NineNineNineHentaiUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments val pathSegments = intent?.data?.pathSegments
@ -15,17 +15,17 @@ class AnimeHUrlActivity : Activity() {
val id = pathSegments[1] val id = pathSegments[1]
val mainIntent = Intent().apply { val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH" action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${AnimeH.SEARCH_PREFIX}$id") putExtra("query", "${NineNineNineHentai.SEARCH_PREFIX}$id")
putExtra("filter", packageName) putExtra("filter", packageName)
} }
try { try {
startActivity(mainIntent) startActivity(mainIntent)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Log.e("AnimeHUrlActivity", e.toString()) Log.e("999HentaiUrlActivity", e.toString())
} }
} else { } else {
Log.e("AnimeHUrlActivity", "could not parse uri from intent $intent") Log.e("999HentaiUrlActivity", "could not parse uri from intent $intent")
} }
finish() finish()

View File

@ -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>

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'PandaChaika' extName = 'PandaChaika'
extClass = '.PandaChaikaFactory' extClass = '.PandaChaikaFactory'
extVersionCode = 2 extVersionCode = 1
isNsfw = true isNsfw = true
} }

View File

@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.extension.all.pandachaika package eu.kanade.tachiyomi.extension.all.pandachaika
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -44,9 +42,6 @@ class PandaChaika(
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val fakkuRegex = Regex("""(?:https?://)?(?:www\.)?fakku\.net/hentai/""")
private val ehentaiRegex = Regex("""(?:https?://)?e-hentai\.org/g/""")
// Popular // Popular
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return GET("$baseSearchUrl/?tags=$searchLang&sort=rating&apply=&json=&page=$page", headers) return GET("$baseSearchUrl/?tags=$searchLang&sort=rating&apply=&json=&page=$page", headers)
@ -78,76 +73,6 @@ class PandaChaika(
} }
} }
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return when {
query.startsWith(PREFIX_ID_SEARCH) -> {
val id = query.removePrefix(PREFIX_ID_SEARCH).toInt()
client.newCall(GET("$baseUrl/api?archive=$id", headers))
.asObservable()
.map { response ->
searchMangaByIdParse(response, id)
}
}
query.startsWith(PREFIX_EHEN_ID_SEARCH) -> {
val id = query.removePrefix(PREFIX_EHEN_ID_SEARCH).replace(ehentaiRegex, "")
val baseLink = "https://e-hentai.org/g/"
val fullLink = baseSearchUrl.toHttpUrl().newBuilder().apply {
addQueryParameter("qsearch", baseLink + id)
addQueryParameter("json", "")
}.build()
client.newCall(GET(fullLink, headers))
.asObservableSuccess()
.map {
val archive = it.parseAs<ArchiveResponse>().archives.getOrNull(0)?.toSManga() ?: throw Exception("Not Found")
MangasPage(listOf(archive), false)
}
}
query.startsWith(PREFIX_FAK_ID_SEARCH) -> {
val slug = query.removePrefix(PREFIX_FAK_ID_SEARCH).replace(fakkuRegex, "")
val baseLink = "https://www.fakku.net/hentai/"
val fullLink = baseSearchUrl.toHttpUrl().newBuilder().apply {
addQueryParameter("qsearch", baseLink + slug)
addQueryParameter("json", "")
}.build()
client.newCall(GET(fullLink, headers))
.asObservableSuccess()
.map {
val archive = it.parseAs<ArchiveResponse>().archives.getOrNull(0)?.toSManga() ?: throw Exception("Not Found")
MangasPage(listOf(archive), false)
}
}
query.startsWith(PREFIX_SOURCE_SEARCH) -> {
val url = query.removePrefix(PREFIX_SOURCE_SEARCH)
client.newCall(GET("$baseSearchUrl/?qsearch=$url&json=", headers))
.asObservableSuccess()
.map {
val archive = it.parseAs<ArchiveResponse>().archives.getOrNull(0)?.toSManga() ?: throw Exception("Not Found")
MangasPage(listOf(archive), false)
}
}
else -> super.fetchSearchManga(page, query, filters)
}
}
private fun searchMangaByIdParse(response: Response, id: Int = 0): MangasPage {
val title = response.parseAs<Archive>().title
val fullLink = baseSearchUrl.toHttpUrl().newBuilder().apply {
addQueryParameter("qsearch", title)
addQueryParameter("json", "")
}.build()
val archive = client.newCall(GET(fullLink, headers))
.execute()
.parseAs<ArchiveResponse>().archives
.find {
it.id == id
}
?.toSManga()
?: throw Exception("Invalid ID")
return MangasPage(listOf(archive), false)
}
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val library = response.parseAs<ArchiveResponse>() val library = response.parseAs<ArchiveResponse>()
@ -325,11 +250,4 @@ class PandaChaika(
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
override fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException() override fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException()
override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException() override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
companion object {
const val PREFIX_ID_SEARCH = "id:"
const val PREFIX_FAK_ID_SEARCH = "fakku:"
const val PREFIX_EHEN_ID_SEARCH = "ehentai:"
const val PREFIX_SOURCE_SEARCH = "source:"
}
} }

View File

@ -8,7 +8,7 @@ import java.util.Date
import java.util.Locale import java.util.Locale
val dateReformat = SimpleDateFormat("EEEE, d MMM yyyy HH:mm (z)", Locale.ENGLISH) val dateReformat = SimpleDateFormat("EEEE, d MMM yyyy HH:mm (z)", Locale.ENGLISH)
fun filterTags(include: String = "", exclude: List<String> = emptyList(), tags: List<String>): String? { fun filterTags(include: String = "", exclude: List<String> = emptyList(), tags: List<String>): String {
return tags.filter { it.startsWith("$include:") && exclude.none { substring -> it.startsWith("$substring:") } } return tags.filter { it.startsWith("$include:") && exclude.none { substring -> it.startsWith("$substring:") } }
.joinToString { .joinToString {
it.substringAfter(":").replace("_", " ").split(" ").joinToString(" ") { s -> it.substringAfter(":").replace("_", " ").split(" ").joinToString(" ") { s ->
@ -16,13 +16,13 @@ fun filterTags(include: String = "", exclude: List<String> = emptyList(), tags:
if (sr.isLowerCase()) sr.titlecase(Locale.getDefault()) else sr.toString() if (sr.isLowerCase()) sr.titlecase(Locale.getDefault()) else sr.toString()
} }
} }
}.takeIf { it.isNotBlank() } }
} }
fun getReadableSize(bytes: Double): String { fun getReadableSize(bytes: Double): String {
return when { return when {
bytes >= 300 * 1000 * 1000 -> "${"%.2f".format(bytes / (1000.0 * 1000.0 * 1000.0))} GB" bytes >= 300 * 1024 * 1024 -> "${"%.2f".format(bytes / (1024.0 * 1024.0 * 1024.0))} GB"
bytes >= 100 * 1000 -> "${"%.2f".format(bytes / (1000.0 * 1000.0))} MB" bytes >= 100 * 1024 -> "${"%.2f".format(bytes / (1024.0 * 1024.0))} MB"
bytes >= 1000 -> "${"%.2f".format(bytes / (1000.0))} kB" bytes >= 1024 -> "${"%.2f".format(bytes / (1024.0))} KB"
else -> "$bytes B" else -> "$bytes B"
} }
} }
@ -31,14 +31,13 @@ fun getReadableSize(bytes: Double): String {
class Archive( class Archive(
val download: String, val download: String,
val posted: Long, val posted: Long,
val title: String,
) )
@Serializable @Serializable
class LongArchive( class LongArchive(
private val thumbnail: String, private val thumbnail: String,
private val title: String, private val title: String,
val id: Int, private val id: Int,
private val posted: Long?, private val posted: Long?,
private val public_date: Long?, private val public_date: Long?,
private val filecount: Int, private val filecount: Int,
@ -51,47 +50,35 @@ class LongArchive(
val groups = filterTags("group", tags = tags) val groups = filterTags("group", tags = tags)
val artists = filterTags("artist", tags = tags) val artists = filterTags("artist", tags = tags)
val publishers = filterTags("publisher", tags = tags) val publishers = filterTags("publisher", tags = tags)
val characters = filterTags("character", tags = tags)
val male = filterTags("male", tags = tags) val male = filterTags("male", tags = tags)
val female = filterTags("female", tags = tags) val female = filterTags("female", tags = tags)
val others = filterTags(exclude = listOf("female", "male", "artist", "publisher", "group", "parody"), tags = tags) val others = filterTags(exclude = listOf("female", "male", "artist", "publisher", "group", "parody"), tags = tags)
val parodies = filterTags("parody", tags = tags) val parodies = filterTags("parody", tags = tags)
var appended = false
url = id.toString() url = id.toString()
title = this@LongArchive.title title = this@LongArchive.title
thumbnail_url = thumbnail thumbnail_url = thumbnail
author = groups ?: artists author = groups.ifEmpty { artists }
artist = artists artist = artists
genre = listOf(male, female, others).joinToString() genre = listOf(male, female, others).joinToString()
description = buildString { description = buildString {
append("Uploader: ", uploader.ifEmpty { "Anonymous" }, "\n") append("Uploader: ", uploader.ifEmpty { "Anonymous" }, "\n")
publishers?.let { publishers.takeIf { it.isNotBlank() }?.let {
append("Publishers: ", it, "\n") append("Publishers: ", it, "\n\n")
} }
append("\n") parodies.takeIf { it.isNotBlank() }?.let {
append("Parodies: ", it, "\n\n")
parodies?.let {
append("Parodies: ", it, "\n")
appended = true
} }
characters?.let { male.takeIf { it.isNotBlank() }?.let {
append("Characters: ", it, "\n")
appended = true
}
if (appended) append("\n")
male?.let {
append("Male tags: ", it, "\n\n") append("Male tags: ", it, "\n\n")
} }
female?.let { female.takeIf { it.isNotBlank() }?.let {
append("Female tags: ", it, "\n\n") append("Female tags: ", it, "\n\n")
} }
others?.let { others.takeIf { it.isNotBlank() }?.let {
append("Other tags: ", it, "\n\n") append("Other tags: ", it, "\n\n")
} }
title_jpn?.takeIf { it.isNotEmpty() }?.let { append("Japanese Title: ", it, "\n") } title_jpn?.let { append("Japanese Title: ", it, "\n") }
append("Pages: ", filecount, "\n") append("Pages: ", filecount, "\n")
append("File Size: ", getReadableSize(filesize), "\n") append("File Size: ", getReadableSize(filesize), "\n")

View File

@ -17,7 +17,6 @@ fun getFilters(): FilterList {
TextFilter("Female Tags", "female"), TextFilter("Female Tags", "female"),
TextFilter("Artists", "artist"), TextFilter("Artists", "artist"),
TextFilter("Parodies", "parody"), TextFilter("Parodies", "parody"),
TextFilter("Characters", "character"),
Filter.Separator(), Filter.Separator(),
TextFilter("Reason", "reason"), TextFilter("Reason", "reason"),
TextFilter("Uploader", "reason"), TextFilter("Uploader", "reason"),
@ -53,11 +52,11 @@ private val getTypes = listOf(
private val getSortsList: List<Pair<String, String>> = listOf( private val getSortsList: List<Pair<String, String>> = listOf(
Pair("Public Date", "public_date"), Pair("Public Date", "public_date"),
Pair("Posted Date", "posted"), Pair("Posted Date", "posted_date"),
Pair("Title", "title"), Pair("Title", "title"),
Pair("Japanese Title", "title_jpn"), Pair("Japanese Title", "title_jpn"),
Pair("Rating", "rating"), Pair("Rating", "rating"),
Pair("Images", "filecount"), Pair("Images", "images"),
Pair("File Size", "filesize"), Pair("File Size", "size"),
Pair("Category", "category"), Pair("Category", "category"),
) )

View File

@ -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)
}
}

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Union Mangas' extName = 'Union Mangas'
extClass = '.UnionMangasFactory' extClass = '.UnionMangasFactory'
extVersionCode = 6 extVersionCode = 5
isNsfw = true isNsfw = true
} }

View File

@ -203,7 +203,7 @@ class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
companion object { companion object {
const val SEARCH_PREFIX = "slug:" const val SEARCH_PREFIX = "slug:"
val apiUrl = "https://api.novelfull.us/api" val apiUrl = "https://app.unionmanga.xyz/api"
val oldApiUrl = "https://api.unionmanga.xyz" val oldApiUrl = "https://api.unionmanga.xyz"
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH) val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
} }

View File

@ -1,8 +1,8 @@
ext { ext {
extName = 'Hadess' extName = 'Crow Scans'
extClass = '.Hadess' extClass = '.CrowScans'
themePkg = 'madara' themePkg = 'mangathemesia'
baseUrl = 'https://www.hadess.xyz' baseUrl = 'https://crowscans.com'
overrideVersionCode = 0 overrideVersionCode = 0
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -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")),
)

View File

@ -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}"
}

View File

@ -2,9 +2,8 @@ ext {
extName = 'MangaNoon' extName = 'MangaNoon'
extClass = '.MangaNoon' extClass = '.MangaNoon'
themePkg = 'mangathemesia' themePkg = 'mangathemesia'
baseUrl = 'https://manjanoon.co' baseUrl = 'https://manjanoon.org'
overrideVersionCode = 3 overrideVersionCode = 2
isNsfw = false
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,89 +1,12 @@
package eu.kanade.tachiyomi.extension.ar.manganoon package eu.kanade.tachiyomi.extension.ar.manganoon
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.source.model.SChapter import java.text.SimpleDateFormat
import org.jsoup.nodes.Element import java.util.Locale
import java.util.Calendar
class MangaNoon : MangaThemesia( class MangaNoon : MangaThemesia(
"مانجا نون", "مانجا نون",
"https://manjanoon.co", "https://manjanoon.org",
"ar", "ar",
) { dateFormat = SimpleDateFormat("MMM d, yyy", Locale("ar")),
)
override fun chapterFromElement(element: Element): SChapter {
return super.chapterFromElement(element).apply {
date_upload = element.selectFirst(".chapterdate")?.text().parseChapterDate()
}
}
// From Galaxy
override fun String?.parseChapterDate(): Long {
this ?: return 0L
val number = Regex("""(\d+)""").find(this)?.value?.toIntOrNull() ?: 0
val cal = Calendar.getInstance()
return when {
listOf("second", "ثانية").any { contains(it, true) } -> {
cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
}
contains("دقيقتين", true) -> {
cal.apply { add(Calendar.MINUTE, -2) }.timeInMillis
}
listOf("minute", "دقائق").any { contains(it, true) } -> {
cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
}
contains("ساعتان", true) -> {
cal.apply { add(Calendar.HOUR, -2) }.timeInMillis
}
listOf("hour", "ساعات").any { contains(it, true) } -> {
cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
}
contains("يوم", true) -> {
cal.apply { add(Calendar.DAY_OF_YEAR, -1) }.timeInMillis
}
contains("يومين", true) -> {
cal.apply { add(Calendar.DAY_OF_YEAR, -2) }.timeInMillis
}
listOf("day", "أيام").any { contains(it, true) } -> {
cal.apply { add(Calendar.DAY_OF_YEAR, -number) }.timeInMillis
}
contains("أسبوع", true) -> {
cal.apply { add(Calendar.WEEK_OF_YEAR, -1) }.timeInMillis
}
contains("أسبوعين", true) -> {
cal.apply { add(Calendar.WEEK_OF_YEAR, -2) }.timeInMillis
}
listOf("week", "أسابيع").any { contains(it, true) } -> {
cal.apply { add(Calendar.WEEK_OF_YEAR, -number) }.timeInMillis
}
contains("شهر", true) -> {
cal.apply { add(Calendar.MONTH, -1) }.timeInMillis
}
contains("شهرين", true) -> {
cal.apply { add(Calendar.MONTH, -2) }.timeInMillis
}
listOf("month", "أشهر").any { contains(it, true) } -> {
cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
}
contains("سنة", true) -> {
cal.apply { add(Calendar.YEAR, -1) }.timeInMillis
}
contains("سنتان", true) -> {
cal.apply { add(Calendar.YEAR, -2) }.timeInMillis
}
listOf("year", "سنوات").any { contains(it, true) } -> {
cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
}
else -> 0L
}
}
}

View File

@ -2,8 +2,8 @@ ext {
extName = 'MangaSwat' extName = 'MangaSwat'
extClass = '.MangaSwat' extClass = '.MangaSwat'
themePkg = 'mangathemesia' themePkg = 'mangathemesia'
baseUrl = 'https://maxlevelteam.com' baseUrl = 'https://t1manga.com'
overrideVersionCode = 20 overrideVersionCode = 19
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -24,7 +24,7 @@ import java.util.Locale
class MangaSwat : class MangaSwat :
MangaThemesia( MangaThemesia(
"MangaSwat", "MangaSwat",
"https://maxlevelteam.com", "https://t1manga.com",
"ar", "ar",
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")), dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
), ),

View File

@ -1,10 +0,0 @@
ext {
extName = 'NoonScan'
extClass = '.NoonScan'
themePkg = 'mangathemesia'
baseUrl = 'https://noonscan.com'
overrideVersionCode = 0
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

@ -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")),
)

View File

@ -0,0 +1,9 @@
ext {
extName = 'Novels Town'
extClass = '.NovelsTown'
themePkg = 'madara'
baseUrl = 'https://novelstown.com'
overrideVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -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 = "الاعمال"
}

View File

@ -1,8 +1,7 @@
ext { ext {
extName = 'Vortex Scans' extName = 'Vortex Scans'
extClass = '.VortexScans' extClass = '.VortexScans'
themePkg = 'iken' extVersionCode = 33
overrideVersionCode = 33
isNsfw = false isNsfw = false
} }

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.multisrc.iken package eu.kanade.tachiyomi.extension.en.arvenscans
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
@ -18,7 +18,7 @@ class SearchResponse(
@Serializable @Serializable
class Manga( class Manga(
private val id: Int, val id: Int,
val slug: String, val slug: String,
private val postTitle: String, private val postTitle: String,
private val postContent: String? = null, private val postContent: String? = null,
@ -29,7 +29,7 @@ class Manga(
private val artist: String? = null, private val artist: String? = null,
private val seriesType: String? = null, private val seriesType: String? = null,
private val seriesStatus: String? = null, private val seriesStatus: String? = null,
val genres: List<Genre> = emptyList(), private val genres: List<Name>? = emptyList(),
) { ) {
fun toSManga(baseUrl: String) = SManga.create().apply { fun toSManga(baseUrl: String) = SManga.create().apply {
url = "$slug#$id" url = "$slug#$id"
@ -70,16 +70,10 @@ class Manga(
"MANHWA" -> add("Manhwa") "MANHWA" -> add("Manhwa")
else -> {} else -> {}
} }
genres.forEach { add(it.name) } genres?.forEach { add(it.name) }
}.distinct().joinToString() }.distinct().joinToString()
} }
@Serializable
class Genre(
val id: Int,
val name: String,
)
@Serializable @Serializable
class Name(val name: String) class Name(val name: String)

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.multisrc.iken package eu.kanade.tachiyomi.extension.en.arvenscans
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl import okhttp3.HttpUrl
@ -65,8 +65,37 @@ class TypeFilter : SelectFilter(
), ),
) )
class GenreFilter(genres: List<Pair<String, String>>) : CheckBoxGroup( class GenreFilter : CheckBoxGroup(
"Genres", "Genres",
"genreIds", "genreIds",
genres, listOf(
Pair("Action", "1"),
Pair("Adventure", "13"),
Pair("Comedy", "7"),
Pair("Drama", "2"),
Pair("elf", "25"),
Pair("Fantas", "28"),
Pair("Fantasy", "8"),
Pair("Historical", "19"),
Pair("Horror", "9"),
Pair("Josei", "21"),
Pair("Manhwa", "5"),
Pair("Martial Arts", "6"),
Pair("Mature", "12"),
Pair("Monsters", "14"),
Pair("Reincarnation", "16"),
Pair("Revenge", "17"),
Pair("Romance", "20"),
Pair("School Life", "23"),
Pair("Seinen", "10"),
Pair("shojo", "26"),
Pair("Shoujo", "22"),
Pair("Shounen", "3"),
Pair("Slice Of Life", "18"),
Pair("Sports", "4"),
Pair("Supernatural", "11"),
Pair("System", "15"),
Pair("terror", "24"),
Pair("Video Games", "27"),
),
) )

View File

@ -1,9 +1,145 @@
package eu.kanade.tachiyomi.extension.en.arvenscans package eu.kanade.tachiyomi.extension.en.arvenscans
import eu.kanade.tachiyomi.multisrc.iken.Iken import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class VortexScans : Iken( class VortexScans : HttpSource() {
"Vortex Scans",
"en", override val name = "Vortex Scans"
"https://vortexscans.org",
) override val baseUrl = "https://vortexscans.org"
override val lang = "en"
override val supportsLatest = true
override val client = network.cloudflareClient
private val json by injectLazy<Json>()
override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/")
private val titleCache by lazy {
val response = client.newCall(GET("$baseUrl/api/query?perPage=9999", headers)).execute()
val data = response.parseAs<SearchResponse>()
data.posts
.filterNot { it.isNovel }
.associateBy { it.slug }
}
override fun popularMangaRequest(page: Int) = GET("$baseUrl/home", headers)
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val slugs = document.select("div:contains(Popular) + div.swiper div.manga-swipe > a")
.map { it.absUrl("href").substringAfterLast("/series/") }
val entries = slugs.mapNotNull {
titleCache[it]?.toSManga(baseUrl)
}
return MangasPage(entries, false)
}
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", getFilterList())
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/api/query".toHttpUrl().newBuilder().apply {
addQueryParameter("page", page.toString())
addQueryParameter("perPage", perPage.toString())
addQueryParameter("searchTerm", query.trim())
filters.filterIsInstance<UrlPartFilter>().forEach {
it.addUrlParameter(this)
}
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val data = response.parseAs<SearchResponse>()
val page = response.request.url.queryParameter("page")!!.toInt()
val entries = data.posts
.filterNot { it.isNovel }
.map { it.toSManga(baseUrl) }
val hasNextPage = data.totalCount > (page * perPage)
return MangasPage(entries, hasNextPage)
}
override fun getFilterList() = FilterList(
StatusFilter(),
TypeFilter(),
GenreFilter(),
)
override fun mangaDetailsRequest(manga: SManga): Request {
val id = manga.url.substringAfterLast("#")
val url = "$baseUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid="
return GET(url, headers)
}
override fun getMangaUrl(manga: SManga): String {
val slug = manga.url.substringBeforeLast("#")
return "$baseUrl/series/$slug"
}
override fun mangaDetailsParse(response: Response): SManga {
val data = response.parseAs<Post<Manga>>()
assert(!data.post.isNovel) { "Novels are unsupported" }
// genres are only returned in search call
// and not when fetching details
return data.post.toSManga(baseUrl).apply {
genre = titleCache[data.post.slug]?.getGenres()
}
}
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> {
val data = response.parseAs<Post<ChapterListResponse>>()
assert(!data.post.isNovel) { "Novels are unsupported" }
return data.post.chapters
.filter { it.isPublic() }
.map { it.toSChapter(data.post.slug) }
}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
return document.select("main section > img").mapIndexed { idx, img ->
Page(idx, imageUrl = img.absUrl("src"))
}
}
override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException()
private inline fun <reified T> Response.parseAs(): T =
json.decodeFromString(body.string())
}
private const val perPage = 18

View File

@ -1,7 +1,9 @@
ext { ext {
extName = 'Asura Scans' extName = 'Asura Scans'
extClass = '.AsuraScans' extClass = '.AsuraScans'
extVersionCode = 36 themePkg = 'mangathemesia'
baseUrl = 'https://asuracomic.net'
overrideVersionCode = 4
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,50 +1,21 @@
package eu.kanade.tachiyomi.extension.en.asurascans package eu.kanade.tachiyomi.extension.en.asurascans
import android.app.Application import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesiaAlt
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import kotlin.concurrent.thread
class AsuraScans : ParsedHttpSource(), ConfigurableSource {
override val name = "Asura Scans"
override val baseUrl = "https://asuracomic.net"
private val apiUrl = "https://gg.asuracomic.net/api"
override val lang = "en"
override val supportsLatest = true
private val dateFormat = SimpleDateFormat("MMMM d yyyy", Locale.US)
private val preferences: SharedPreferences =
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
class AsuraScans : MangaThemesiaAlt(
"Asura Scans",
"https://asuracomic.net",
"en",
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US),
randomUrlPrefKey = "pref_permanent_manga_url_2_en",
) {
init { init {
// remove legacy preferences // remove legacy preferences
preferences.run { preferences.run {
@ -54,256 +25,47 @@ class AsuraScans : ParsedHttpSource(), ConfigurableSource {
if (contains("pref_base_url_host")) { if (contains("pref_base_url_host")) {
edit().remove("pref_base_url_host").apply() edit().remove("pref_base_url_host").apply()
} }
if (contains("pref_permanent_manga_url_2_en")) {
edit().remove("pref_permanent_manga_url_2_en").apply()
}
} }
} }
private val json: Json by injectLazy() override val client = super.client.newBuilder()
override val client = network.cloudflareClient.newBuilder()
.rateLimit(1, 3) .rateLimit(1, 3)
.apply {
val interceptors = interceptors()
val index = interceptors.indexOfFirst { "Brotli" in it.javaClass.simpleName }
if (index >= 0) {
interceptors.add(interceptors.removeAt(index))
}
}
.build() .build()
override fun headersBuilder() = super.headersBuilder() override val seriesDescriptionSelector = "div.desc p, div.entry-content p, div[itemprop=description]:not(:has(p))"
.add("Referer", "$baseUrl/") override val seriesArtistSelector = ".fmed b:contains(artist)+span, .infox span:contains(artist)"
override val seriesAuthorSelector = ".fmed b:contains(author)+span, .infox span:contains(author)"
override fun popularMangaRequest(page: Int): Request = override val pageSelector = "div.rdminimal > img, div.rdminimal > p > img, div.rdminimal > a > img, div.rdminimal > p > a > img, " +
GET("$baseUrl/series?genres=&status=-1&types=-1&order=rating&page=$page", headers) "div.rdminimal > noscript > img, div.rdminimal > p > noscript > img, div.rdminimal > a > noscript > img, div.rdminimal > p > a > noscript > img"
override fun popularMangaSelector() = searchMangaSelector()
override fun popularMangaFromElement(element: Element) = searchMangaFromElement(element)
override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/series?genres=&status=-1&types=-1&order=update&page=$page", headers)
override fun latestUpdatesSelector() = searchMangaSelector()
override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element)
override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/series".toHttpUrl().newBuilder() val request = super.searchMangaRequest(page, query, filters)
if (query.isBlank()) return request
url.addQueryParameter("page", page.toString()) val url = request.url.newBuilder()
.addPathSegment("page/$page/")
.removeAllQueryParameters("page")
.removeAllQueryParameters("title")
.addQueryParameter("s", query)
.build()
if (query.isNotBlank()) { return request.newBuilder()
url.addQueryParameter("name", query) .url(url)
} .build()
val genres = filters.firstInstanceOrNull<GenreFilter>()?.state.orEmpty()
.filter(Genre::state)
.map(Genre::id)
.joinToString(",")
val status = filters.firstInstanceOrNull<StatusFilter>()?.toUriPart() ?: "-1"
val types = filters.firstInstanceOrNull<TypeFilter>()?.toUriPart() ?: "-1"
val order = filters.firstInstanceOrNull<OrderFilter>()?.toUriPart() ?: "rating"
url.addQueryParameter("genres", genres)
url.addQueryParameter("status", status)
url.addQueryParameter("types", types)
url.addQueryParameter("order", order)
return GET(url.build(), headers)
}
override fun searchMangaSelector() = "div.grid > a[href]"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.attr("abs:href").toPermSlugIfNeeded())
title = element.selectFirst("div.block > span.block")!!.ownText()
thumbnail_url = element.selectFirst("img")?.attr("abs:src")
}
override fun searchMangaNextPageSelector() = "div.flex > a.flex.bg-themecolor:contains(Next)"
override fun getFilterList(): FilterList {
fetchFilters()
val filters = mutableListOf<Filter<*>>()
if (filtersState == FiltersState.FETCHED) {
filters += listOf(
GenreFilter("Genres", getGenreFilters()),
StatusFilter("Status", getStatusFilters()),
TypeFilter("Types", getTypeFilters()),
)
} else {
filters += Filter.Header("Press 'Reset' to attempt to fetch the filters")
}
filters += OrderFilter(
"Order by",
listOf(
Pair("Rating", "rating"),
Pair("Update", "update"),
Pair("Latest", "latest"),
Pair("Z-A", "desc"),
Pair("A-Z", "asc"),
),
)
return FilterList(filters)
}
private fun getGenreFilters(): List<Genre> = genresList.map { Genre(it.first, it.second) }
private fun getStatusFilters(): List<Pair<String, String>> = statusesList.map { it.first to it.second.toString() }
private fun getTypeFilters(): List<Pair<String, String>> = typesList.map { it.first to it.second.toString() }
private var genresList: List<Pair<String, Int>> = emptyList()
private var statusesList: List<Pair<String, Int>> = emptyList()
private var typesList: List<Pair<String, Int>> = emptyList()
private var fetchFiltersAttempts = 0
private var filtersState = FiltersState.NOT_FETCHED
private fun fetchFilters() {
if (filtersState != FiltersState.NOT_FETCHED || fetchFiltersAttempts >= 3) return
filtersState = FiltersState.FETCHING
fetchFiltersAttempts++
thread {
try {
val response = client.newCall(GET("$apiUrl/series/filters", headers)).execute()
val filters = json.decodeFromString<FiltersDto>(response.body.string())
genresList = filters.genres.filter { it.id > 0 }.map { it.name.trim() to it.id }
statusesList = filters.statuses.map { it.name.trim() to it.id }
typesList = filters.types.map { it.name.trim() to it.id }
filtersState = FiltersState.FETCHED
} catch (e: Throwable) {
filtersState = FiltersState.NOT_FETCHED
}
}
}
override fun mangaDetailsRequest(manga: SManga): Request {
if (!preferences.dynamicUrl()) return super.mangaDetailsRequest(manga)
val match = OLD_FORMAT_MANGA_REGEX.find(manga.url)?.groupValues?.get(2)
val slug = match ?: manga.url.substringAfter("/series/").substringBefore("/")
val savedSlug = preferences.slugMap[slug] ?: "$slug-"
return GET("$baseUrl/series/$savedSlug", headers)
}
override fun mangaDetailsParse(response: Response): SManga {
if (preferences.dynamicUrl()) {
val url = response.request.url.toString()
val newSlug = url.substringAfter("/series/").substringBefore("/")
val absSlug = newSlug.substringBeforeLast("-")
preferences.slugMap = preferences.slugMap.apply { put(absSlug, newSlug) }
}
return super.mangaDetailsParse(response)
}
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst("span.text-xl.font-bold")!!.ownText()
thumbnail_url = document.selectFirst("img[alt=poster]")?.attr("abs:src")
description = document.selectFirst("span.font-medium.text-sm")?.text()
author = document.selectFirst("div.grid > div:has(h3:eq(0):containsOwn(Author)) > h3:eq(1)")?.ownText()
artist = document.selectFirst("div.grid > div:has(h3:eq(0):containsOwn(Artist)) > h3:eq(1)")?.ownText()
genre = document.select("div[class^=space] > div.flex > button.text-white").joinToString { it.ownText() }
status = parseStatus(document.selectFirst("div.flex:has(h3:eq(0):containsOwn(Status)) > h3:eq(1)")?.ownText())
}
private fun parseStatus(status: String?) = when (status) {
"Ongoing", "Season End" -> SManga.ONGOING
"Hiatus" -> SManga.ON_HIATUS
"Completed" -> SManga.COMPLETED
"Dropped" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
override fun chapterListParse(response: Response): List<SChapter> {
if (preferences.dynamicUrl()) {
val url = response.request.url.toString()
val newSlug = url.substringAfter("/series/").substringBefore("/")
val absSlug = newSlug.substringBeforeLast("-")
preferences.slugMap = preferences.slugMap.apply { put(absSlug, newSlug) }
}
return super.chapterListParse(response)
}
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListSelector() = "div.scrollbar-thumb-themecolor > a.block"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.attr("abs:href").toPermSlugIfNeeded())
name = element.selectFirst("h3:eq(0)")!!.text()
date_upload = try {
val text = element.selectFirst("h3:eq(1)")!!.ownText()
val cleanText = text.replace(CLEAN_DATE_REGEX, "$1")
dateFormat.parse(cleanText)?.time ?: 0
} catch (_: Exception) {
0L
}
}
override fun pageListRequest(chapter: SChapter): Request {
if (!preferences.dynamicUrl()) return super.pageListRequest(chapter)
val match = OLD_FORMAT_CHAPTER_REGEX.containsMatchIn(chapter.url)
if (match) throw Exception("Please refresh the chapter list before reading.")
val slug = chapter.url.substringAfter("/series/").substringBefore("/")
val savedSlug = preferences.slugMap[slug] ?: "$slug-"
return GET(baseUrl + chapter.url.replace(slug, savedSlug), headers)
} }
// Skip scriptPages
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
return document.select("div > img[alt=chapter]").mapIndexed { i, element -> return document.select(pageSelector)
Page(i, imageUrl = element.attr("abs:src")) .filterNot { it.attr("src").isNullOrEmpty() }
} .mapIndexed { i, img -> Page(i, document.location(), img.attr("abs:src")) }
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
private enum class FiltersState { NOT_FETCHED, FETCHING, FETCHED }
private inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
filterIsInstance<R>().firstOrNull()
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = PREF_DYNAMIC_URL
title = "Automatically update dynamic URLs"
summary = "Automatically update random numbers in manga URLs.\nHelps mitigating HTTP 404 errors during update and \"in library\" marks when browsing.\nNote: This setting may require clearing database in advanced settings and migrating all manga to the same source."
setDefaultValue(true)
}.let(screen::addPreference)
}
private var SharedPreferences.slugMap: MutableMap<String, String>
get() {
val jsonMap = getString(PREF_SLUG_MAP, "{}")!!
return try {
json.decodeFromString<Map<String, String>>(jsonMap).toMutableMap()
} catch (_: Exception) {
mutableMapOf()
}
}
set(newSlugMap) {
edit()
.putString(PREF_SLUG_MAP, json.encodeToString(newSlugMap))
.apply()
}
private fun SharedPreferences.dynamicUrl(): Boolean = getBoolean(PREF_DYNAMIC_URL, true)
private fun String.toPermSlugIfNeeded(): String {
if (!preferences.dynamicUrl()) return this
val slug = this.substringAfter("/series/").substringBefore("/")
val absSlug = slug.substringBeforeLast("-")
preferences.slugMap = preferences.slugMap.apply { put(absSlug, slug) }
return this.replace(slug, absSlug)
}
companion object {
private val CLEAN_DATE_REGEX = """(\d+)(st|nd|rd|th)""".toRegex()
private val OLD_FORMAT_MANGA_REGEX = """^/manga/(\d+-)?([^/]+)/?$""".toRegex()
private val OLD_FORMAT_CHAPTER_REGEX = """^/(\d+-)?[^/]*-chapter-\d+(-\d+)*/?$""".toRegex()
private const val PREF_SLUG_MAP = "pref_slug_map"
private const val PREF_DYNAMIC_URL = "pref_dynamic_url"
} }
} }

View File

@ -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,
)

View File

@ -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
}

View File

@ -1,10 +1,9 @@
ext { ext {
extName = 'Fury Manga' extName = 'Blazescans'
extClass = '.FuryManga' extClass = '.Blazescans'
themePkg = 'mangathemesia' themePkg = 'mangathemesia'
baseUrl = 'https://furymanga.com' baseUrl = 'https://blazetoon.com'
overrideVersionCode = 2 overrideVersionCode = 1
isNsfw = true
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -4,14 +4,7 @@ import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class FuryManga : MangaThemesia( class Blazescans : MangaThemesia("Blazescans", "https://blazetoon.com", "en") {
"Fury Manga",
"https://furymanga.com",
"en",
"/comics",
) {
override val id = 3912200442923601567
override val client = super.client.newBuilder() override val client = super.client.newBuilder()
.rateLimit(1, 2, TimeUnit.SECONDS) .rateLimit(1, 2, TimeUnit.SECONDS)
.build() .build()

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'ComicExtra' extName = 'ComicExtra'
extClass = '.ComicExtra' extClass = '.ComicExtra'
extVersionCode = 16 extVersionCode = 15
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -23,7 +23,7 @@ class ComicExtra : ParsedHttpSource() {
override val name = "ComicExtra" override val name = "ComicExtra"
override val baseUrl = "https://comixextra.com" override val baseUrl = "https://comicextra.org"
override val lang = "en" override val lang = "en"

View File

@ -1,7 +0,0 @@
ext {
extName = 'Darths & Droids'
extClass = '.DarthsDroids'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@ -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:
// Im sorry if this extension causes too much traffic for your site.
// Unfortunately we cant just download and use your Zip downloads.
// Shall problems arise, well 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 books
// 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 doesnt 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 its 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 itll 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 books future
// archive name.
// ◦ This is especially apparent with the »Solo« book, whichs
// 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 books page
// feed to be empty. Even worse, they may find it starting anew with
// different pages. A manual refresh *should* change the books `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 thats
// even worse in terms of user experience. Maybe one day well have new
// extension APIs for dealing with unique webcomic weirdnesses. cause
// trust me, theres 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 cant 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 were 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 its `div.center>p>img`, except for pages released on
// Aprils Fools day, when its `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 Aprils 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)
}
}

Some files were not shown because too many files have changed in this diff Show More