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

View File

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

View File

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

View File

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

View File

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

View File

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

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")
}
baseVersionCode = 30
baseVersionCode = 28

View File

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

View File

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

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.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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
ext {
extName = 'AnimeH'
extClass = '.AnimeHFactory'
extVersionCode = 7
extName = '999Hentai'
extClass = '.NineNineNineHentaiFactory'
extVersionCode = 6
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.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

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 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()

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 {
extName = 'PandaChaika'
extClass = '.PandaChaikaFactory'
extVersionCode = 2
extVersionCode = 1
isNsfw = true
}

View File

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

View File

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

View File

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

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 {
extName = 'Union Mangas'
extClass = '.UnionMangasFactory'
extVersionCode = 6
extVersionCode = 5
isNsfw = true
}

View File

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

View File

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

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'
extClass = '.MangaNoon'
themePkg = 'mangathemesia'
baseUrl = 'https://manjanoon.co'
overrideVersionCode = 3
isNsfw = false
baseUrl = 'https://manjanoon.org'
overrideVersionCode = 2
}
apply from: "$rootDir/common.gradle"

View File

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

View File

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

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 :
MangaThemesia(
"MangaSwat",
"https://maxlevelteam.com",
"https://t1manga.com",
"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 {
extName = 'Vortex Scans'
extClass = '.VortexScans'
themePkg = 'iken'
overrideVersionCode = 33
extVersionCode = 33
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.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)

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

View File

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

View File

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

View File

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

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 {
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"

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 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()

View File

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

View File

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

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