Compare commits
83 Commits
01e67374ec
...
ac57f5e3dd
Author | SHA1 | Date |
---|---|---|
bapeey | ac57f5e3dd | |
Yush0DAN | 9f307df515 | |
KenjieDec | 1c9ae26c1e | |
aoba-seragakii | 1f3c64e5c5 | |
bapeey | c58f4c3449 | |
bapeey | 55cad2ee8f | |
Evrey | f8a94f9717 | |
Nyarunaa | ae10943664 | |
AwkwardPeak7 | 2c6e3b45fd | |
bapeey | 4c68d5f8d3 | |
Chopper | c12d0d302f | |
Chopper | 0e9d4b5c3d | |
Chopper | 94539a9923 | |
Chopper | 888ec79b29 | |
bapeey | 637d3a6cdd | |
bapeey | a1252ffd93 | |
bapeey | 7cfd7eecc9 | |
AwkwardPeak7 | 7c31254a7d | |
inipew | e07b44263d | |
Chopper | 0cda850385 | |
KenjieDec | 14c5eec0de | |
Vetle Ledaal | b3a4bf4697 | |
AwkwardPeak7 | 2be998797f | |
Vetle Ledaal | 3908c908ca | |
Vetle Ledaal | 33b9118f16 | |
Vetle Ledaal | a4db6edd2c | |
Secozzi | e380f9b974 | |
Vetle Ledaal | ff29d26ace | |
Vetle Ledaal | 77943d4f37 | |
Vetle Ledaal | 46fbc6a591 | |
AwkwardPeak7 | 16eff66f2e | |
Chopper | 3786ec8cfc | |
Chopper | c23c65c165 | |
Chopper | a5ff37e47a | |
bapeey | e7b098cdfe | |
inipew | ba8f7ac4b4 | |
AwkwardPeak7 | 36b5061699 | |
Yush0DAN | 476e950291 | |
Vetle Ledaal | 682bbb4703 | |
Vetle Ledaal | fe4676497a | |
AwkwardPeak7 | ec59467da4 | |
AwkwardPeak7 | 424021dac5 | |
AwkwardPeak7 | 48e449a67e | |
Chopper | 704af6a046 | |
Chopper | 4206c918bf | |
Chopper | e5815aaf29 | |
bapeey | 3b2fc2be9e | |
Chopper | bd4df286af | |
nedius | 1ca13b4697 | |
Chopper | b28987c34c | |
Chopper | 3249f02716 | |
AwkwardPeak7 | fec86f2276 | |
AwkwardPeak7 | d741e353e7 | |
AwkwardPeak7 | 1a3acc77d3 | |
AwkwardPeak7 | e721db72a2 | |
KenjieDec | 734c7a1e85 | |
KenjieDec | 2ef807ca07 | |
Chopper | cb508becbb | |
Chopper | a53cb1bd8d | |
Chopper | 89308ebcac | |
Chopper | 8c648b7a74 | |
Chopper | b83956c970 | |
KenjieDec | 4754f27a16 | |
bapeey | 02ebd645ad | |
Chopper | 0391029417 | |
Chopper | ef6466408b | |
AwkwardPeak7 | 17ef845943 | |
AwkwardPeak7 | 29e4234bb8 | |
AwkwardPeak7 | 5393c9dd11 | |
AwkwardPeak7 | 07546397ac | |
Roman | c9280eb5b2 | |
KenjieDec | 41812dd97b | |
renovate[bot] | 7257dc89a8 | |
Chopper | 73b30510f5 | |
Chopper | 0f91d36deb | |
Chopper | 6c1d3fb563 | |
kana-shii | dccd4920ec | |
Chopper | 133cb56ef3 | |
Chopper | 61988cdfe5 | |
Chopper | a1b627001f | |
bapeey | 3c13e2f1ec | |
bapeey | e81fe09571 | |
bapeey | 990ea76c28 |
|
@ -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.8-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 3
|
baseVersionCode = 4
|
||||||
|
|
|
@ -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 img").mapIndexed { i, it ->
|
return document.select("div.post-body a:has(> img)").mapIndexed { i, it ->
|
||||||
Page(i, imageUrl = it.absUrl("src"))
|
Page(i, imageUrl = it.absUrl("href"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 24
|
baseVersionCode = 25
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:i18n"))
|
api(project(":lib:i18n"))
|
||||||
|
|
|
@ -225,13 +225,13 @@ abstract class HeanCms(
|
||||||
throw Exception(intl.format("url_changed_error", name, name))
|
throw Exception(intl.format("url_changed_error", name, name))
|
||||||
}
|
}
|
||||||
|
|
||||||
val seriesId = manga.url.substringAfterLast("#")
|
val seriesSlug = manga.url.substringAfterLast("/").substringBefore("#")
|
||||||
|
|
||||||
val apiHeaders = headersBuilder()
|
val apiHeaders = headersBuilder()
|
||||||
.add("Accept", ACCEPT_JSON)
|
.add("Accept", ACCEPT_JSON)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return GET("$apiUrl/series/id/$seriesId", apiHeaders)
|
return GET("$apiUrl/series/$seriesSlug", apiHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.arvenscans
|
package eu.kanade.tachiyomi.multisrc.iken
|
||||||
|
|
||||||
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(
|
||||||
val id: Int,
|
private 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,
|
||||||
private val genres: List<Name>? = emptyList(),
|
val genres: List<Genre> = emptyList(),
|
||||||
) {
|
) {
|
||||||
fun toSManga(baseUrl: String) = SManga.create().apply {
|
fun toSManga(baseUrl: String) = SManga.create().apply {
|
||||||
url = "$slug#$id"
|
url = "$slug#$id"
|
||||||
|
@ -70,10 +70,16 @@ class Manga(
|
||||||
"MANHWA" -> add("Manhwa")
|
"MANHWA" -> add("Manhwa")
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
genres?.forEach { add(it.name) }
|
genres.forEach { add(it.name) }
|
||||||
}.distinct().joinToString()
|
}.distinct().joinToString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Genre(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Name(val name: String)
|
class Name(val name: String)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.arvenscans
|
package eu.kanade.tachiyomi.multisrc.iken
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
@ -65,37 +65,8 @@ class TypeFilter : SelectFilter(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class GenreFilter : CheckBoxGroup(
|
class GenreFilter(genres: List<Pair<String, String>>) : CheckBoxGroup(
|
||||||
"Genres",
|
"Genres",
|
||||||
"genreIds",
|
"genreIds",
|
||||||
listOf(
|
genres,
|
||||||
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"),
|
|
||||||
),
|
|
||||||
)
|
)
|
|
@ -0,0 +1,153 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.iken
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
abstract class Iken(
|
||||||
|
override val name: String,
|
||||||
|
override val lang: String,
|
||||||
|
override val baseUrl: String,
|
||||||
|
) : HttpSource() {
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
|
private val json by injectLazy<Json>()
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.set("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
private var genres = emptyList<Pair<String, String>>()
|
||||||
|
private val titleCache by lazy {
|
||||||
|
val response = client.newCall(GET("$baseUrl/api/query?perPage=9999", headers)).execute()
|
||||||
|
val data = response.parseAs<SearchResponse>()
|
||||||
|
|
||||||
|
data.posts
|
||||||
|
.filterNot { it.isNovel }
|
||||||
|
.also { posts ->
|
||||||
|
genres = posts.flatMap {
|
||||||
|
it.genres.map { genre ->
|
||||||
|
genre.name to genre.id.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.associateBy { it.slug }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/home", headers)
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val slugs = document.select("div:contains(Popular) + div.swiper div.manga-swipe > a")
|
||||||
|
.map { it.absUrl("href").substringAfterLast("/series/") }
|
||||||
|
|
||||||
|
val entries = slugs.mapNotNull {
|
||||||
|
titleCache[it]?.toSManga(baseUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(entries, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", getFilterList())
|
||||||
|
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = "$baseUrl/api/query".toHttpUrl().newBuilder().apply {
|
||||||
|
addQueryParameter("page", page.toString())
|
||||||
|
addQueryParameter("perPage", perPage.toString())
|
||||||
|
addQueryParameter("searchTerm", query.trim())
|
||||||
|
filters.filterIsInstance<UrlPartFilter>().forEach {
|
||||||
|
it.addUrlParameter(this)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val data = response.parseAs<SearchResponse>()
|
||||||
|
val page = response.request.url.queryParameter("page")!!.toInt()
|
||||||
|
|
||||||
|
val entries = data.posts
|
||||||
|
.filterNot { it.isNovel }
|
||||||
|
.map { it.toSManga(baseUrl) }
|
||||||
|
|
||||||
|
val hasNextPage = data.totalCount > (page * perPage)
|
||||||
|
|
||||||
|
return MangasPage(entries, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
StatusFilter(),
|
||||||
|
TypeFilter(),
|
||||||
|
GenreFilter(genres),
|
||||||
|
Filter.Header("Open popular mangas if genre filter is empty"),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
|
val id = manga.url.substringAfterLast("#")
|
||||||
|
val url = "$baseUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid="
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMangaUrl(manga: SManga): String {
|
||||||
|
val slug = manga.url.substringBeforeLast("#")
|
||||||
|
|
||||||
|
return "$baseUrl/series/$slug"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
val data = response.parseAs<Post<Manga>>()
|
||||||
|
|
||||||
|
assert(!data.post.isNovel) { "Novels are unsupported" }
|
||||||
|
|
||||||
|
// genres are only returned in search call
|
||||||
|
// and not when fetching details
|
||||||
|
return data.post.toSManga(baseUrl).apply {
|
||||||
|
genre = titleCache[data.post.slug]?.getGenres()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val data = response.parseAs<Post<ChapterListResponse>>()
|
||||||
|
|
||||||
|
assert(!data.post.isNovel) { "Novels are unsupported" }
|
||||||
|
|
||||||
|
return data.post.chapters
|
||||||
|
.filter { it.isPublic() }
|
||||||
|
.map { it.toSChapter(data.post.slug) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
return document.select("main section > img").mapIndexed { idx, img ->
|
||||||
|
Page(idx, imageUrl = img.absUrl("src"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response) =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T =
|
||||||
|
json.decodeFromString(body.string())
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val perPage = 18
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 28
|
baseVersionCode = 30
|
||||||
|
|
|
@ -188,7 +188,11 @@ abstract class LibGroup(
|
||||||
} else {
|
} else {
|
||||||
it.replace("\\", "")
|
it.replace("\\", "")
|
||||||
}
|
}
|
||||||
returnValue = str.parseAs<AuthToken>()
|
str.parseAs<AuthToken>().let { auth ->
|
||||||
|
if (auth.isValid()) {
|
||||||
|
returnValue = auth
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
latch.countDown()
|
latch.countDown()
|
||||||
}
|
}
|
||||||
|
@ -324,7 +328,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.itemNumber }) {
|
if (chapters.any { chpIt -> chpIt.chapter_number == it.value.number.toFloat() }) {
|
||||||
chapters.add(it.value.toSChapter(slugUrl, currentBranch.value.branchId, isScanUser()))
|
chapters.add(it.value.toSChapter(slugUrl, currentBranch.value.branchId, isScanUser()))
|
||||||
}
|
}
|
||||||
} else if (sortingList == "ms_combining") { // ms_combining
|
} else if (sortingList == "ms_combining") { // ms_combining
|
||||||
|
|
|
@ -202,7 +202,6 @@ 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(
|
||||||
|
@ -241,7 +240,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 = itemNumber ?: -1f
|
chapter_number = number.toFloat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,8 +266,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(
|
||||||
|
@ -283,13 +282,15 @@ 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
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 16 KiB |
|
@ -1,7 +1,8 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Galaxy'
|
extName = 'Galaxy'
|
||||||
extClass = '.GalaxyFactory'
|
extClass = '.GalaxyFactory'
|
||||||
extVersionCode = 2
|
extVersionCode = 4
|
||||||
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
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 {
|
||||||
|
|
||||||
|
@ -8,8 +15,53 @@ class GalaxyFactory : SourceFactory {
|
||||||
override val id = 2602904659965278831
|
override val id = 2602904659965278831
|
||||||
}
|
}
|
||||||
|
|
||||||
class GalaxyManga : Galaxy("Galaxy Manga", "https://ayoub-zrr.xyz", "ar") {
|
class GalaxyManga :
|
||||||
|
Galaxy("Galaxy Manga", "https://galaxymanga.net", "ar"),
|
||||||
|
ConfigurableSource {
|
||||||
override val id = 2729515745226258240
|
override val id = 2729515745226258240
|
||||||
|
|
||||||
|
override val baseUrl by lazy { getPrefBaseUrl() }
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val RESTART_APP = ".لتطبيق الإعدادات الجديدة أعد تشغيل التطبيق"
|
||||||
|
private const val BASE_URL_PREF_TITLE = "تعديل الرابط"
|
||||||
|
private const val BASE_URL_PREF = "overrideBaseUrl"
|
||||||
|
private const val BASE_URL_PREF_SUMMARY = ".للاستخدام المؤقت. تحديث التطبيق سيؤدي الى حذف الإعدادات"
|
||||||
|
private const val DEFAULT_BASE_URL_PREF = "defaultBaseUrl"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
val baseUrlPref = androidx.preference.EditTextPreference(screen.context).apply {
|
||||||
|
key = BASE_URL_PREF
|
||||||
|
title = BASE_URL_PREF_TITLE
|
||||||
|
summary = BASE_URL_PREF_SUMMARY
|
||||||
|
this.setDefaultValue(super.baseUrl)
|
||||||
|
dialogTitle = BASE_URL_PREF_TITLE
|
||||||
|
dialogMessage = "Default: ${super.baseUrl}"
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, _ ->
|
||||||
|
Toast.makeText(screen.context, RESTART_APP, Toast.LENGTH_LONG).show()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
screen.addPreference(baseUrlPref)
|
||||||
|
}
|
||||||
|
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, super.baseUrl)!!
|
||||||
|
|
||||||
|
init {
|
||||||
|
preferences.getString(DEFAULT_BASE_URL_PREF, null).let { prefDefaultBaseUrl ->
|
||||||
|
if (prefDefaultBaseUrl != super.baseUrl) {
|
||||||
|
preferences.edit()
|
||||||
|
.putString(BASE_URL_PREF, super.baseUrl)
|
||||||
|
.putString(DEFAULT_BASE_URL_PREF, super.baseUrl)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createSources() = listOf(
|
override fun createSources() = listOf(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Hitomi'
|
extName = 'Hitomi'
|
||||||
extClass = '.HitomiFactory'
|
extClass = '.HitomiFactory'
|
||||||
extVersionCode = 31
|
extVersionCode = 32
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
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
|
||||||
|
@ -19,9 +24,14 @@ 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
|
||||||
|
@ -30,13 +40,14 @@ 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() {
|
) : HttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
override val name = "Hitomi"
|
override val name = "Hitomi"
|
||||||
|
|
||||||
|
@ -50,7 +61,14 @@ class Hitomi(
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
override val client = network.cloudflareClient
|
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 fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.set("referer", "$baseUrl/")
|
.set("referer", "$baseUrl/")
|
||||||
|
@ -488,7 +506,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 }
|
author = groups?.joinToString { it.formatted } ?: artists?.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 {
|
||||||
|
@ -567,14 +585,25 @@ 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",
|
||||||
"https://${subDomain}a.$domain/webp/$commonId$imageId/$hash.webp",
|
imageUrl,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -657,6 +686,45 @@ 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()
|
||||||
|
@ -664,4 +732,8 @@ class Hitomi(
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
||||||
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
|
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PREF_IMAGETYPE = "pref_image_type"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,9 @@ class Gallery(
|
||||||
@Serializable
|
@Serializable
|
||||||
class ImageFile(
|
class ImageFile(
|
||||||
val hash: String,
|
val hash: String,
|
||||||
|
val haswebp: Int,
|
||||||
|
val hasavif: Int,
|
||||||
|
val hasjxl: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<application android:icon="@mipmap/ic_launcher">
|
<application android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
android:name=".all.ninenineninehentai.NineNineNineHentaiUrlActivity"
|
android:name=".all.ninenineninehentai.AnimeHUrlActivity"
|
||||||
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="999hentai.net"/>
|
<data android:host="animeh.to"/>
|
||||||
<data android:scheme="https"/>
|
<data android:scheme="https"/>
|
||||||
<data android:pathPattern="/hchapter/..*"/>
|
<data android:pathPattern="/hchapter/..*"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = '999Hentai'
|
extName = 'AnimeH'
|
||||||
extClass = '.NineNineNineHentaiFactory'
|
extClass = '.AnimeHFactory'
|
||||||
extVersionCode = 6
|
extVersionCode = 7
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 47 KiB |
|
@ -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 NineNineNineHentai(
|
open class AnimeH(
|
||||||
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 = "999Hentai"
|
override val name = "AnimeH"
|
||||||
|
|
||||||
override val baseUrl = "https://999hentai.net"
|
override val baseUrl = "https://animeh.to"
|
||||||
|
|
||||||
private val apiUrl = "https://hapi.999hentai.net/api"
|
private val apiUrl = "https://api.animeh.to/api"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
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 }
|
|
@ -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 NineNineNineHentaiUrlActivity : Activity() {
|
class AnimeHUrlActivity : 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 NineNineNineHentaiUrlActivity : 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", "${NineNineNineHentai.SEARCH_PREFIX}$id")
|
putExtra("query", "${AnimeH.SEARCH_PREFIX}$id")
|
||||||
putExtra("filter", packageName)
|
putExtra("filter", packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startActivity(mainIntent)
|
startActivity(mainIntent)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Log.e("999HentaiUrlActivity", e.toString())
|
Log.e("AnimeHUrlActivity", e.toString())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.e("999HentaiUrlActivity", "could not parse uri from intent $intent")
|
Log.e("AnimeHUrlActivity", "could not parse uri from intent $intent")
|
||||||
}
|
}
|
||||||
|
|
||||||
finish()
|
finish()
|
|
@ -1,13 +0,0 @@
|
||||||
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"),
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<activity
|
||||||
|
android:name=".all.pandachaika.PandaChaikaUrlActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="panda.chaika.moe"
|
||||||
|
android:pathPattern="/archive/..*"
|
||||||
|
android:scheme="https"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'PandaChaika'
|
extName = 'PandaChaika'
|
||||||
extClass = '.PandaChaikaFactory'
|
extClass = '.PandaChaikaFactory'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
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
|
||||||
|
@ -42,6 +44,9 @@ 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)
|
||||||
|
@ -73,6 +78,76 @@ 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>()
|
||||||
|
|
||||||
|
@ -250,4 +325,11 @@ class PandaChaika(
|
||||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||||
override fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException()
|
override fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException()
|
||||||
override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
|
override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PREFIX_ID_SEARCH = "id:"
|
||||||
|
const val PREFIX_FAK_ID_SEARCH = "fakku:"
|
||||||
|
const val PREFIX_EHEN_ID_SEARCH = "ehentai:"
|
||||||
|
const val PREFIX_SOURCE_SEARCH = "source:"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
val dateReformat = SimpleDateFormat("EEEE, d MMM yyyy HH:mm (z)", Locale.ENGLISH)
|
val dateReformat = SimpleDateFormat("EEEE, d MMM yyyy HH:mm (z)", Locale.ENGLISH)
|
||||||
fun filterTags(include: String = "", exclude: List<String> = emptyList(), tags: List<String>): String {
|
fun filterTags(include: String = "", exclude: List<String> = emptyList(), tags: List<String>): String? {
|
||||||
return tags.filter { it.startsWith("$include:") && exclude.none { substring -> it.startsWith("$substring:") } }
|
return tags.filter { it.startsWith("$include:") && exclude.none { substring -> it.startsWith("$substring:") } }
|
||||||
.joinToString {
|
.joinToString {
|
||||||
it.substringAfter(":").replace("_", " ").split(" ").joinToString(" ") { s ->
|
it.substringAfter(":").replace("_", " ").split(" ").joinToString(" ") { s ->
|
||||||
|
@ -16,13 +16,13 @@ fun filterTags(include: String = "", exclude: List<String> = emptyList(), tags:
|
||||||
if (sr.isLowerCase()) sr.titlecase(Locale.getDefault()) else sr.toString()
|
if (sr.isLowerCase()) sr.titlecase(Locale.getDefault()) else sr.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}.takeIf { it.isNotBlank() }
|
||||||
}
|
}
|
||||||
fun getReadableSize(bytes: Double): String {
|
fun getReadableSize(bytes: Double): String {
|
||||||
return when {
|
return when {
|
||||||
bytes >= 300 * 1024 * 1024 -> "${"%.2f".format(bytes / (1024.0 * 1024.0 * 1024.0))} GB"
|
bytes >= 300 * 1000 * 1000 -> "${"%.2f".format(bytes / (1000.0 * 1000.0 * 1000.0))} GB"
|
||||||
bytes >= 100 * 1024 -> "${"%.2f".format(bytes / (1024.0 * 1024.0))} MB"
|
bytes >= 100 * 1000 -> "${"%.2f".format(bytes / (1000.0 * 1000.0))} MB"
|
||||||
bytes >= 1024 -> "${"%.2f".format(bytes / (1024.0))} KB"
|
bytes >= 1000 -> "${"%.2f".format(bytes / (1000.0))} kB"
|
||||||
else -> "$bytes B"
|
else -> "$bytes B"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,13 +31,14 @@ 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,
|
||||||
private val id: Int,
|
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,
|
||||||
|
@ -50,35 +51,47 @@ 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.ifEmpty { artists }
|
author = groups ?: 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.takeIf { it.isNotBlank() }?.let {
|
publishers?.let {
|
||||||
append("Publishers: ", it, "\n\n")
|
append("Publishers: ", it, "\n")
|
||||||
}
|
}
|
||||||
parodies.takeIf { it.isNotBlank() }?.let {
|
append("\n")
|
||||||
append("Parodies: ", it, "\n\n")
|
|
||||||
|
parodies?.let {
|
||||||
|
append("Parodies: ", it, "\n")
|
||||||
|
appended = true
|
||||||
}
|
}
|
||||||
male.takeIf { it.isNotBlank() }?.let {
|
characters?.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.takeIf { it.isNotBlank() }?.let {
|
female?.let {
|
||||||
append("Female tags: ", it, "\n\n")
|
append("Female tags: ", it, "\n\n")
|
||||||
}
|
}
|
||||||
others.takeIf { it.isNotBlank() }?.let {
|
others?.let {
|
||||||
append("Other tags: ", it, "\n\n")
|
append("Other tags: ", it, "\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
title_jpn?.let { append("Japanese Title: ", it, "\n") }
|
title_jpn?.takeIf { it.isNotEmpty() }?.let { append("Japanese Title: ", it, "\n") }
|
||||||
append("Pages: ", filecount, "\n")
|
append("Pages: ", filecount, "\n")
|
||||||
append("File Size: ", getReadableSize(filesize), "\n")
|
append("File Size: ", getReadableSize(filesize), "\n")
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ 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"),
|
||||||
|
@ -52,11 +53,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_date"),
|
Pair("Posted Date", "posted"),
|
||||||
Pair("Title", "title"),
|
Pair("Title", "title"),
|
||||||
Pair("Japanese Title", "title_jpn"),
|
Pair("Japanese Title", "title_jpn"),
|
||||||
Pair("Rating", "rating"),
|
Pair("Rating", "rating"),
|
||||||
Pair("Images", "images"),
|
Pair("Images", "filecount"),
|
||||||
Pair("File Size", "size"),
|
Pair("File Size", "filesize"),
|
||||||
Pair("Category", "category"),
|
Pair("Category", "category"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.pandachaika
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
class PandaChaikaUrlActivity : Activity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val pathSegments = intent?.data?.pathSegments
|
||||||
|
if (pathSegments != null && pathSegments.size > 2) {
|
||||||
|
val id = "${pathSegments[1]}/${pathSegments[2]}"
|
||||||
|
val mainIntent = Intent().apply {
|
||||||
|
action = "eu.kanade.tachiyomi.SEARCH"
|
||||||
|
putExtra("query", "${PandaChaika.PREFIX_ID_SEARCH}$id")
|
||||||
|
putExtra("filter", packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivity(mainIntent)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Log.e("KoharuUrlActivity", "Could not start activity", e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e("KoharuUrlActivity", "Could not parse URI from intent $intent")
|
||||||
|
}
|
||||||
|
|
||||||
|
finish()
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Union Mangas'
|
extName = 'Union Mangas'
|
||||||
extClass = '.UnionMangasFactory'
|
extClass = '.UnionMangasFactory'
|
||||||
extVersionCode = 5
|
extVersionCode = 6
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -203,7 +203,7 @@ class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SEARCH_PREFIX = "slug:"
|
const val SEARCH_PREFIX = "slug:"
|
||||||
val apiUrl = "https://app.unionmanga.xyz/api"
|
val apiUrl = "https://api.novelfull.us/api"
|
||||||
val oldApiUrl = "https://api.unionmanga.xyz"
|
val oldApiUrl = "https://api.unionmanga.xyz"
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Crow Scans'
|
extName = 'Hadess'
|
||||||
extClass = '.CrowScans'
|
extClass = '.Hadess'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://crowscans.com'
|
baseUrl = 'https://www.hadess.xyz'
|
||||||
overrideVersionCode = 0
|
overrideVersionCode = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 26 KiB |
|
@ -1,12 +0,0 @@
|
||||||
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")),
|
|
||||||
)
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ar.crowscans
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class Hadess : Madara(
|
||||||
|
"Hadess",
|
||||||
|
"https://www.hadess.xyz",
|
||||||
|
"ar",
|
||||||
|
dateFormat = SimpleDateFormat("dd MMMM، yyyy", Locale("ar")),
|
||||||
|
) {
|
||||||
|
override val versionId = 2
|
||||||
|
|
||||||
|
override val client = super.client.newBuilder()
|
||||||
|
.rateLimit(3)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override val useNewChapterEndpoint = true
|
||||||
|
|
||||||
|
override val useLoadMoreRequest = LoadMoreStrategy.Always
|
||||||
|
|
||||||
|
override val mangaDetailsSelectorStatus =
|
||||||
|
".summary-heading:contains(الحالة) + ${super.mangaDetailsSelectorStatus}"
|
||||||
|
}
|
|
@ -2,8 +2,9 @@ ext {
|
||||||
extName = 'MangaNoon'
|
extName = 'MangaNoon'
|
||||||
extClass = '.MangaNoon'
|
extClass = '.MangaNoon'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://manjanoon.org'
|
baseUrl = 'https://manjanoon.co'
|
||||||
overrideVersionCode = 2
|
overrideVersionCode = 3
|
||||||
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,12 +1,89 @@
|
||||||
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 java.text.SimpleDateFormat
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import java.util.Locale
|
import org.jsoup.nodes.Element
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
class MangaNoon : MangaThemesia(
|
class MangaNoon : MangaThemesia(
|
||||||
"مانجا نون",
|
"مانجا نون",
|
||||||
"https://manjanoon.org",
|
"https://manjanoon.co",
|
||||||
"ar",
|
"ar",
|
||||||
dateFormat = SimpleDateFormat("MMM d, yyy", Locale("ar")),
|
) {
|
||||||
)
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
|
return super.chapterFromElement(element).apply {
|
||||||
|
date_upload = element.selectFirst(".chapterdate")?.text().parseChapterDate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// From Galaxy
|
||||||
|
override fun String?.parseChapterDate(): Long {
|
||||||
|
this ?: return 0L
|
||||||
|
|
||||||
|
val number = Regex("""(\d+)""").find(this)?.value?.toIntOrNull() ?: 0
|
||||||
|
val cal = Calendar.getInstance()
|
||||||
|
|
||||||
|
return when {
|
||||||
|
listOf("second", "ثانية").any { contains(it, true) } -> {
|
||||||
|
cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
contains("دقيقتين", true) -> {
|
||||||
|
cal.apply { add(Calendar.MINUTE, -2) }.timeInMillis
|
||||||
|
}
|
||||||
|
listOf("minute", "دقائق").any { contains(it, true) } -> {
|
||||||
|
cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
contains("ساعتان", true) -> {
|
||||||
|
cal.apply { add(Calendar.HOUR, -2) }.timeInMillis
|
||||||
|
}
|
||||||
|
listOf("hour", "ساعات").any { contains(it, true) } -> {
|
||||||
|
cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
contains("يوم", true) -> {
|
||||||
|
cal.apply { add(Calendar.DAY_OF_YEAR, -1) }.timeInMillis
|
||||||
|
}
|
||||||
|
contains("يومين", true) -> {
|
||||||
|
cal.apply { add(Calendar.DAY_OF_YEAR, -2) }.timeInMillis
|
||||||
|
}
|
||||||
|
listOf("day", "أيام").any { contains(it, true) } -> {
|
||||||
|
cal.apply { add(Calendar.DAY_OF_YEAR, -number) }.timeInMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
contains("أسبوع", true) -> {
|
||||||
|
cal.apply { add(Calendar.WEEK_OF_YEAR, -1) }.timeInMillis
|
||||||
|
}
|
||||||
|
contains("أسبوعين", true) -> {
|
||||||
|
cal.apply { add(Calendar.WEEK_OF_YEAR, -2) }.timeInMillis
|
||||||
|
}
|
||||||
|
listOf("week", "أسابيع").any { contains(it, true) } -> {
|
||||||
|
cal.apply { add(Calendar.WEEK_OF_YEAR, -number) }.timeInMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
contains("شهر", true) -> {
|
||||||
|
cal.apply { add(Calendar.MONTH, -1) }.timeInMillis
|
||||||
|
}
|
||||||
|
contains("شهرين", true) -> {
|
||||||
|
cal.apply { add(Calendar.MONTH, -2) }.timeInMillis
|
||||||
|
}
|
||||||
|
listOf("month", "أشهر").any { contains(it, true) } -> {
|
||||||
|
cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
contains("سنة", true) -> {
|
||||||
|
cal.apply { add(Calendar.YEAR, -1) }.timeInMillis
|
||||||
|
}
|
||||||
|
contains("سنتان", true) -> {
|
||||||
|
cal.apply { add(Calendar.YEAR, -2) }.timeInMillis
|
||||||
|
}
|
||||||
|
listOf("year", "سنوات").any { contains(it, true) } -> {
|
||||||
|
cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> 0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,8 +2,8 @@ ext {
|
||||||
extName = 'MangaSwat'
|
extName = 'MangaSwat'
|
||||||
extClass = '.MangaSwat'
|
extClass = '.MangaSwat'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://t1manga.com'
|
baseUrl = 'https://maxlevelteam.com'
|
||||||
overrideVersionCode = 19
|
overrideVersionCode = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 16 KiB |
|
@ -24,7 +24,7 @@ import java.util.Locale
|
||||||
class MangaSwat :
|
class MangaSwat :
|
||||||
MangaThemesia(
|
MangaThemesia(
|
||||||
"MangaSwat",
|
"MangaSwat",
|
||||||
"https://t1manga.com",
|
"https://maxlevelteam.com",
|
||||||
"ar",
|
"ar",
|
||||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
ext {
|
||||||
|
extName = 'NoonScan'
|
||||||
|
extClass = '.NoonScan'
|
||||||
|
themePkg = 'mangathemesia'
|
||||||
|
baseUrl = 'https://noonscan.com'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
isNsfw = false
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 43 KiB |
|
@ -0,0 +1,12 @@
|
||||||
|
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")),
|
||||||
|
)
|
|
@ -1,9 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'Novels Town'
|
|
||||||
extClass = '.NovelsTown'
|
|
||||||
themePkg = 'madara'
|
|
||||||
baseUrl = 'https://novelstown.com'
|
|
||||||
overrideVersionCode = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 19 KiB |
|
@ -1,7 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.novelstown
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
|
||||||
|
|
||||||
class NovelsTown : Madara("Novels Town", "https://novelstown.com", "ar") {
|
|
||||||
override val mangaSubString = "الاعمال"
|
|
||||||
}
|
|
|
@ -1,7 +1,8 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Vortex Scans'
|
extName = 'Vortex Scans'
|
||||||
extClass = '.VortexScans'
|
extClass = '.VortexScans'
|
||||||
extVersionCode = 33
|
themePkg = 'iken'
|
||||||
|
overrideVersionCode = 33
|
||||||
isNsfw = false
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,145 +1,9 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.arvenscans
|
package eu.kanade.tachiyomi.extension.en.arvenscans
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.multisrc.iken.Iken
|
||||||
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 : HttpSource() {
|
class VortexScans : Iken(
|
||||||
|
"Vortex Scans",
|
||||||
override val name = "Vortex Scans"
|
"en",
|
||||||
|
"https://vortexscans.org",
|
||||||
override val baseUrl = "https://vortexscans.org"
|
)
|
||||||
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient
|
|
||||||
|
|
||||||
private val json by injectLazy<Json>()
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
|
||||||
.set("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
private val titleCache by lazy {
|
|
||||||
val response = client.newCall(GET("$baseUrl/api/query?perPage=9999", headers)).execute()
|
|
||||||
val data = response.parseAs<SearchResponse>()
|
|
||||||
|
|
||||||
data.posts
|
|
||||||
.filterNot { it.isNovel }
|
|
||||||
.associateBy { it.slug }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/home", headers)
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
val slugs = document.select("div:contains(Popular) + div.swiper div.manga-swipe > a")
|
|
||||||
.map { it.absUrl("href").substringAfterLast("/series/") }
|
|
||||||
|
|
||||||
val entries = slugs.mapNotNull {
|
|
||||||
titleCache[it]?.toSManga(baseUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
return MangasPage(entries, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", getFilterList())
|
|
||||||
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = "$baseUrl/api/query".toHttpUrl().newBuilder().apply {
|
|
||||||
addQueryParameter("page", page.toString())
|
|
||||||
addQueryParameter("perPage", perPage.toString())
|
|
||||||
addQueryParameter("searchTerm", query.trim())
|
|
||||||
filters.filterIsInstance<UrlPartFilter>().forEach {
|
|
||||||
it.addUrlParameter(this)
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
val data = response.parseAs<SearchResponse>()
|
|
||||||
val page = response.request.url.queryParameter("page")!!.toInt()
|
|
||||||
|
|
||||||
val entries = data.posts
|
|
||||||
.filterNot { it.isNovel }
|
|
||||||
.map { it.toSManga(baseUrl) }
|
|
||||||
|
|
||||||
val hasNextPage = data.totalCount > (page * perPage)
|
|
||||||
|
|
||||||
return MangasPage(entries, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
|
||||||
StatusFilter(),
|
|
||||||
TypeFilter(),
|
|
||||||
GenreFilter(),
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
|
||||||
val id = manga.url.substringAfterLast("#")
|
|
||||||
val url = "$baseUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid="
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga): String {
|
|
||||||
val slug = manga.url.substringBeforeLast("#")
|
|
||||||
|
|
||||||
return "$baseUrl/series/$slug"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
|
||||||
val data = response.parseAs<Post<Manga>>()
|
|
||||||
|
|
||||||
assert(!data.post.isNovel) { "Novels are unsupported" }
|
|
||||||
|
|
||||||
// genres are only returned in search call
|
|
||||||
// and not when fetching details
|
|
||||||
return data.post.toSManga(baseUrl).apply {
|
|
||||||
genre = titleCache[data.post.slug]?.getGenres()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val data = response.parseAs<Post<ChapterListResponse>>()
|
|
||||||
|
|
||||||
assert(!data.post.isNovel) { "Novels are unsupported" }
|
|
||||||
|
|
||||||
return data.post.chapters
|
|
||||||
.filter { it.isPublic() }
|
|
||||||
.map { it.toSChapter(data.post.slug) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
return document.select("main section > img").mapIndexed { idx, img ->
|
|
||||||
Page(idx, imageUrl = img.absUrl("src"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response) =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T =
|
|
||||||
json.decodeFromString(body.string())
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val perPage = 18
|
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Asura Scans'
|
extName = 'Asura Scans'
|
||||||
extClass = '.AsuraScans'
|
extClass = '.AsuraScans'
|
||||||
themePkg = 'mangathemesia'
|
extVersionCode = 36
|
||||||
baseUrl = 'https://asuracomic.net'
|
|
||||||
overrideVersionCode = 4
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,21 +1,50 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.asurascans
|
package eu.kanade.tachiyomi.extension.en.asurascans
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesiaAlt
|
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.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 {
|
||||||
|
@ -25,47 +54,256 @@ class AsuraScans : MangaThemesiaAlt(
|
||||||
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()
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
|
.rateLimit(1, 3)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override val seriesDescriptionSelector = "div.desc p, div.entry-content p, div[itemprop=description]:not(:has(p))"
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
override val seriesArtistSelector = ".fmed b:contains(artist)+span, .infox span:contains(artist)"
|
.add("Referer", "$baseUrl/")
|
||||||
override val seriesAuthorSelector = ".fmed b:contains(author)+span, .infox span:contains(author)"
|
|
||||||
|
|
||||||
override val pageSelector = "div.rdminimal > img, div.rdminimal > p > img, div.rdminimal > a > img, div.rdminimal > p > a > img, " +
|
override fun popularMangaRequest(page: Int): Request =
|
||||||
"div.rdminimal > noscript > img, div.rdminimal > p > noscript > img, div.rdminimal > a > noscript > img, div.rdminimal > p > a > noscript > img"
|
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 fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val request = super.searchMangaRequest(page, query, filters)
|
val url = "$baseUrl/series".toHttpUrl().newBuilder()
|
||||||
if (query.isBlank()) return request
|
|
||||||
|
|
||||||
val url = request.url.newBuilder()
|
url.addQueryParameter("page", page.toString())
|
||||||
.addPathSegment("page/$page/")
|
|
||||||
.removeAllQueryParameters("page")
|
|
||||||
.removeAllQueryParameters("title")
|
|
||||||
.addQueryParameter("s", query)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return request.newBuilder()
|
if (query.isNotBlank()) {
|
||||||
.url(url)
|
url.addQueryParameter("name", query)
|
||||||
.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(pageSelector)
|
return document.select("div > img[alt=chapter]").mapIndexed { i, element ->
|
||||||
.filterNot { it.attr("src").isNullOrEmpty() }
|
Page(i, imageUrl = element.attr("abs:src"))
|
||||||
.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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
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,
|
||||||
|
)
|
|
@ -0,0 +1,17 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.asurascans
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
|
||||||
|
class Genre(title: String, val id: Int) : Filter.CheckBox(title)
|
||||||
|
class GenreFilter(title: String, genres: List<Genre>) : Filter.Group<Genre>(title, genres)
|
||||||
|
|
||||||
|
class StatusFilter(title: String, statuses: List<Pair<String, String>>) : UriPartFilter(title, statuses)
|
||||||
|
|
||||||
|
class TypeFilter(title: String, types: List<Pair<String, String>>) : UriPartFilter(title, types)
|
||||||
|
|
||||||
|
class OrderFilter(title: String, orders: List<Pair<String, String>>) : UriPartFilter(title, orders)
|
||||||
|
|
||||||
|
open class UriPartFilter(displayName: String, val vals: List<Pair<String, String>>) :
|
||||||
|
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||||
|
fun toUriPart() = vals[state].second
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Blazescans'
|
extName = 'Fury Manga'
|
||||||
extClass = '.Blazescans'
|
extClass = '.FuryManga'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://blazetoon.com'
|
baseUrl = 'https://furymanga.com'
|
||||||
overrideVersionCode = 1
|
overrideVersionCode = 2
|
||||||
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 12 KiB |
|
@ -4,7 +4,14 @@ 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 Blazescans : MangaThemesia("Blazescans", "https://blazetoon.com", "en") {
|
class FuryManga : MangaThemesia(
|
||||||
|
"Fury Manga",
|
||||||
|
"https://furymanga.com",
|
||||||
|
"en",
|
||||||
|
"/comics",
|
||||||
|
) {
|
||||||
|
override val id = 3912200442923601567
|
||||||
|
|
||||||
override val client = super.client.newBuilder()
|
override val client = super.client.newBuilder()
|
||||||
.rateLimit(1, 2, TimeUnit.SECONDS)
|
.rateLimit(1, 2, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'ComicExtra'
|
extName = 'ComicExtra'
|
||||||
extClass = '.ComicExtra'
|
extClass = '.ComicExtra'
|
||||||
extVersionCode = 15
|
extVersionCode = 16
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -23,7 +23,7 @@ class ComicExtra : ParsedHttpSource() {
|
||||||
|
|
||||||
override val name = "ComicExtra"
|
override val name = "ComicExtra"
|
||||||
|
|
||||||
override val baseUrl = "https://comicextra.org"
|
override val baseUrl = "https://comixextra.com"
|
||||||
|
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Darths & Droids'
|
||||||
|
extClass = '.DarthsDroids'
|
||||||
|
extVersionCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 9.5 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 51 KiB |
|
@ -0,0 +1,247 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.darthsdroids
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import rx.Observable
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
// Dear Darths & Droids creators:
|
||||||
|
// I’m sorry if this extension causes too much traffic for your site.
|
||||||
|
// Unfortunately we can’t just download and use your Zip downloads.
|
||||||
|
// Shall problems arise, we’ll reduce the rate limit.
|
||||||
|
class DarthsDroids : HttpSource() {
|
||||||
|
override val name = "Darths & Droids"
|
||||||
|
override val baseUrl = "https://www.darthsanddroids.net"
|
||||||
|
override val lang = "en"
|
||||||
|
override val supportsLatest = false
|
||||||
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
|
.rateLimitHost(baseUrl.toHttpUrl(), 10, 1, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// Picks a thumbnail from the profile pictures of the »cast« pages:
|
||||||
|
// https://www.darthsanddroids.net/cast/
|
||||||
|
//
|
||||||
|
// Where possible, pick a thumbnail from the corresponding book’s
|
||||||
|
// cast page. Try to avoid having a character appear more than once
|
||||||
|
// as thumbnail, giving all main characters equal amounts of spotlight.
|
||||||
|
// Pick a character people would intuïtively associate with the
|
||||||
|
// corresponding film, like Qui-Gon for Phantom Menace or Leia for
|
||||||
|
// A New Hope.
|
||||||
|
//
|
||||||
|
// If a book doesn’t have its own cast page, try source a fitting
|
||||||
|
// profile picture from a different page. Avoid sourcing thumbnails
|
||||||
|
// from a different website.
|
||||||
|
private fun dndThumbnailUrlForTitle(nthManga: Int): String = when (nthManga) {
|
||||||
|
// The numbers are assigned in order of appearance of a book on the archive page.
|
||||||
|
0 -> "$baseUrl/cast/QuiGon.jpg" // D&D1
|
||||||
|
1 -> "$baseUrl/cast/Anakin2.jpg" // D&D2
|
||||||
|
2 -> "$baseUrl/cast/ObiWan3.jpg" // D&D3
|
||||||
|
3 -> "$baseUrl/cast/JarJar2.jpg" // JJ
|
||||||
|
4 -> "$baseUrl/cast/Leia4.jpg" // D&D4
|
||||||
|
5 -> "$baseUrl/cast/Han5.jpg" // D&D5
|
||||||
|
6 -> "$baseUrl/cast/Luke6.jpg" // D&D6
|
||||||
|
7 -> "$baseUrl/cast/Cassian.jpg" // R1
|
||||||
|
8 -> "$baseUrl/cast/C3PO4.jpg" // Muppets
|
||||||
|
9 -> "$baseUrl/cast/Finn7.jpg" // D&D7
|
||||||
|
10 -> "$baseUrl/cast/Han4.jpg" // Solo
|
||||||
|
11 -> "$baseUrl/cast/Hux8.jpg" // D&D8
|
||||||
|
// Just some nonsense fallback that screams »Star Wars« but is also so recognisably
|
||||||
|
// OT that one can understand it’s a mere fallback. Better thumbnails require an
|
||||||
|
// extension update.
|
||||||
|
else -> "$baseUrl/cast/Vader4.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dndManga(archiveUrl: String, mangaTitle: String, mangaStatus: Int, nthManga: Int): SManga = SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(archiveUrl)
|
||||||
|
thumbnail_url = dndThumbnailUrlForTitle(nthManga)
|
||||||
|
title = mangaTitle
|
||||||
|
author = "David Morgan-Mar & Co."
|
||||||
|
artist = "David Morgan-Mar & Co."
|
||||||
|
description = """What if Star Wars as we know it didn't exist, but instead the
|
||||||
|
|plot of the movies was being made up on the spot by players of
|
||||||
|
|a Tabletop Game?
|
||||||
|
|
|
||||||
|
|Well, for one, the results might actually make a lot more sense,
|
||||||
|
|from an out-of-story point of view…
|
||||||
|
""".trimMargin()
|
||||||
|
genre = "Campaign Comic, Comedy, Space Opera, Science Fiction"
|
||||||
|
status = mangaStatus
|
||||||
|
update_strategy = when (mangaStatus) {
|
||||||
|
SManga.COMPLETED -> UpdateStrategy.ONLY_FETCH_ONCE
|
||||||
|
else -> UpdateStrategy.ALWAYS_UPDATE
|
||||||
|
}
|
||||||
|
initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request =
|
||||||
|
GET("$baseUrl/archive.html", headers)
|
||||||
|
|
||||||
|
// The book and page archive feeds are rather special for this webcomic.
|
||||||
|
// The main archive page `/archive.html` is a combined feed for both,
|
||||||
|
// all previous and finished books, as well as all pages of the book that
|
||||||
|
// is currently releasing. Every finished book gets its own archive page
|
||||||
|
// like `/archive4.html` or `/archiveJJ.html` into which all page links
|
||||||
|
// are moved. So whatever book is currently releasing in `/archive.html`
|
||||||
|
// will eventually be moved into its own archive, and it’ll instead
|
||||||
|
// appear as a book-archive link in `/archive.html`.
|
||||||
|
//
|
||||||
|
// This means a few things:
|
||||||
|
// • The currently releasing book eventually changes its `url`!
|
||||||
|
// • The URL of the currently releasing book will be taken over by
|
||||||
|
// whichever new book comes next.
|
||||||
|
// • There is no deterministic way of guessing a book’s future
|
||||||
|
// archive name.
|
||||||
|
// ◦ This is especially apparent with the »Solo« book, which’s
|
||||||
|
// archive page is `/solo/`, while all others are `/archiveX.html`.
|
||||||
|
//
|
||||||
|
// So eventually, Tachiyomi & Co. will glitch out once a currently
|
||||||
|
// releasing book finishes. People will find the current book’s page
|
||||||
|
// feed to be empty. Even worse, they may find it starting anew with
|
||||||
|
// different pages. A manual refresh *should* change the book’s `url`
|
||||||
|
// to its new archive page, and all reading progress should be preserved.
|
||||||
|
// Then the user will have to manually add the new book to their library.
|
||||||
|
//
|
||||||
|
// The alternative would be to have a pseudo book »<Title> (ongoing)«
|
||||||
|
// that just disappears, being replaced by »<Title>«. But i think that’s
|
||||||
|
// even worse in terms of user experience. Maybe one day we’ll have new
|
||||||
|
// extension APIs for dealing with unique webcomic weirdnesses. ’cause
|
||||||
|
// trust me, there’s worse.
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val mainArchive = response.asJsoup()
|
||||||
|
val archiveData = mainArchive.select("div.text > table.text > tbody > tr")
|
||||||
|
|
||||||
|
val mangas = mutableListOf<SManga>()
|
||||||
|
var nextMangaTitle = name
|
||||||
|
var nthManga = 0
|
||||||
|
|
||||||
|
run stop@{
|
||||||
|
archiveData.forEach {
|
||||||
|
val maybeTitle = it.selectFirst("th")?.text()
|
||||||
|
if (maybeTitle != null) {
|
||||||
|
nextMangaTitle = "$name $maybeTitle"
|
||||||
|
} else {
|
||||||
|
val maybeArchive = it.selectFirst("""td[colspan="3"] > a""")?.absUrl("href")
|
||||||
|
if (maybeArchive != null) {
|
||||||
|
mangas.add(dndManga(maybeArchive, nextMangaTitle, SManga.COMPLETED, nthManga++))
|
||||||
|
} else {
|
||||||
|
// We reached the end, assuming the page layout stays consistent beyond D&D8.
|
||||||
|
// Thus, we append our final manga with this current page as its archive.
|
||||||
|
// Unfortunately this means we will needlessly fetch this page twice.
|
||||||
|
mangas.add(dndManga("/archive.html", nextMangaTitle, SManga.ONGOING, nthManga))
|
||||||
|
return@stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(mangas, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not efficient, but the simplest way for me to refresh.
|
||||||
|
// We also can’t really use the `mangaDetailsRequest + mangaDetailsParse`
|
||||||
|
// approach, for we actually expect one of the books’ `url`s to change.
|
||||||
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
||||||
|
fetchPopularManga(0)
|
||||||
|
.map { mangasPage ->
|
||||||
|
mangasPage
|
||||||
|
.mangas
|
||||||
|
// Do not test for URL-equality, for the last book will always
|
||||||
|
// eventually migrate its archive page from `/archive.html` to
|
||||||
|
// its own page.
|
||||||
|
.first { it.title == manga.title }
|
||||||
|
}
|
||||||
|
|
||||||
|
// This implementation here is needlessly complicated, for it has to automatically detect
|
||||||
|
// whether we’re in a date-annotated archive, the main archive, or a dateless archive.
|
||||||
|
// All three are largely similar, there are just *some* (annoying) differences we have to
|
||||||
|
// deal with.
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val archivePages = response.asJsoup()
|
||||||
|
|
||||||
|
// For books where all pages released the same day, there is no page date column,
|
||||||
|
// so instead we grab the release date of the archive page itself from its footer.
|
||||||
|
val pageDate = archivePages
|
||||||
|
.select("""br + i""")
|
||||||
|
.mapNotNull { EXTR_PAGE_DATE.find(it.text())?.groupValues?.getOrNull(1) }
|
||||||
|
.map { PAGE_DATE_FMT.parse(it)?.time }
|
||||||
|
.firstOrNull()
|
||||||
|
?: 0L
|
||||||
|
var i = 0
|
||||||
|
|
||||||
|
return archivePages
|
||||||
|
.select("""div.text > table.text > tbody > tr""")
|
||||||
|
.mapNotNull {
|
||||||
|
val pageData = it.select("""td""")
|
||||||
|
var pageAnchor = pageData.getOrNull(2)?.selectFirst("a")
|
||||||
|
// null for »Intermission«, main archive, dateless archive,…
|
||||||
|
if (pageAnchor != null) {
|
||||||
|
SChapter.create().apply {
|
||||||
|
name = pageAnchor!!.text()
|
||||||
|
chapter_number = (i++).toFloat()
|
||||||
|
date_upload = runCatching {
|
||||||
|
DATE_FMT.parse(pageData[0].text())!!.time
|
||||||
|
}.getOrDefault(0L)
|
||||||
|
setUrlWithoutDomain(pageAnchor!!.absUrl("href"))
|
||||||
|
}
|
||||||
|
} else if (!pageData.hasAttr("colspan")) {
|
||||||
|
// Are we in a dateless archive?
|
||||||
|
pageAnchor = pageData.getOrNull(0)?.selectFirst("a")
|
||||||
|
if (pageAnchor != null) {
|
||||||
|
SChapter.create().apply {
|
||||||
|
name = pageAnchor.text()
|
||||||
|
chapter_number = (i++).toFloat()
|
||||||
|
date_upload = pageDate
|
||||||
|
setUrlWithoutDomain(pageAnchor.absUrl("href"))
|
||||||
|
}
|
||||||
|
} else { null }
|
||||||
|
} else { null }
|
||||||
|
}
|
||||||
|
.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> =
|
||||||
|
// Careful. For almost all images it’s `div.center>p>img`, except for pages released on
|
||||||
|
// April’s Fools day, when it’s `div.center>p>a>img`. We could still add the `p` in
|
||||||
|
// between, but it was decided to leave it out, in case yet another *almost* same
|
||||||
|
// page layout pops up in the future.
|
||||||
|
//
|
||||||
|
// For example, this episode was released during April’s Fools day.
|
||||||
|
// https://www.darthsanddroids.net/episodes/0082.html
|
||||||
|
response
|
||||||
|
.asJsoup()
|
||||||
|
.select("""div.center img""")
|
||||||
|
.mapIndexed { i, img ->
|
||||||
|
Page(
|
||||||
|
index = i,
|
||||||
|
imageUrl = img.absUrl("src"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = throw UnsupportedOperationException()
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException()
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
||||||
|
override fun imageUrlRequest(page: Page): Request = throw UnsupportedOperationException()
|
||||||
|
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DATE_FMT = SimpleDateFormat("EEE d MMM, yyyy", Locale.US)
|
||||||
|
private val EXTR_PAGE_DATE = """Published\:\s+(\w+,\s+\d+\s+\w+,\s+\d+\;\s+\d+\:\d+\:\d+\s+\w+)""".toRegex()
|
||||||
|
private val PAGE_DATE_FMT = SimpleDateFormat("EEEEE, d MMMMM, yyyy; HH:mm:ss zzz", Locale.US)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,8 @@ ext {
|
||||||
extName = 'Drake Scans'
|
extName = 'Drake Scans'
|
||||||
extClass = '.DrakeScans'
|
extClass = '.DrakeScans'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://drake-scans.com'
|
baseUrl = 'https://drakecomic.com'
|
||||||
overrideVersionCode = 12
|
overrideVersionCode = 13
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|