Compare commits
No commits in common. "c2b107a8bd17b366286dcbeba06fcdfd9fc0ac7e" and "feb6c74f5c5c3668a0d23744f296cff92e207075" have entirely different histories.
c2b107a8bd
...
feb6c74f5c
|
@ -720,10 +720,6 @@ And for a release build of Tachiyomi:
|
||||||
|
|
||||||
### Android Debugger
|
### Android Debugger
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> If you didn't build the main app from source with debug enabled and are using a release/beta APK, you **need** a rooted device.
|
|
||||||
> If you are using an emulator instead, make sure you choose a profile **without** Google Play.
|
|
||||||
|
|
||||||
You can leverage the Android Debugger to step through your extension while debugging.
|
You can leverage the Android Debugger to step through your extension while debugging.
|
||||||
|
|
||||||
You *cannot* simply use Android Studio's `Debug 'module.name'` -> this will most likely result in an
|
You *cannot* simply use Android Studio's `Debug 'module.name'` -> this will most likely result in an
|
||||||
|
|
|
@ -63,7 +63,6 @@ android {
|
||||||
release {
|
release {
|
||||||
signingConfig signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
vcsInfo.include false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,10 +74,6 @@ android {
|
||||||
buildConfig true
|
buildConfig true
|
||||||
}
|
}
|
||||||
|
|
||||||
packaging {
|
|
||||||
resources.excludes.add("kotlin-tooling-metadata.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
|
|
@ -4,7 +4,7 @@ coroutines_version = "1.6.4"
|
||||||
serialization_version = "1.4.0"
|
serialization_version = "1.4.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
gradle-agp = { module = "com.android.tools.build:gradle", version = "8.4.1" }
|
gradle-agp = { module = "com.android.tools.build:gradle", version = "8.2.1" }
|
||||||
gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" }
|
gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" }
|
||||||
gradle-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" }
|
gradle-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" }
|
||||||
gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" }
|
gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" }
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 5
|
baseVersionCode = 4
|
||||||
|
|
|
@ -30,10 +30,11 @@ abstract class FansubsCat(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val baseUrl: String,
|
override val baseUrl: String,
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
val apiBaseUrl: String,
|
|
||||||
val isHentaiSite: Boolean,
|
val isHentaiSite: Boolean,
|
||||||
) : HttpSource() {
|
) : HttpSource() {
|
||||||
|
|
||||||
|
private val apiBaseUrl = "https://api.fansubs.cat"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||||
|
@ -90,7 +91,7 @@ abstract class FansubsCat(
|
||||||
// Popular
|
// Popular
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
return GET("$apiBaseUrl/manga/popular/$page", headers)
|
return GET("$apiBaseUrl/manga/popular/$page?hentai=$isHentaiSite", headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage = parseMangaFromJson(response)
|
override fun popularMangaParse(response: Response): MangasPage = parseMangaFromJson(response)
|
||||||
|
@ -98,7 +99,7 @@ abstract class FansubsCat(
|
||||||
// Latest
|
// Latest
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
return GET("$apiBaseUrl/manga/recent/$page", headers)
|
return GET("$apiBaseUrl/manga/recent/$page?hentai=$isHentaiSite", headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage = parseMangaFromJson(response)
|
override fun latestUpdatesParse(response: Response): MangasPage = parseMangaFromJson(response)
|
||||||
|
@ -109,15 +110,13 @@ abstract class FansubsCat(
|
||||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||||
val mangaTypeFilter = filterList.find { it is MangaTypeFilter } as MangaTypeFilter
|
val mangaTypeFilter = filterList.find { it is MangaTypeFilter } as MangaTypeFilter
|
||||||
val stateFilter = filterList.find { it is StateFilter } as StateFilter
|
val stateFilter = filterList.find { it is StateFilter } as StateFilter
|
||||||
|
val demographyFilter = filterList.find { it is DemographyFilter } as DemographyFilter
|
||||||
val genreFilter = filterList.find { it is GenreTagFilter } as GenreTagFilter
|
val genreFilter = filterList.find { it is GenreTagFilter } as GenreTagFilter
|
||||||
val themeFilter = filterList.find { it is ThemeTagFilter } as ThemeTagFilter
|
val themeFilter = filterList.find { it is ThemeTagFilter } as ThemeTagFilter
|
||||||
val builder = "$apiBaseUrl/manga/search/$page".toHttpUrl().newBuilder()
|
val builder = "$apiBaseUrl/manga/search/$page?hentai=$isHentaiSite".toHttpUrl().newBuilder()
|
||||||
mangaTypeFilter.addQueryParameter(builder)
|
mangaTypeFilter.addQueryParameter(builder)
|
||||||
stateFilter.addQueryParameter(builder)
|
stateFilter.addQueryParameter(builder)
|
||||||
if (!isHentaiSite) {
|
|
||||||
val demographyFilter = filterList.find { it is DemographyFilter } as DemographyFilter
|
|
||||||
demographyFilter.addQueryParameter(builder)
|
demographyFilter.addQueryParameter(builder)
|
||||||
}
|
|
||||||
genreFilter.addQueryParameter(builder)
|
genreFilter.addQueryParameter(builder)
|
||||||
themeFilter.addQueryParameter(builder)
|
themeFilter.addQueryParameter(builder)
|
||||||
if (query.isNotBlank()) {
|
if (query.isNotBlank()) {
|
||||||
|
@ -132,7 +131,7 @@ abstract class FansubsCat(
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
return GET(
|
return GET(
|
||||||
"$apiBaseUrl/manga/details/${manga.url.substringAfterLast('/')}",
|
"$apiBaseUrl/manga/details/${manga.url.substringAfterLast('/')}?hentai=$isHentaiSite",
|
||||||
headers,
|
headers,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -167,7 +166,7 @@ abstract class FansubsCat(
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
return GET(
|
return GET(
|
||||||
"$apiBaseUrl/manga/chapters/${manga.url.substringAfterLast('/')}",
|
"$apiBaseUrl/manga/chapters/${manga.url.substringAfterLast('/')}?hentai=$isHentaiSite",
|
||||||
headers,
|
headers,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -179,7 +178,7 @@ abstract class FansubsCat(
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
return GET(
|
return GET(
|
||||||
"$apiBaseUrl/manga/pages/${chapter.url.substringAfterLast('/')}",
|
"$apiBaseUrl/manga/pages/${chapter.url.substringAfterLast('/')}?hentai=$isHentaiSite",
|
||||||
headers,
|
headers,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
plugins {
|
||||||
|
id("lib-multisrc")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseVersionCode = 6
|
|
@ -0,0 +1,251 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.flixscans
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
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 kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Callback
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
abstract class FlixScans(
|
||||||
|
override val name: String,
|
||||||
|
override val baseUrl: String,
|
||||||
|
override val lang: String,
|
||||||
|
protected val apiUrl: String = "$baseUrl/api/v1",
|
||||||
|
protected val cdnUrl: String = baseUrl.replace("://", "://media.").plus("/"),
|
||||||
|
) : HttpSource() {
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
protected open val json: Json by injectLazy()
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
|
.rateLimit(2)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
return GET("$apiUrl/webtoon/pages/home/romance", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val result = response.parseAs<HomeDto>()
|
||||||
|
|
||||||
|
val entries = (result.hot + result.topAll + result.topMonth + result.topWeek)
|
||||||
|
.distinctBy { it.id }
|
||||||
|
.map { it.toSManga(cdnUrl) }
|
||||||
|
|
||||||
|
return MangasPage(entries, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
return GET("$apiUrl/search/advance?page=$page&serie_type=webtoon", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
val result = response.parseAs<ApiResponse<BrowseSeries>>()
|
||||||
|
|
||||||
|
val entries = result.data.map { it.toSManga(cdnUrl) }
|
||||||
|
val hasNextPage = result.lastPage > result.currentPage
|
||||||
|
|
||||||
|
return MangasPage(entries, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var fetchGenreList: List<GenreHolder> = emptyList()
|
||||||
|
private var fetchGenreCallOngoing = false
|
||||||
|
private var fetchGenreFailed = false
|
||||||
|
private var fetchGenreAttempt = 0
|
||||||
|
|
||||||
|
private fun fetchGenre() {
|
||||||
|
if (fetchGenreAttempt < 3 && (fetchGenreList.isEmpty() || fetchGenreFailed) && !fetchGenreCallOngoing) {
|
||||||
|
fetchGenreCallOngoing = true
|
||||||
|
|
||||||
|
// fetch genre asynchronously as it sometimes hangs
|
||||||
|
client.newCall(fetchGenreRequest()).enqueue(fetchGenreCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val fetchGenreCallback = object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
fetchGenreAttempt++
|
||||||
|
fetchGenreFailed = true
|
||||||
|
fetchGenreCallOngoing = false
|
||||||
|
|
||||||
|
e.message?.let { Log.e("$name Filters", it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
fetchGenreCallOngoing = false
|
||||||
|
fetchGenreAttempt++
|
||||||
|
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
fetchGenreFailed = true
|
||||||
|
response.close()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val parsed = runCatching {
|
||||||
|
response.use(::fetchGenreParse)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchGenreFailed = parsed.isFailure
|
||||||
|
fetchGenreList = parsed.getOrElse {
|
||||||
|
Log.e("$name Filters", it.stackTraceToString())
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchGenreRequest(): Request {
|
||||||
|
return GET("$apiUrl/search/genres", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchGenreParse(response: Response): List<GenreHolder> {
|
||||||
|
return response.parseAs<List<GenreHolder>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList {
|
||||||
|
fetchGenre()
|
||||||
|
|
||||||
|
val filters: MutableList<Filter<*>> = mutableListOf(
|
||||||
|
Filter.Header("Ignored when using Text Search"),
|
||||||
|
MainGenreFilter(),
|
||||||
|
TypeFilter(),
|
||||||
|
StatusFilter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
filters += if (fetchGenreList.isNotEmpty()) {
|
||||||
|
listOf(
|
||||||
|
GenreFilter("Genre", fetchGenreList),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
Filter.Separator(),
|
||||||
|
Filter.Header("Press 'reset' to attempt to show Genres"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return FilterList(filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
if (query.isNotEmpty()) {
|
||||||
|
val url = "$apiUrl/search/serie".toHttpUrl().newBuilder()
|
||||||
|
.addPathSegment(query.trim())
|
||||||
|
.addQueryParameter("page", page.toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
val advSearchUrl = apiUrl.toHttpUrl().newBuilder().apply {
|
||||||
|
addPathSegments("search/advance")
|
||||||
|
addQueryParameter("page", page.toString())
|
||||||
|
addQueryParameter("serie_type", "webtoon")
|
||||||
|
|
||||||
|
filters.forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is GenreFilter -> {
|
||||||
|
filter.checked.let {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
addQueryParameter("genres", it.joinToString(","))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is MainGenreFilter -> {
|
||||||
|
if (filter.state > 0) {
|
||||||
|
addQueryParameter("main_genres", filter.selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TypeFilter -> {
|
||||||
|
if (filter.state > 0) {
|
||||||
|
addQueryParameter("type", filter.selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is StatusFilter -> {
|
||||||
|
if (filter.state > 0) {
|
||||||
|
addQueryParameter("status", filter.selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(advSearchUrl, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
|
||||||
|
|
||||||
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
|
val (prefix, id) = getPrefixIdFromUrl(manga.url)
|
||||||
|
|
||||||
|
return GET("$apiUrl/webtoon/series/$id/$prefix", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
val result = response.parseAs<SeriesResponse>()
|
||||||
|
|
||||||
|
return result.serie.toSManga(cdnUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
|
val (prefix, id) = getPrefixIdFromUrl(manga.url)
|
||||||
|
|
||||||
|
return GET("$apiUrl/webtoon/chapters/$id-desc#$prefix", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val chapters = response.parseAs<List<Chapter>>()
|
||||||
|
val prefix = response.request.url.fragment!!
|
||||||
|
|
||||||
|
return chapters.map { it.toSChapter(prefix) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
val (prefix, id) = getPrefixIdFromUrl(chapter.url)
|
||||||
|
|
||||||
|
return GET("$apiUrl/webtoon/chapters/chapter/$id/$prefix", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun getPrefixIdFromUrl(url: String): Pair<String, String> {
|
||||||
|
return with(url.substringAfterLast("/")) {
|
||||||
|
val split = split("-")
|
||||||
|
|
||||||
|
split[0] to split[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val result = response.parseAs<PageListResponse>()
|
||||||
|
|
||||||
|
return result.chapter.chapterData.webtoon.mapIndexed { i, img ->
|
||||||
|
Page(i, "", cdnUrl + img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
protected inline fun <reified T> Response.parseAs(): T =
|
||||||
|
use { body.string() }.let(json::decodeFromString)
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.flixscans
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ApiResponse<T>(
|
||||||
|
val data: List<T>,
|
||||||
|
@SerialName("current_page") val currentPage: Int,
|
||||||
|
@SerialName("last_page") val lastPage: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HomeDto(
|
||||||
|
val hot: List<BrowseSeries>,
|
||||||
|
val topWeek: List<BrowseSeries>,
|
||||||
|
val topMonth: List<BrowseSeries>,
|
||||||
|
val topAll: List<BrowseSeries>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BrowseSeries(
|
||||||
|
val id: Int,
|
||||||
|
val title: String,
|
||||||
|
val slug: String,
|
||||||
|
val prefix: Int,
|
||||||
|
val thumbnail: String?,
|
||||||
|
) {
|
||||||
|
fun toSManga(cdnUrl: String) = SManga.create().apply {
|
||||||
|
title = this@BrowseSeries.title
|
||||||
|
url = "/series/$prefix-$id-$slug"
|
||||||
|
thumbnail_url = thumbnail?.let { cdnUrl + it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SearchInput(
|
||||||
|
val title: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GenreHolder(
|
||||||
|
val name: String,
|
||||||
|
val id: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SeriesResponse(
|
||||||
|
val serie: Series,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Series(
|
||||||
|
val id: Int,
|
||||||
|
val title: String,
|
||||||
|
val slug: String,
|
||||||
|
val prefix: Int,
|
||||||
|
val thumbnail: String?,
|
||||||
|
val story: String?,
|
||||||
|
val serieType: String?,
|
||||||
|
val mainGenres: String?,
|
||||||
|
val otherNames: List<String>? = emptyList(),
|
||||||
|
val status: String?,
|
||||||
|
val type: String?,
|
||||||
|
val authors: List<GenreHolder>? = emptyList(),
|
||||||
|
val artists: List<GenreHolder>? = emptyList(),
|
||||||
|
val genres: List<GenreHolder>? = emptyList(),
|
||||||
|
) {
|
||||||
|
fun toSManga(cdnUrl: String) = SManga.create().apply {
|
||||||
|
title = this@Series.title
|
||||||
|
url = "/series/$prefix-$id-$slug"
|
||||||
|
thumbnail_url = cdnUrl + thumbnail
|
||||||
|
author = authors?.joinToString { it.name.trim() }
|
||||||
|
artist = artists?.joinToString { it.name.trim() }
|
||||||
|
genre = (otherGenres + genres?.map { it.name.trim() }.orEmpty())
|
||||||
|
.distinct().joinToString { it.trim() }
|
||||||
|
description = story?.let { Jsoup.parse(it).text() }
|
||||||
|
if (otherNames?.isNotEmpty() == true) {
|
||||||
|
if (description.isNullOrEmpty()) {
|
||||||
|
description = "Alternative Names:\n"
|
||||||
|
} else {
|
||||||
|
description += "\n\nAlternative Names:\n"
|
||||||
|
}
|
||||||
|
description += otherNames.joinToString("\n") { "• ${it.trim()}" }
|
||||||
|
}
|
||||||
|
status = when (this@Series.status?.trim()) {
|
||||||
|
"ongoing" -> SManga.ONGOING
|
||||||
|
"completed" -> SManga.COMPLETED
|
||||||
|
"onhold" -> SManga.ON_HIATUS
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val otherGenres = listOfNotNull(serieType, mainGenres, type)
|
||||||
|
.map { word ->
|
||||||
|
word.trim().replaceFirstChar {
|
||||||
|
if (it.isLowerCase()) {
|
||||||
|
it.titlecase(Locale.getDefault())
|
||||||
|
} else {
|
||||||
|
it.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Chapter(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
val slug: String,
|
||||||
|
val createdAt: String? = null,
|
||||||
|
) {
|
||||||
|
fun toSChapter(prefix: String) = SChapter.create().apply {
|
||||||
|
url = "/read/webtoon/$prefix-$id-$slug"
|
||||||
|
name = this@Chapter.name
|
||||||
|
date_upload = runCatching { dateFormat.parse(createdAt!!)!!.time }.getOrDefault(0L)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.ENGLISH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PageListResponse(
|
||||||
|
val chapter: ChapterPages,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ChapterPages(
|
||||||
|
val chapterData: ChapterPageData,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ChapterPageData(
|
||||||
|
val webtoon: List<String>,
|
||||||
|
)
|
|
@ -0,0 +1,62 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.flixscans
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
|
||||||
|
abstract class SelectFilter(
|
||||||
|
name: String,
|
||||||
|
private val options: List<String>,
|
||||||
|
) : Filter.Select<String>(
|
||||||
|
name,
|
||||||
|
options.toTypedArray(),
|
||||||
|
) {
|
||||||
|
val selected get() = options[state]
|
||||||
|
}
|
||||||
|
|
||||||
|
class CheckBoxFilter(
|
||||||
|
name: String,
|
||||||
|
val id: String,
|
||||||
|
) : Filter.CheckBox(name)
|
||||||
|
|
||||||
|
class GenreFilter(
|
||||||
|
name: String,
|
||||||
|
private val genres: List<GenreHolder>,
|
||||||
|
) : Filter.Group<CheckBoxFilter>(
|
||||||
|
name,
|
||||||
|
genres.map { CheckBoxFilter(it.name.trim(), it.id.toString()) },
|
||||||
|
) {
|
||||||
|
val checked get() = state.filter { it.state }.map { it.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
class MainGenreFilter : SelectFilter(
|
||||||
|
"Main Genre",
|
||||||
|
listOf(
|
||||||
|
"",
|
||||||
|
"fantasy",
|
||||||
|
"romance",
|
||||||
|
"action",
|
||||||
|
"drama",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class TypeFilter : SelectFilter(
|
||||||
|
"Type",
|
||||||
|
listOf(
|
||||||
|
"",
|
||||||
|
"manhwa",
|
||||||
|
"manhua",
|
||||||
|
"manga",
|
||||||
|
"comic",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class StatusFilter : SelectFilter(
|
||||||
|
"Status",
|
||||||
|
listOf(
|
||||||
|
"",
|
||||||
|
"ongoing",
|
||||||
|
"completed",
|
||||||
|
"droped",
|
||||||
|
"onhold",
|
||||||
|
"soon",
|
||||||
|
),
|
||||||
|
)
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 2
|
baseVersionCode = 1
|
||||||
|
|
|
@ -294,7 +294,7 @@ abstract class GalleryAdults(
|
||||||
val categoryFilters = filters.filterIsInstance<CategoryFilters>().firstOrNull()
|
val categoryFilters = filters.filterIsInstance<CategoryFilters>().firstOrNull()
|
||||||
|
|
||||||
// Only for query string or multiple tags
|
// Only for query string or multiple tags
|
||||||
val url = "$baseUrl/search/".toHttpUrl().newBuilder().apply {
|
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply {
|
||||||
getSortOrderURIs().forEachIndexed { index, pair ->
|
getSortOrderURIs().forEachIndexed { index, pair ->
|
||||||
addQueryParameter(pair.second, toBinary(sortOrderFilter?.state == index))
|
addQueryParameter(pair.second, toBinary(sortOrderFilter?.state == index))
|
||||||
}
|
}
|
||||||
|
@ -310,7 +310,7 @@ abstract class GalleryAdults(
|
||||||
addEncodedQueryParameter(intermediateSearchKey, buildQueryString(selectedGenres.map { it.name }, query))
|
addEncodedQueryParameter(intermediateSearchKey, buildQueryString(selectedGenres.map { it.name }, query))
|
||||||
addPageUri(page)
|
addPageUri(page)
|
||||||
}
|
}
|
||||||
return GET(url.build(), headers)
|
return GET(url.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open val advancedSearchKey = "key"
|
protected open val advancedSearchKey = "key"
|
||||||
|
@ -331,7 +331,7 @@ abstract class GalleryAdults(
|
||||||
// Advanced search
|
// Advanced search
|
||||||
val advancedSearchFilters = filters.filterIsInstance<AdvancedTextFilter>()
|
val advancedSearchFilters = filters.filterIsInstance<AdvancedTextFilter>()
|
||||||
|
|
||||||
val url = "$baseUrl/$advancedSearchUri/".toHttpUrl().newBuilder().apply {
|
val url = "$baseUrl/$advancedSearchUri".toHttpUrl().newBuilder().apply {
|
||||||
getSortOrderURIs().forEachIndexed { index, pair ->
|
getSortOrderURIs().forEachIndexed { index, pair ->
|
||||||
addQueryParameter(pair.second, toBinary(sortOrderFilter?.state == index))
|
addQueryParameter(pair.second, toBinary(sortOrderFilter?.state == index))
|
||||||
}
|
}
|
||||||
|
@ -379,7 +379,7 @@ abstract class GalleryAdults(
|
||||||
addEncodedQueryParameter(advancedSearchKey, keys.joinToString("+"))
|
addEncodedQueryParameter(advancedSearchKey, keys.joinToString("+"))
|
||||||
addPageUri(page)
|
addPageUri(page)
|
||||||
}
|
}
|
||||||
return GET(url.build(), headers)
|
return GET(url.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 23
|
baseVersionCode = 22
|
||||||
|
|
|
@ -53,13 +53,7 @@ abstract class GroupLe(
|
||||||
.contains("internal/redirect") or (response.code == 301)
|
.contains("internal/redirect") or (response.code == 301)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (originalRequest.url.toString().contains("/list?")) {
|
throw IOException("Ссылка на мангу была изменена. Перемигрируйте мангу на тот же (или смежный с GroupLe) источник или передобавьте из Поисковика/Каталога.")
|
||||||
throw IOException("Смените домен: Поисковик > Расширения > $name > ⚙\uFE0F")
|
|
||||||
}
|
|
||||||
throw IOException(
|
|
||||||
"URL серии изменился. Перенесите/мигрируйте с $name " +
|
|
||||||
"на $name (или смежный с GroupLe), чтобы список глав обновился",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 12
|
baseVersionCode = 11
|
||||||
|
|
|
@ -194,14 +194,12 @@ open class Kemono(
|
||||||
|
|
||||||
override fun imageRequest(page: Page): Request {
|
override fun imageRequest(page: Page): Request {
|
||||||
val imageUrl = page.imageUrl!!
|
val imageUrl = page.imageUrl!!
|
||||||
|
|
||||||
if (!preferences.getBoolean(USE_LOW_RES_IMG, false)) return GET(imageUrl, headers)
|
if (!preferences.getBoolean(USE_LOW_RES_IMG, false)) return GET(imageUrl, headers)
|
||||||
|
val index = imageUrl.indexOf('/', startIndex = 8) // https://
|
||||||
val index = imageUrl.indexOf('/', 8)
|
|
||||||
val url = buildString {
|
val url = buildString {
|
||||||
append(imageUrl, 0, index)
|
append(imageUrl, 0, index)
|
||||||
append("/thumbnail/data")
|
append("/thumbnail")
|
||||||
append(imageUrl.substring(index))
|
append(imageUrl, index, imageUrl.length)
|
||||||
}
|
}
|
||||||
return GET(url, headers)
|
return GET(url, headers)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 6
|
baseVersionCode = 5
|
||||||
|
|
|
@ -319,7 +319,7 @@ abstract class LectorTmo(
|
||||||
return GET(chapter.url, tmoHeaders)
|
return GET(chapter.url, tmoHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> = mutableListOf<Page>().apply {
|
||||||
var doc = redirectToReadPage(document)
|
var doc = redirectToReadPage(document)
|
||||||
|
|
||||||
val currentUrl = doc.location()
|
val currentUrl = doc.location()
|
||||||
|
@ -336,24 +336,21 @@ abstract class LectorTmo(
|
||||||
.build()
|
.build()
|
||||||
doc = client.newCall(GET(newUrl, redirectHeaders)).execute().asJsoup()
|
doc = client.newCall(GET(newUrl, redirectHeaders)).execute().asJsoup()
|
||||||
}
|
}
|
||||||
val imagesScript = doc.selectFirst("script:containsData(var dirPath):containsData(var images)")
|
|
||||||
|
|
||||||
imagesScript?.data()?.let {
|
doc.select("div.viewer-container img:not(noscript img)").forEach {
|
||||||
val dirPath = DIRPATH_REGEX.find(imagesScript.data())?.groupValues?.get(1)
|
add(
|
||||||
val images = IMAGES_REGEX.find(imagesScript.data())?.groupValues?.get(1)?.split(",")?.map { img ->
|
Page(
|
||||||
img.trim().removeSurrounding("\"")
|
size,
|
||||||
}
|
doc.location(),
|
||||||
if (dirPath != null && images != null) {
|
it.let {
|
||||||
return images.mapIndexed { i, img ->
|
if (it.hasAttr("data-src")) {
|
||||||
Page(i, doc.location(), "$dirPath$img")
|
it.attr("abs:data-src")
|
||||||
}
|
} else {
|
||||||
}
|
it.attr("abs:src")
|
||||||
}
|
|
||||||
|
|
||||||
doc.select("div.viewer-container img:not(noscript img)").let {
|
|
||||||
return it.mapIndexed { i, img ->
|
|
||||||
Page(i, doc.location(), img.imgAttr())
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -423,13 +420,6 @@ abstract class LectorTmo(
|
||||||
return document
|
return document
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Element.imgAttr(): String {
|
|
||||||
return when {
|
|
||||||
this.hasAttr("data-src") -> this.attr("abs:data-src")
|
|
||||||
else -> this.attr("abs:src")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.unescapeUrl(): String {
|
private fun String.unescapeUrl(): String {
|
||||||
return if (this.startsWith("http:\\/\\/") || this.startsWith("https:\\/\\/")) {
|
return if (this.startsWith("http:\\/\\/") || this.startsWith("https:\\/\\/")) {
|
||||||
this.replace("\\/", "/")
|
this.replace("\\/", "/")
|
||||||
|
@ -615,9 +605,6 @@ abstract class LectorTmo(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val DIRPATH_REGEX = """var\s+dirPath\s*=\s*'(.*?)'\s*;""".toRegex()
|
|
||||||
val IMAGES_REGEX = """var\s+images\s*=.*\[(.*?)\]\s*'\s*\)\s*;""".toRegex()
|
|
||||||
|
|
||||||
private const val SCANLATOR_PREF = "scanlatorPref"
|
private const val SCANLATOR_PREF = "scanlatorPref"
|
||||||
private const val SCANLATOR_PREF_TITLE = "Mostrar todos los scanlator"
|
private const val SCANLATOR_PREF_TITLE = "Mostrar todos los scanlator"
|
||||||
private const val SCANLATOR_PREF_SUMMARY = "Se mostraran capítulos repetidos pero con diferentes Scanlators"
|
private const val SCANLATOR_PREF_SUMMARY = "Se mostraran capítulos repetidos pero con diferentes Scanlators"
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 28
|
baseVersionCode = 25
|
||||||
|
|
|
@ -1,295 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.multisrc.libgroup
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Data<T>(
|
|
||||||
val data: T,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Constants(
|
|
||||||
@SerialName("ageRestriction") val ageRestrictions: List<IdLabelSiteType>,
|
|
||||||
@SerialName("format") val formats: List<IdNameSiteType>,
|
|
||||||
val genres: List<IdNameSiteType>,
|
|
||||||
val imageServers: List<ImageServer>,
|
|
||||||
@SerialName("scanlateStatus") val scanlateStatuses: List<IdLabelSiteType>,
|
|
||||||
@SerialName("status") val titleStatuses: List<IdLabelSiteType>,
|
|
||||||
val tags: List<IdNameSiteType>,
|
|
||||||
val types: List<IdLabelSiteType>,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
class IdLabelSiteType(
|
|
||||||
val id: Int,
|
|
||||||
val label: String,
|
|
||||||
@SerialName("site_ids") val siteIds: List<Int>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class IdNameSiteType(
|
|
||||||
val id: Int,
|
|
||||||
val name: String,
|
|
||||||
@SerialName("site_ids") val siteIds: List<Int>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class ImageServer(
|
|
||||||
val id: String,
|
|
||||||
val label: String,
|
|
||||||
val url: String,
|
|
||||||
@SerialName("site_ids") val siteIds: List<Int>,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun getServer(isServers: String?, siteId: Int): ImageServer =
|
|
||||||
if (!isServers.isNullOrBlank()) {
|
|
||||||
imageServers.first { it.id == isServers && it.siteIds.contains(siteId) }
|
|
||||||
} else {
|
|
||||||
imageServers.first { it.siteIds.contains(siteId) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCategories(siteId: Int): List<IdLabelSiteType> = types.filter { it.siteIds.contains(siteId) }
|
|
||||||
fun getFormats(siteId: Int): List<IdNameSiteType> = formats.filter { it.siteIds.contains(siteId) }
|
|
||||||
fun getGenres(siteId: Int): List<IdNameSiteType> = genres.filter { it.siteIds.contains(siteId) }
|
|
||||||
fun getTags(siteId: Int): List<IdNameSiteType> = tags.filter { it.siteIds.contains(siteId) }
|
|
||||||
fun getScanlateStatuses(siteId: Int): List<IdLabelSiteType> = scanlateStatuses.filter { it.siteIds.contains(siteId) }
|
|
||||||
fun getTitleStatuses(siteId: Int): List<IdLabelSiteType> = titleStatuses.filter { it.siteIds.contains(siteId) }
|
|
||||||
fun getAgeRestrictions(siteId: Int): List<IdLabelSiteType> = ageRestrictions.filter { it.siteIds.contains(siteId) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class MangasPageDto(
|
|
||||||
val data: List<MangaShort>,
|
|
||||||
val meta: MangaPageMeta,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
class MangaPageMeta(
|
|
||||||
@SerialName("has_next_page") val hasNextPage: Boolean,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun mapToSManga(isEng: String): List<SManga> {
|
|
||||||
return this.data.map { it.toSManga(isEng) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class MangaShort(
|
|
||||||
val name: String,
|
|
||||||
@SerialName("rus_name") val rusName: String?,
|
|
||||||
@SerialName("eng_name") val engName: String?,
|
|
||||||
@SerialName("slug_url") val slugUrl: String,
|
|
||||||
val cover: Cover,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
data class Cover(
|
|
||||||
val default: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun toSManga(isEng: String) = SManga.create().apply {
|
|
||||||
title = getSelectedLanguage(isEng, rusName, engName, name)
|
|
||||||
thumbnail_url = cover.default.orEmpty()
|
|
||||||
url = "/$slugUrl"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Manga(
|
|
||||||
val type: LabelType,
|
|
||||||
val ageRestriction: LabelType,
|
|
||||||
val rating: Rating,
|
|
||||||
val genres: List<NameType>,
|
|
||||||
val tags: List<NameType>,
|
|
||||||
@SerialName("rus_name") val rusName: String?,
|
|
||||||
@SerialName("eng_name") val engName: String?,
|
|
||||||
val name: String,
|
|
||||||
val cover: MangaShort.Cover,
|
|
||||||
val authors: List<NameType>,
|
|
||||||
val artists: List<NameType>,
|
|
||||||
val status: LabelType,
|
|
||||||
val scanlateStatus: LabelType,
|
|
||||||
@SerialName("is_licensed") val isLicensed: Boolean,
|
|
||||||
val otherNames: List<String>,
|
|
||||||
val summary: String,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
class LabelType(
|
|
||||||
val label: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class NameType(
|
|
||||||
val name: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Rating(
|
|
||||||
val average: Float,
|
|
||||||
val votes: Int,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun toSManga(isEng: String): SManga = SManga.create().apply {
|
|
||||||
title = getSelectedLanguage(isEng, rusName, engName, name)
|
|
||||||
thumbnail_url = cover.default
|
|
||||||
author = authors.joinToString { it.name }
|
|
||||||
artist = artists.joinToString { it.name }
|
|
||||||
status = parseStatus(isLicensed, scanlateStatus.label, this@Manga.status.label)
|
|
||||||
genre = type.label.ifBlank { "Манга" } + ", " + ageRestriction.label + ", " +
|
|
||||||
genres.joinToString { it.name.trim() } + ", " + tags.joinToString { it.name.trim() }
|
|
||||||
description = getOppositeLanguage(isEng, rusName, engName) + rating.average.parseAverage() + " " + rating.average +
|
|
||||||
" (голосов: " + rating.votes + ")\n" + otherNames.joinAltNames() + summary
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Float.parseAverage(): String {
|
|
||||||
return when {
|
|
||||||
this > 9.5 -> "★★★★★"
|
|
||||||
this > 8.5 -> "★★★★✬"
|
|
||||||
this > 7.5 -> "★★★★☆"
|
|
||||||
this > 6.5 -> "★★★✬☆"
|
|
||||||
this > 5.5 -> "★★★☆☆"
|
|
||||||
this > 4.5 -> "★★✬☆☆"
|
|
||||||
this > 3.5 -> "★★☆☆☆"
|
|
||||||
this > 2.5 -> "★✬☆☆☆"
|
|
||||||
this > 1.5 -> "★☆☆☆☆"
|
|
||||||
this > 0.5 -> "✬☆☆☆☆"
|
|
||||||
else -> "☆☆☆☆☆"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(isLicensed: Boolean, statusTranslate: String, statusTitle: String): Int = when {
|
|
||||||
isLicensed -> SManga.LICENSED
|
|
||||||
statusTranslate == "Завершён" && statusTitle == "Приостановлен" || statusTranslate == "Заморожен" || statusTranslate == "Заброшен" -> SManga.ON_HIATUS
|
|
||||||
statusTranslate == "Завершён" && statusTitle == "Выпуск прекращён" -> SManga.CANCELLED
|
|
||||||
statusTranslate == "Продолжается" -> SManga.ONGOING
|
|
||||||
statusTranslate == "Выходит" -> SManga.ONGOING
|
|
||||||
statusTranslate == "Завершён" -> SManga.COMPLETED
|
|
||||||
statusTranslate == "Вышло" -> SManga.PUBLISHING_FINISHED
|
|
||||||
else -> when (statusTitle) {
|
|
||||||
"Онгоинг" -> SManga.ONGOING
|
|
||||||
"Анонс" -> SManga.ONGOING
|
|
||||||
"Завершён" -> SManga.COMPLETED
|
|
||||||
"Приостановлен" -> SManga.ON_HIATUS
|
|
||||||
"Выпуск прекращён" -> SManga.CANCELLED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<String>.joinAltNames(): String = when {
|
|
||||||
this.isNotEmpty() -> "Альтернативные названия:\n" + this.joinToString(" / ") + "\n\n"
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSelectedLanguage(isEng: String, rusName: String?, engName: String?, name: String): String = when {
|
|
||||||
isEng == "rus" && rusName.orEmpty().isNotEmpty() -> rusName!!
|
|
||||||
isEng == "eng" && engName.orEmpty().isNotEmpty() -> engName!!
|
|
||||||
else -> name
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getOppositeLanguage(isEng: String, rusName: String?, engName: String?): String = when {
|
|
||||||
isEng == "eng" && rusName.orEmpty().isNotEmpty() -> rusName + "\n"
|
|
||||||
isEng == "rus" && engName.orEmpty().isNotEmpty() -> engName + "\n"
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Chapter(
|
|
||||||
val id: Int,
|
|
||||||
@SerialName("branches_count") val branchesCount: Int,
|
|
||||||
val branches: List<Branch>,
|
|
||||||
val name: String?,
|
|
||||||
val number: String,
|
|
||||||
val volume: String,
|
|
||||||
@SerialName("item_number") val itemNumber: Float?,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
class Branch(
|
|
||||||
@SerialName("branch_id") val branchId: Int?,
|
|
||||||
@SerialName("created_at") val createdAt: String,
|
|
||||||
val teams: List<Team>,
|
|
||||||
val user: User,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
class Team(
|
|
||||||
val name: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class User(
|
|
||||||
val username: String,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun first(branchId: Int? = null): Branch? {
|
|
||||||
return runCatching { if (branchId != null) branches.first { it.branchId == branchId } else branches.first() }.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getTeamName(branchId: Int? = null): String? {
|
|
||||||
return runCatching { first(branchId)!!.teams.first().name }.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getUserName(branchId: Int? = null): String? {
|
|
||||||
return runCatching { first(branchId)!!.user.username }.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toSChapter(slugUrl: String, branchId: Int? = null, isScanUser: Boolean): SChapter = SChapter.create().apply {
|
|
||||||
val chapterName = "Том $volume. Глава $number"
|
|
||||||
name = if (this@Chapter.name.isNullOrBlank()) chapterName else "$chapterName - ${this@Chapter.name}"
|
|
||||||
val branchStr = if (branchId != null) "&branch_id=$branchId" else ""
|
|
||||||
url = "/$slugUrl/chapter?$branchStr&volume=$volume&number=$number"
|
|
||||||
scanlator = getTeamName(branchId) ?: if (isScanUser) getUserName(branchId) else null
|
|
||||||
date_upload = runCatching { LibGroup.simpleDateFormat.parse(first(branchId)!!.createdAt)!!.time }.getOrDefault(0L)
|
|
||||||
chapter_number = itemNumber ?: -1f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun List<Chapter>.getBranchCount(): Int = this.maxOf { chapter -> chapter.branches.size }
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Branch(
|
|
||||||
val id: Int,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Pages(
|
|
||||||
val pages: List<MangaPage>,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
class MangaPage(
|
|
||||||
val slug: Int,
|
|
||||||
val url: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun toPageList(): List<Page> = pages.map { Page(it.slug, it.url) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class AuthToken(
|
|
||||||
private val auth: Auth,
|
|
||||||
private val token: Token,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
class Auth(
|
|
||||||
val id: Int,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Token(
|
|
||||||
val timestamp: Long,
|
|
||||||
@SerialName("expires_in") val expiresIn: Long,
|
|
||||||
@SerialName("token_type") val tokenType: String,
|
|
||||||
@SerialName("access_token") val accessToken: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun isExpired(): Boolean {
|
|
||||||
val currentTime = System.currentTimeMillis()
|
|
||||||
val expiresIn = token.timestamp + (token.expiresIn * 1000)
|
|
||||||
return expiresIn < currentTime
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getToken(): String = "${token.tokenType} ${token.accessToken}"
|
|
||||||
|
|
||||||
fun getUserId(): Int = auth.id
|
|
||||||
}
|
|
|
@ -19,7 +19,7 @@ class LibUrlActivity : Activity() {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val pathSegments = intent?.data?.pathSegments
|
val pathSegments = intent?.data?.pathSegments
|
||||||
if (pathSegments != null && pathSegments.size > 0) {
|
if (pathSegments != null && pathSegments.size > 0) {
|
||||||
val titleid = pathSegments[2]
|
val titleid = pathSegments[0]
|
||||||
val mainIntent = Intent().apply {
|
val mainIntent = Intent().apply {
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
action = "eu.kanade.tachiyomi.SEARCH"
|
||||||
putExtra("query", "${LibGroup.PREFIX_SLUG_SEARCH}$titleid")
|
putExtra("query", "${LibGroup.PREFIX_SLUG_SEARCH}$titleid")
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.likemanga
|
package eu.kanade.tachiyomi.multisrc.likemanga
|
||||||
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
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.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
@ -22,29 +20,25 @@ import okhttp3.Response
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class LikeManga : ParsedHttpSource() {
|
abstract class LikeManga(
|
||||||
override val name = "LikeManga"
|
override val name: String,
|
||||||
|
override val baseUrl: String,
|
||||||
override val baseUrl = "https://likemanga.io"
|
override val lang: String,
|
||||||
|
) : ParsedHttpSource() {
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
override val client = network.cloudflareClient
|
||||||
.rateLimit(1, 2)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.add("Referer", "$baseUrl/")
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
return searchMangaRequest(page, "", FilterList(SortFilter("top-manga")))
|
return searchMangaRequest(page, "", FilterList(SortFilter("top-manga")))
|
||||||
}
|
}
|
||||||
|
@ -63,19 +57,6 @@ class LikeManga : ParsedHttpSource() {
|
||||||
override fun latestUpdatesSelector() = searchMangaSelector()
|
override fun latestUpdatesSelector() = searchMangaSelector()
|
||||||
override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
|
override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
if (query.startsWith(URL_SEARCH_PREFIX)) {
|
|
||||||
val url = "$baseUrl/${query.substringAfter(URL_SEARCH_PREFIX)}"
|
|
||||||
return client.newCall(GET(url, headers)).asObservableSuccess().map { response ->
|
|
||||||
MangasPage(
|
|
||||||
mangas = listOf(mangaDetailsParse(response).apply { setUrlWithoutDomain(url) }),
|
|
||||||
hasNextPage = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.fetchSearchManga(page, query, filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
addQueryParameter("act", "searchadvance")
|
addQueryParameter("act", "searchadvance")
|
||||||
|
@ -259,6 +240,12 @@ class LikeManga : ParsedHttpSource() {
|
||||||
|
|
||||||
override fun chapterListSelector() = ".wp-manga-chapter"
|
override fun chapterListSelector() = ".wp-manga-chapter"
|
||||||
|
|
||||||
|
private fun String?.parseDate(): Long {
|
||||||
|
return runCatching {
|
||||||
|
dateFormat.parse(this!!)!!.time
|
||||||
|
}.getOrDefault(0L)
|
||||||
|
}
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
val element = document.selectFirst("div.reading input#next_img_token")
|
val element = document.selectFirst("div.reading input#next_img_token")
|
||||||
|
|
||||||
|
@ -290,14 +277,12 @@ class LikeManga : ParsedHttpSource() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String?.parseDate(): Long =
|
|
||||||
try { dateFormat.parse(this!!)!!.time } catch (_: Exception) { 0L }
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val URL_SEARCH_PREFIX = "slug:"
|
val dateFormat by lazy {
|
||||||
private val dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.ENGLISH)
|
SimpleDateFormat("MMMM dd, yyyy", Locale.ENGLISH)
|
||||||
|
}
|
||||||
private val chapterPageCountRegex = Regex("""load_list_chapter\((\d+)\)""")
|
private val chapterPageCountRegex = Regex("""load_list_chapter\((\d+)\)""")
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.likemanga
|
package eu.kanade.tachiyomi.multisrc.likemanga
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
|
|
@ -615,7 +615,6 @@ abstract class Madara(
|
||||||
"Đã hoàn thành",
|
"Đã hoàn thành",
|
||||||
"Завершено",
|
"Завершено",
|
||||||
"Tamamlanan",
|
"Tamamlanan",
|
||||||
"Complété",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
protected val ongoingStatusList: Array<String> = arrayOf(
|
protected val ongoingStatusList: Array<String> = arrayOf(
|
||||||
|
@ -623,7 +622,7 @@ abstract class Madara(
|
||||||
"Em Andamento", "En cours", "En Cours", "En cours de publication", "Ativo", "Lançando", "Đang Tiến Hành", "Devam Ediyor",
|
"Em Andamento", "En cours", "En Cours", "En cours de publication", "Ativo", "Lançando", "Đang Tiến Hành", "Devam Ediyor",
|
||||||
"Devam ediyor", "In Corso", "In Arrivo", "مستمرة", "مستمر", "En Curso", "En curso", "Emision",
|
"Devam ediyor", "In Corso", "In Arrivo", "مستمرة", "مستمر", "En Curso", "En curso", "Emision",
|
||||||
"Curso", "En marcha", "Publicandose", "En emision", "连载中", "Em Lançamento", "Devam Ediyo",
|
"Curso", "En marcha", "Publicandose", "En emision", "连载中", "Em Lançamento", "Devam Ediyo",
|
||||||
"Đang làm", "Em postagem", "Devam Eden", "Em progresso", "Em curso",
|
"Đang làm", "Em postagem", "Devam Eden", "Em progresso",
|
||||||
)
|
)
|
||||||
|
|
||||||
protected val hiatusStatusList: Array<String> = arrayOf(
|
protected val hiatusStatusList: Array<String> = arrayOf(
|
||||||
|
@ -636,7 +635,6 @@ abstract class Madara(
|
||||||
"متوقف",
|
"متوقف",
|
||||||
"En Pause",
|
"En Pause",
|
||||||
"Заморожено",
|
"Заморожено",
|
||||||
"En attente",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
protected val canceledStatusList: Array<String> = arrayOf(
|
protected val canceledStatusList: Array<String> = arrayOf(
|
||||||
|
@ -648,7 +646,6 @@ abstract class Madara(
|
||||||
"ملغي",
|
"ملغي",
|
||||||
"Abandonné",
|
"Abandonné",
|
||||||
"Заброшено",
|
"Заброшено",
|
||||||
"Annulé",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
|
|
@ -208,7 +208,7 @@ constructor(
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector(): String? = ".pagination a[rel=next]"
|
override fun searchMangaNextPageSelector(): String? = ".pagination a[rel=next]"
|
||||||
|
|
||||||
protected open fun parseSearchDirectory(page: Int): MangasPage {
|
protected fun parseSearchDirectory(page: Int): MangasPage {
|
||||||
val manga = searchDirectory.subList((page - 1) * 24, min(page * 24, searchDirectory.size))
|
val manga = searchDirectory.subList((page - 1) * 24, min(page * 24, searchDirectory.size))
|
||||||
.map {
|
.map {
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 3
|
baseVersionCode = 2
|
||||||
|
|
|
@ -36,12 +36,12 @@ abstract class Senkuro(
|
||||||
override val supportsLatest = false
|
override val supportsLatest = false
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||||
.add("User-Agent", "Tachiyomi (+https://github.com/keiyoushi/extensions-source)")
|
.add("User-Agent", "Tachiyomi (+https://github.com/tachiyomiorg/tachiyomi)")
|
||||||
.add("Content-Type", "application/json")
|
.add("Content-Type", "application/json")
|
||||||
|
|
||||||
override val client: OkHttpClient =
|
override val client: OkHttpClient =
|
||||||
network.client.newBuilder()
|
network.client.newBuilder()
|
||||||
.rateLimit(3)
|
.rateLimit(5)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private inline fun <reified T : Any> T.toJsonRequestBody(): RequestBody =
|
private inline fun <reified T : Any> T.toJsonRequestBody(): RequestBody =
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name="eu.kanade.tachiyomi.multisrc.terrascan.TerraScanUrlActivity"
|
|
||||||
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="${SOURCEHOST}"
|
|
||||||
android:pathPattern="/manga/..*"
|
|
||||||
android:scheme="${SOURCESCHEME}" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
|
@ -1,245 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.multisrc.terrascan
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
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.ParsedHttpSource
|
|
||||||
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 org.jsoup.Jsoup
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
abstract class TerraScan(
|
|
||||||
override val name: String,
|
|
||||||
override val baseUrl: String,
|
|
||||||
override val lang: String,
|
|
||||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd-MM-yyyy", Locale("pt", "BR")),
|
|
||||||
) : ParsedHttpSource() {
|
|
||||||
|
|
||||||
override val supportsLatest: Boolean = true
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient
|
|
||||||
|
|
||||||
private val noRedirectClient = network.cloudflareClient.newBuilder()
|
|
||||||
.followRedirects(false)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
private var genresList: List<Genre> = emptyList()
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga?q=p&page=$page", headers)
|
|
||||||
|
|
||||||
open val popularMangaTitleSelector: String = "p, h3"
|
|
||||||
open val popularMangaThumbnailSelector: String = "img"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
|
||||||
title = element.selectFirst(popularMangaTitleSelector)!!.ownText()
|
|
||||||
thumbnail_url = element.selectFirst(popularMangaThumbnailSelector)?.srcAttr()
|
|
||||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = ".pagination > .page-item:not(.disabled):last-child"
|
|
||||||
|
|
||||||
override fun popularMangaSelector(): String = ".series-paginated .grid-item-series, .series-paginated .series"
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
if (genresList.isEmpty()) {
|
|
||||||
genresList = parseGenres(document)
|
|
||||||
}
|
|
||||||
val mangas = document.select(popularMangaSelector())
|
|
||||||
.map(::popularMangaFromElement)
|
|
||||||
|
|
||||||
return MangasPage(mangas, document.selectFirst(popularMangaNextPageSelector()) != null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/manga?q=u&page=$page", headers)
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
if (query.startsWith(URL_SEARCH_PREFIX)) {
|
|
||||||
val slug = query.substringAfter(URL_SEARCH_PREFIX)
|
|
||||||
return client.newCall(GET("$baseUrl/manga/$slug", headers))
|
|
||||||
.asObservableSuccess().map { response ->
|
|
||||||
MangasPage(listOf(mangaDetailsParse(response)), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.fetchSearchManga(page, query, filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = baseUrl.toHttpUrl().newBuilder()
|
|
||||||
|
|
||||||
if (query.isNotBlank()) {
|
|
||||||
url.addPathSegment("search")
|
|
||||||
.addQueryParameter("q", query)
|
|
||||||
return GET(url.build(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
url.addPathSegment("manga")
|
|
||||||
|
|
||||||
filters.forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is GenreFilter -> {
|
|
||||||
filter.state.forEach {
|
|
||||||
if (it.state) {
|
|
||||||
url.addQueryParameter(it.query, it.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
url.addQueryParameter("page", "$page")
|
|
||||||
|
|
||||||
return GET(url.build(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = null
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = ".col-6.col-sm-3.col-md-3.col-lg-2.p-1"
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
if (response.request.url.pathSegments.contains("search")) {
|
|
||||||
return searchByQueryMangaParse(response)
|
|
||||||
}
|
|
||||||
return super.searchMangaParse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList {
|
|
||||||
val filters = mutableListOf<Filter<out Any>>()
|
|
||||||
if (genresList.isNotEmpty()) {
|
|
||||||
filters += GenreFilter(
|
|
||||||
title = "Gêneros",
|
|
||||||
genres = genresList,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
filters += Filter.Header("Aperte 'Redefinir' mostrar os gêneros disponíveis")
|
|
||||||
}
|
|
||||||
return FilterList(filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
open val mangaDetailsContainerSelector: String = "main"
|
|
||||||
open val mangaDetailsTitleSelector: String = "h1"
|
|
||||||
open val mangaDetailsThumbnailSelector: String = "img"
|
|
||||||
open val mangaDetailsDescriptionSelector: String = "p"
|
|
||||||
open val mangaDetailsGenreSelector: String = ".card:has(h5:contains(Categorias)) a, .card:has(h5:contains(Categorias)) div"
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
|
||||||
with(document.selectFirst(mangaDetailsContainerSelector)!!) {
|
|
||||||
title = selectFirst(mangaDetailsTitleSelector)!!.text()
|
|
||||||
thumbnail_url = selectFirst(mangaDetailsThumbnailSelector)?.absUrl("href")
|
|
||||||
description = selectFirst(mangaDetailsDescriptionSelector)?.text()
|
|
||||||
genre = document.select(mangaDetailsGenreSelector)
|
|
||||||
.joinToString { it.ownText() }
|
|
||||||
}
|
|
||||||
setUrlWithoutDomain(document.location())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
|
||||||
with(element.selectFirst("h5")!!) {
|
|
||||||
name = ownText()
|
|
||||||
date_upload = selectFirst("div")!!.ownText().toDate()
|
|
||||||
}
|
|
||||||
setUrlWithoutDomain(element.absUrl("href"))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = ".col-chapter a"
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
val mangaChapterUrl = document.location()
|
|
||||||
val maxPage = findPageCount(mangaChapterUrl)
|
|
||||||
return (1..maxPage).map { page -> Page(page - 1, "$mangaChapterUrl/$page") }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = document.selectFirst("main img")!!.srcAttr()
|
|
||||||
|
|
||||||
private fun searchByQueryMangaParse(response: Response): MangasPage {
|
|
||||||
val fragment = Jsoup.parseBodyFragment(
|
|
||||||
json.decodeFromString<String>(response.body.string()),
|
|
||||||
baseUrl,
|
|
||||||
)
|
|
||||||
|
|
||||||
return MangasPage(
|
|
||||||
mangas = fragment.select(searchMangaSelector()).map(::searchMangaFromElement),
|
|
||||||
hasNextPage = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findPageCount(pageUrl: String): Int {
|
|
||||||
var lowerBound = 1
|
|
||||||
var upperBound = 100
|
|
||||||
|
|
||||||
while (lowerBound <= upperBound) {
|
|
||||||
val midpoint = lowerBound + (upperBound - lowerBound) / 2
|
|
||||||
|
|
||||||
val request = Request.Builder().apply {
|
|
||||||
url("$pageUrl/$midpoint")
|
|
||||||
headers(headers)
|
|
||||||
head()
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
val response = try {
|
|
||||||
noRedirectClient.newCall(request).execute()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw Exception("Failed to fetch $pageUrl")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.code == 302) {
|
|
||||||
upperBound = midpoint - 1
|
|
||||||
} else {
|
|
||||||
lowerBound = midpoint + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lowerBound
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Element.srcAttr(): String = when {
|
|
||||||
hasAttr("data-src") -> absUrl("data-src")
|
|
||||||
else -> absUrl("src")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.toDate() = try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
|
|
||||||
|
|
||||||
open val genreFilterSelector: String = "form div > div:has(input) div"
|
|
||||||
|
|
||||||
private fun parseGenres(document: Document): List<Genre> {
|
|
||||||
return document.select(genreFilterSelector)
|
|
||||||
.map { element ->
|
|
||||||
val input = element.selectFirst("input")!!
|
|
||||||
Genre(
|
|
||||||
name = element.selectFirst("label")!!.ownText(),
|
|
||||||
query = input.attr("name"),
|
|
||||||
value = input.attr("value"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val URL_SEARCH_PREFIX = "slug:"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 6
|
baseVersionCode = 5
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:i18n"))
|
api(project(":lib:i18n"))
|
||||||
|
|
|
@ -163,8 +163,6 @@ abstract class WPComics(
|
||||||
val minuteWords = listOf("minute", "phút")
|
val minuteWords = listOf("minute", "phút")
|
||||||
val hourWords = listOf("hour", "giờ")
|
val hourWords = listOf("hour", "giờ")
|
||||||
val dayWords = listOf("day", "ngày")
|
val dayWords = listOf("day", "ngày")
|
||||||
val monthWords = listOf("month", "tháng")
|
|
||||||
val yearWords = listOf("year", "năm")
|
|
||||||
val agoWords = listOf("ago", "trước")
|
val agoWords = listOf("ago", "trước")
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
|
@ -173,8 +171,6 @@ abstract class WPComics(
|
||||||
val calendar = Calendar.getInstance()
|
val calendar = Calendar.getInstance()
|
||||||
|
|
||||||
when {
|
when {
|
||||||
yearWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.YEAR, -trimmedDate[0].toInt()) }
|
|
||||||
monthWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.MONTH, -trimmedDate[0].toInt()) }
|
|
||||||
dayWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) }
|
dayWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) }
|
||||||
hourWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) }
|
hourWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) }
|
||||||
minuteWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) }
|
minuteWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) }
|
||||||
|
|
|
@ -8,7 +8,7 @@ import androidx.preference.PreferenceScreen
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to return UserAgentType based on SharedPreference value
|
* Helper function to return UserAgentType based on SharedPreference value
|
||||||
*/
|
*/
|
||||||
fun SharedPreferences.getPrefUAType(): UserAgentType {
|
fun SharedPreferences.getPrefUAType(): UserAgentType {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Akuma'
|
extName = 'Akuma'
|
||||||
extClass = '.AkumaFactory'
|
extClass = '.Akuma'
|
||||||
extVersionCode = 4
|
extVersionCode = 1
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,20 +21,15 @@ import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.TimeZone
|
|
||||||
|
|
||||||
class Akuma(
|
class Akuma : ParsedHttpSource() {
|
||||||
override val lang: String,
|
|
||||||
private val akumaLang: String,
|
|
||||||
) : ParsedHttpSource() {
|
|
||||||
|
|
||||||
override val name = "Akuma"
|
override val name = "Akuma"
|
||||||
|
|
||||||
override val baseUrl = "https://akuma.moe"
|
override val baseUrl = "https://akuma.moe"
|
||||||
|
|
||||||
|
override val lang = "all"
|
||||||
|
|
||||||
override val supportsLatest = false
|
override val supportsLatest = false
|
||||||
|
|
||||||
private var nextHash: String? = null
|
private var nextHash: String? = null
|
||||||
|
@ -43,9 +38,6 @@ class Akuma(
|
||||||
|
|
||||||
private val ddosGuardIntercept = DDosGuardInterceptor(network.client)
|
private val ddosGuardIntercept = DDosGuardInterceptor(network.client)
|
||||||
|
|
||||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ENGLISH).apply {
|
|
||||||
timeZone = TimeZone.getTimeZone("UTC")
|
|
||||||
}
|
|
||||||
override val client: OkHttpClient = network.client.newBuilder()
|
override val client: OkHttpClient = network.client.newBuilder()
|
||||||
.addInterceptor(ddosGuardIntercept)
|
.addInterceptor(ddosGuardIntercept)
|
||||||
.addInterceptor(::tokenInterceptor)
|
.addInterceptor(::tokenInterceptor)
|
||||||
|
@ -110,19 +102,12 @@ class Akuma(
|
||||||
.add("view", "3")
|
.add("view", "3")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val url = baseUrl.toHttpUrlOrNull()!!.newBuilder()
|
return if (page == 1) {
|
||||||
|
|
||||||
if (page == 1) {
|
|
||||||
nextHash = null
|
nextHash = null
|
||||||
|
POST(baseUrl, headers, payload)
|
||||||
} else {
|
} else {
|
||||||
url.addQueryParameter("cursor", nextHash)
|
POST("$baseUrl/?cursor=$nextHash", headers, payload)
|
||||||
}
|
}
|
||||||
if (lang != "all") {
|
|
||||||
// append like `q=language:english$`
|
|
||||||
url.addQueryParameter("q", "language:$akumaLang$")
|
|
||||||
}
|
|
||||||
|
|
||||||
return POST(url.toString(), headers, payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaSelector() = ".post-loop li"
|
override fun popularMangaSelector() = ".post-loop li"
|
||||||
|
@ -131,10 +116,6 @@ class Akuma(
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
|
|
||||||
if (document.text().contains("Max keywords of 3 exceeded.")) {
|
|
||||||
throw Exception("Login required for more than 3 filters")
|
|
||||||
} else if (document.text().contains("Max keywords of 8 exceeded.")) throw Exception("Only max of 8 filters are allowed")
|
|
||||||
|
|
||||||
val mangas = document.select(popularMangaSelector()).map { element ->
|
val mangas = document.select(popularMangaSelector()).map { element ->
|
||||||
popularMangaFromElement(element)
|
popularMangaFromElement(element)
|
||||||
}
|
}
|
||||||
|
@ -173,39 +154,8 @@ class Akuma(
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val request = popularMangaRequest(page)
|
val request = popularMangaRequest(page)
|
||||||
|
|
||||||
val finalQuery: MutableList<String> = mutableListOf(query)
|
|
||||||
|
|
||||||
if (lang != "all") {
|
|
||||||
finalQuery.add("language: $akumaLang$")
|
|
||||||
}
|
|
||||||
filters.forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is TextFilter -> {
|
|
||||||
if (filter.state.isNotEmpty()) {
|
|
||||||
finalQuery.addAll(
|
|
||||||
filter.state.split(",").filter { it.isNotBlank() }.map {
|
|
||||||
(if (it.trim().startsWith("-")) "-" else "") + "${filter.tag}:\"${it.trim().replace("-", "")}\""
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is OptionFilter -> {
|
|
||||||
if (filter.state > 0) finalQuery.add("opt:${filter.getValue()}")
|
|
||||||
}
|
|
||||||
is CategoryFilter -> {
|
|
||||||
filter.state.forEach {
|
|
||||||
when {
|
|
||||||
it.isIncluded() -> finalQuery.add("category:\"${it.name}\"")
|
|
||||||
it.isExcluded() -> finalQuery.add("-category:\"${it.name}\"")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val url = request.url.newBuilder()
|
val url = request.url.newBuilder()
|
||||||
.setQueryParameter("q", finalQuery.joinToString(" "))
|
.addQueryParameter("q", query.trim())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return request.newBuilder()
|
return request.newBuilder()
|
||||||
|
@ -218,62 +168,24 @@ class Akuma(
|
||||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = with(document) {
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
SManga.create().apply {
|
title = document.select(".entry-title").text()
|
||||||
title = select(".entry-title").text()
|
thumbnail_url = document.select(".img-thumbnail").attr("abs:src")
|
||||||
thumbnail_url = select(".img-thumbnail").attr("abs:src")
|
author = document.select("li.meta-data > span.artist + span.value").text()
|
||||||
|
genre = document.select(".info-list a").joinToString { it.text() }
|
||||||
author = select(".group~.value").eachText().joinToString()
|
description = document.select(".pages span.value").text() + " Pages"
|
||||||
artist = select(".artist~.value").eachText().joinToString()
|
|
||||||
|
|
||||||
val characters = select(".character~.value").eachText()
|
|
||||||
val parodies = select(".parody~.value").eachText()
|
|
||||||
val males = select(".male~.value")
|
|
||||||
.map { "${it.text()} ♂" }
|
|
||||||
val females = select(".female~.value")
|
|
||||||
.map { "${it.text()} ♀" }
|
|
||||||
val others = select(".other~.value")
|
|
||||||
.map { "${it.text()} ◊" }
|
|
||||||
// show all in tags for quickly searching
|
|
||||||
|
|
||||||
genre = (males + females + others).joinToString()
|
|
||||||
description = buildString {
|
|
||||||
append(
|
|
||||||
"Full English and Japanese title: \n",
|
|
||||||
select(".entry-title").text(),
|
|
||||||
"\n",
|
|
||||||
select(".entry-title+span").text(),
|
|
||||||
"\n\n",
|
|
||||||
)
|
|
||||||
|
|
||||||
// translated should show up in the description
|
|
||||||
append("Language: ", select(".language~.value").eachText().joinToString(), "\n")
|
|
||||||
append("Pages: ", select(".pages .value").text(), "\n")
|
|
||||||
append("Upload Date: ", select(".date .value>time").text().replace(" ", ", ") + " UTC", "\n")
|
|
||||||
append("Categories: ", selectFirst(".info-list .value")?.text() ?: "Unknown", "\n\n")
|
|
||||||
|
|
||||||
// show followings for easy to reference
|
|
||||||
parodies.takeIf { it.isNotEmpty() }?.let { append("Parodies: ", parodies.joinToString(), "\n") }
|
|
||||||
characters.takeIf { it.isNotEmpty() }?.let { append("Characters: ", characters.joinToString(), "\n") }
|
|
||||||
}
|
|
||||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||||
status = SManga.UNKNOWN
|
status = SManga.COMPLETED
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
val document = response.asJsoup()
|
return Observable.just(
|
||||||
|
listOf(
|
||||||
return listOf(
|
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
setUrlWithoutDomain("${response.request.url}/1")
|
url = "${manga.url}/1"
|
||||||
name = "Chapter"
|
name = "Chapter"
|
||||||
date_upload = try {
|
|
||||||
dateFormat.parse(document.select(".date .value>time").text())!!.time
|
|
||||||
} catch (_: ParseException) {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,8 +201,6 @@ class Akuma(
|
||||||
pageList.add(Page(i, "$url/$i"))
|
pageList.add(Page(i, "$url/$i"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pageList[0].imageUrl = imageUrlParse(document)
|
|
||||||
|
|
||||||
return pageList
|
return pageList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -298,8 +208,6 @@ class Akuma(
|
||||||
return document.select(".entry-content img").attr("abs:src")
|
return document.select(".entry-content img").attr("abs:src")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFilterList(): FilterList = getFilters()
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val PREFIX_ID = "id:"
|
const val PREFIX_ID = "id:"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.akuma
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
|
|
||||||
class AkumaFactory : SourceFactory {
|
|
||||||
override fun createSources(): List<Source> = listOf(
|
|
||||||
Akuma("all", "all"),
|
|
||||||
Akuma("en", "english"),
|
|
||||||
Akuma("id", "indonesian"),
|
|
||||||
Akuma("jv", "javanese"),
|
|
||||||
Akuma("ca", "catalan"),
|
|
||||||
Akuma("ceb", "cebuano"),
|
|
||||||
Akuma("cs", "czech"),
|
|
||||||
Akuma("da", "danish"),
|
|
||||||
Akuma("de", "german"),
|
|
||||||
Akuma("et", "estonian"),
|
|
||||||
Akuma("es", "spanish"),
|
|
||||||
Akuma("eo", "esperanto"),
|
|
||||||
Akuma("fr", "french"),
|
|
||||||
Akuma("it", "italian"),
|
|
||||||
Akuma("hi", "hindi"),
|
|
||||||
Akuma("hu", "hungarian"),
|
|
||||||
Akuma("nl", "dutch"),
|
|
||||||
Akuma("pl", "polish"),
|
|
||||||
Akuma("pt", "portuguese"),
|
|
||||||
Akuma("vi", "vietnamese"),
|
|
||||||
Akuma("tr", "turkish"),
|
|
||||||
Akuma("ru", "russian"),
|
|
||||||
Akuma("uk", "ukrainian"),
|
|
||||||
Akuma("ar", "arabic"),
|
|
||||||
Akuma("ko", "korean"),
|
|
||||||
Akuma("zh", "chinese"),
|
|
||||||
Akuma("ja", "japanese"),
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.akuma
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
|
|
||||||
fun getFilters(): FilterList {
|
|
||||||
return FilterList(
|
|
||||||
Filter.Header("Separate tags with commas (,)"),
|
|
||||||
Filter.Header("Prepend with dash (-) to exclude"),
|
|
||||||
TextFilter("Female Tags", "female"),
|
|
||||||
TextFilter("Male Tags", "male"),
|
|
||||||
TextFilter("Other Tags", "other"),
|
|
||||||
CategoryFilter(),
|
|
||||||
TextFilter("Groups", "group"),
|
|
||||||
TextFilter("Artists", "artist"),
|
|
||||||
TextFilter("Parody", "parody"),
|
|
||||||
TextFilter("Characters", "character"),
|
|
||||||
Filter.Separator(),
|
|
||||||
Filter.Header("Search in favorites, read, or commented"),
|
|
||||||
OptionFilter(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class TextFilter(name: String, val tag: String) : Filter.Text(name)
|
|
||||||
internal class OptionFilter(val value: List<Pair<String, String>> = options) : Filter.Select<String>("Options", options.map { it.first }.toTypedArray()) {
|
|
||||||
fun getValue() = options[state].second
|
|
||||||
}
|
|
||||||
|
|
||||||
internal open class TagTriState(name: String) : Filter.TriState(name)
|
|
||||||
internal class CategoryFilter() :
|
|
||||||
Filter.Group<Filter.TriState>("Categories", categoryList.map { TagTriState(it) })
|
|
||||||
|
|
||||||
private val categoryList = listOf(
|
|
||||||
"Doujinshi",
|
|
||||||
"Manga",
|
|
||||||
"Image Set",
|
|
||||||
"Artist CG",
|
|
||||||
"Game CG",
|
|
||||||
"Western",
|
|
||||||
"Non-H",
|
|
||||||
"Cosplay",
|
|
||||||
"Misc",
|
|
||||||
)
|
|
||||||
private val options = listOf(
|
|
||||||
"None" to "",
|
|
||||||
"Favorited only" to "favorited",
|
|
||||||
"Read only" to "read",
|
|
||||||
"Commented only" to "commented",
|
|
||||||
)
|
|
|
@ -18,15 +18,6 @@ class AsmHentai(
|
||||||
lang = lang,
|
lang = lang,
|
||||||
) {
|
) {
|
||||||
override val supportsLatest = mangaLang.isNotBlank()
|
override val supportsLatest = mangaLang.isNotBlank()
|
||||||
override val supportSpeechless: Boolean = true
|
|
||||||
|
|
||||||
override fun Element.mangaLang() =
|
|
||||||
select("a:has(.flag)").attr("href")
|
|
||||||
.removeSuffix("/").substringAfterLast("/")
|
|
||||||
.let {
|
|
||||||
// Include Speechless in search results
|
|
||||||
if (it == LANGUAGE_SPEECHLESS) mangaLang else it
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun Element.mangaUrl() =
|
override fun Element.mangaUrl() =
|
||||||
selectFirst(".image a")?.attr("abs:href")
|
selectFirst(".image a")?.attr("abs:href")
|
||||||
|
@ -34,6 +25,10 @@ class AsmHentai(
|
||||||
override fun Element.mangaThumbnail() =
|
override fun Element.mangaThumbnail() =
|
||||||
selectFirst(".image img")?.imgAttr()
|
selectFirst(".image img")?.imgAttr()
|
||||||
|
|
||||||
|
override fun Element.mangaLang() =
|
||||||
|
select("a:has(.flag)").attr("href")
|
||||||
|
.removeSuffix("/").substringAfterLast("/")
|
||||||
|
|
||||||
override fun popularMangaSelector() = ".preview_item"
|
override fun popularMangaSelector() = ".preview_item"
|
||||||
|
|
||||||
override val favoritePath = "inc/user.php?act=favs"
|
override val favoritePath = "inc/user.php?act=favs"
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'Galaxy'
|
|
||||||
extClass = '.GalaxyFactory'
|
|
||||||
extVersionCode = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 19 KiB |
|
@ -1,327 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.galaxy
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
|
||||||
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.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import java.util.Calendar
|
|
||||||
|
|
||||||
abstract class Galaxy(
|
|
||||||
override val name: String,
|
|
||||||
override val baseUrl: String,
|
|
||||||
override val lang: String,
|
|
||||||
) : HttpSource() {
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
|
||||||
.rateLimit(2)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
|
||||||
.add("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
|
||||||
return if (page == 1) {
|
|
||||||
GET("$baseUrl/webtoons/romance/home", headers)
|
|
||||||
} else {
|
|
||||||
GET("$baseUrl/webtoons/action/home", headers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
val entries = document.select(
|
|
||||||
"""div.tabs div[wire:snapshot*=App\\Models\\Serie], main div:has(h2:matches(Today\'s Hot|الرائج اليوم)) a[wire:snapshot*=App\\Models\\Serie]""",
|
|
||||||
).map { element ->
|
|
||||||
SManga.create().apply {
|
|
||||||
setUrlWithoutDomain(
|
|
||||||
if (element.tagName().equals("a")) {
|
|
||||||
element.absUrl("href")
|
|
||||||
} else {
|
|
||||||
element.selectFirst("a")!!.absUrl("href")
|
|
||||||
},
|
|
||||||
)
|
|
||||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
|
||||||
title = element.selectFirst("div.text-sm")!!.text()
|
|
||||||
}
|
|
||||||
}.distinctBy { it.url }
|
|
||||||
|
|
||||||
return MangasPage(entries, response.request.url.pathSegments.getOrNull(1) == "romance")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
val url = "$baseUrl/latest?serie_type=webtoon&main_genres=romance" +
|
|
||||||
if (page > 1) {
|
|
||||||
"&page=$page"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
val entries = document.select("div[wire:snapshot*=App\\\\Models\\\\Serie]").map { element ->
|
|
||||||
SManga.create().apply {
|
|
||||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
|
||||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
|
||||||
title = element.select("div.flex a[href*=/series/]").last()!!.text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val hasNextPage = document.selectFirst("[role=navigation] button[wire:click*=nextPage]") != null
|
|
||||||
|
|
||||||
return MangasPage(entries, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var filters: List<FilterData> = emptyList()
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
|
||||||
protected fun launchIO(block: () -> Unit) = scope.launch {
|
|
||||||
try {
|
|
||||||
block()
|
|
||||||
} catch (_: Exception) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList {
|
|
||||||
launchIO {
|
|
||||||
if (filters.isEmpty()) {
|
|
||||||
val document = client.newCall(GET("$baseUrl/search", headers)).execute().asJsoup()
|
|
||||||
|
|
||||||
val mainGenre = FilterData(
|
|
||||||
displayName = document.select("label[for$=main_genres]").text(),
|
|
||||||
options = document.select("select[wire:model.live=main_genres] option").map {
|
|
||||||
it.text() to it.attr("value")
|
|
||||||
},
|
|
||||||
queryParameter = "main_genres",
|
|
||||||
)
|
|
||||||
val typeFilter = FilterData(
|
|
||||||
displayName = document.select("label[for$=type]").text(),
|
|
||||||
options = document.select("select[wire:model.live=type] option").map {
|
|
||||||
it.text() to it.attr("value")
|
|
||||||
},
|
|
||||||
queryParameter = "type",
|
|
||||||
)
|
|
||||||
val statusFilter = FilterData(
|
|
||||||
displayName = document.select("label[for$=status]").text(),
|
|
||||||
options = document.select("select[wire:model.live=status] option").map {
|
|
||||||
it.text() to it.attr("value")
|
|
||||||
},
|
|
||||||
queryParameter = "status",
|
|
||||||
)
|
|
||||||
val genreFilter = FilterData(
|
|
||||||
displayName = if (lang == "ar") {
|
|
||||||
"التصنيفات"
|
|
||||||
} else {
|
|
||||||
"Genre"
|
|
||||||
},
|
|
||||||
options = document.select("div[x-data*=genre] > div").map {
|
|
||||||
it.text() to it.attr("wire:key")
|
|
||||||
},
|
|
||||||
queryParameter = "genre",
|
|
||||||
)
|
|
||||||
|
|
||||||
filters = listOf(mainGenre, typeFilter, statusFilter, genreFilter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val filters: List<Filter<*>> = filters.map {
|
|
||||||
SelectFilter(
|
|
||||||
it.displayName,
|
|
||||||
it.options,
|
|
||||||
it.queryParameter,
|
|
||||||
)
|
|
||||||
}.ifEmpty {
|
|
||||||
listOf(
|
|
||||||
Filter.Header("Press 'reset' to load filters"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return FilterList(filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply {
|
|
||||||
addQueryParameter("serie_type", "webtoon")
|
|
||||||
addQueryParameter("title", query.trim())
|
|
||||||
filters.filterIsInstance<SelectFilter>().forEach {
|
|
||||||
it.addFilterParameter(this)
|
|
||||||
}
|
|
||||||
if (page > 1) {
|
|
||||||
addQueryParameter("page", page.toString())
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
return SManga.create().apply {
|
|
||||||
title = document.select("#full_model h3").text()
|
|
||||||
thumbnail_url = document.selectFirst("main img[src*=series/webtoon]")?.absUrl("src")
|
|
||||||
status = when (document.getQueryParam("status")) {
|
|
||||||
"ongoing", "soon" -> SManga.ONGOING
|
|
||||||
"completed", "droped" -> SManga.COMPLETED
|
|
||||||
"onhold" -> SManga.ON_HIATUS
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
genre = buildList {
|
|
||||||
document.getQueryParam("type")
|
|
||||||
?.capitalize()?.let(::add)
|
|
||||||
document.select("#full_model a[href*=search?genre]")
|
|
||||||
.eachText().let(::addAll)
|
|
||||||
}.joinToString()
|
|
||||||
author = document.select("#full_model [wire:key^=a-]").eachText().joinToString()
|
|
||||||
artist = document.select("#full_model [wire:key^=r-]").eachText().joinToString()
|
|
||||||
description = buildString {
|
|
||||||
append(document.select("#full_model p").text().trim())
|
|
||||||
append("\n\nAlternative Names:\n")
|
|
||||||
document.select("#full_model [wire:key^=n-]")
|
|
||||||
.joinToString("\n") { "• ${it.text().trim().removeMdEscaped()}" }
|
|
||||||
.let(::append)
|
|
||||||
}.trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Document.getQueryParam(queryParam: String): String? {
|
|
||||||
return selectFirst("#full_model a[href*=search?$queryParam]")
|
|
||||||
?.absUrl("href")?.toHttpUrlOrNull()?.queryParameter(queryParam)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.capitalize(): String {
|
|
||||||
val result = StringBuilder(length)
|
|
||||||
var capitalize = true
|
|
||||||
for (char in this) {
|
|
||||||
result.append(
|
|
||||||
if (capitalize) {
|
|
||||||
char.uppercase()
|
|
||||||
} else {
|
|
||||||
char.lowercase()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
capitalize = char.isWhitespace()
|
|
||||||
}
|
|
||||||
return result.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val mdRegex = Regex("""&#(\d+);""")
|
|
||||||
|
|
||||||
private fun String.removeMdEscaped(): String {
|
|
||||||
val char = mdRegex.find(this)?.groupValues?.get(1)?.toIntOrNull()
|
|
||||||
?: return this
|
|
||||||
|
|
||||||
return replaceFirst(mdRegex, Char(char).toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
return document.select("a[href*=/read/]:not([type=button])").map { element ->
|
|
||||||
SChapter.create().apply {
|
|
||||||
setUrlWithoutDomain(element.absUrl("href"))
|
|
||||||
name = element.select("span.font-normal").text()
|
|
||||||
date_upload = element.selectFirst("div:not(:has(> svg)) > span.text-xs")
|
|
||||||
?.text().parseRelativeDate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun String?.parseRelativeDate(): 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
return document.select("[wire:key^=image] img").mapIndexed { idx, img ->
|
|
||||||
Page(idx, imageUrl = img.absUrl("src"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.galaxy
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
|
|
||||||
class GalaxyFactory : SourceFactory {
|
|
||||||
|
|
||||||
class GalaxyWebtoon : Galaxy("Galaxy Webtoon", "https://galaxyaction.net", "en") {
|
|
||||||
override val id = 2602904659965278831
|
|
||||||
}
|
|
||||||
|
|
||||||
class GalaxyManga : Galaxy("Galaxy Manga", "https://ayoub-zrr.xyz", "ar") {
|
|
||||||
override val id = 2729515745226258240
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createSources() = listOf(
|
|
||||||
GalaxyWebtoon(),
|
|
||||||
GalaxyManga(),
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.galaxy
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
|
|
||||||
class SelectFilter(
|
|
||||||
name: String,
|
|
||||||
private val options: List<Pair<String, String>>,
|
|
||||||
private val queryParam: String,
|
|
||||||
) : Filter.Select<String>(
|
|
||||||
name,
|
|
||||||
buildList {
|
|
||||||
add("")
|
|
||||||
addAll(options.map { it.first })
|
|
||||||
}.toTypedArray(),
|
|
||||||
) {
|
|
||||||
fun addFilterParameter(url: HttpUrl.Builder) {
|
|
||||||
if (state == 0) return
|
|
||||||
|
|
||||||
url.addQueryParameter(queryParam, options[state - 1].second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FilterData(
|
|
||||||
val displayName: String,
|
|
||||||
val options: List<Pair<String, String>>,
|
|
||||||
val queryParameter: String,
|
|
||||||
)
|
|
|
@ -1,8 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = '3Hentai'
|
|
||||||
extClass = '.Hentai3Factory'
|
|
||||||
extVersionCode = 1
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 10 KiB |
|
@ -1,207 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.hentai3
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.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 org.jsoup.nodes.Element
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.TimeZone
|
|
||||||
|
|
||||||
class Hentai3(
|
|
||||||
override val lang: String = "all",
|
|
||||||
private val searchLang: String = "",
|
|
||||||
) : HttpSource() {
|
|
||||||
|
|
||||||
override val name = "3Hentai"
|
|
||||||
|
|
||||||
override val baseUrl = "https://3hentai.net"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
|
||||||
.set("referer", "$baseUrl/")
|
|
||||||
.set("origin", baseUrl)
|
|
||||||
|
|
||||||
// Popular
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
|
||||||
return GET("$baseUrl/${if (searchLang.isNotEmpty()) "language/$searchLang/${if (page > 1) page else ""}?" else "search?q=pages%3A>0&pages=$page&"}sort=popular", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
|
|
||||||
val mangas = doc.select("a[href*=/d/]").map(::popularMangaFromElement)
|
|
||||||
val hasNextPage = doc.selectFirst("a[rel=next]") != null
|
|
||||||
|
|
||||||
return MangasPage(mangas, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun popularMangaFromElement(element: Element): SManga {
|
|
||||||
return SManga.create().apply {
|
|
||||||
title = element.selectFirst("div")!!.ownText()
|
|
||||||
setUrlWithoutDomain(element.absUrl("href"))
|
|
||||||
thumbnail_url = element.selectFirst("img:not([class])")!!.absUrl("src")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Latest
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
return GET("$baseUrl/${if (searchLang.isNotEmpty()) "language/$searchLang/$page" else "search?q=pages%3A>0&pages=$page"}", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
|
|
||||||
|
|
||||||
// Search
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val tags = mutableListOf<String>()
|
|
||||||
var singleTag: Pair<String, String>? = null
|
|
||||||
var sort = ""
|
|
||||||
|
|
||||||
if (searchLang.isNotEmpty()) tags.add("language:$searchLang")
|
|
||||||
filters.forEach {
|
|
||||||
when (it) {
|
|
||||||
is SelectFilter -> sort = it.getValue()
|
|
||||||
|
|
||||||
is TextFilter -> {
|
|
||||||
if (it.state.isNotEmpty()) {
|
|
||||||
val splitted = it.state.split(",").filter(String::isNotBlank)
|
|
||||||
if (splitted.size < 2 && it.type != "tags") {
|
|
||||||
singleTag = it.type to it.state.replace(" ", "-")
|
|
||||||
} else {
|
|
||||||
splitted.map { tag ->
|
|
||||||
val trimmed = tag.trim().lowercase()
|
|
||||||
tags.add(
|
|
||||||
buildString {
|
|
||||||
if (trimmed.startsWith('-')) append("-")
|
|
||||||
append(it.type, ":'")
|
|
||||||
append(trimmed.removePrefix("-"), if (it.specific.isNotEmpty()) " (${it.specific})'" else "'")
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
if (singleTag != null) {
|
|
||||||
addPathSegment(singleTag!!.first)
|
|
||||||
addPathSegment(singleTag!!.second)
|
|
||||||
if (page > 1) addPathSegment(page.toString())
|
|
||||||
} else {
|
|
||||||
addPathSegment("search")
|
|
||||||
addQueryParameter(
|
|
||||||
"q",
|
|
||||||
when {
|
|
||||||
tags.isNotEmpty() -> tags.joinToString()
|
|
||||||
query.isNotEmpty() -> query
|
|
||||||
else -> "page:>0"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (page > 1) addQueryParameter("page", page.toString())
|
|
||||||
}
|
|
||||||
addQueryParameter("sort", sort)
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
|
|
||||||
|
|
||||||
// Details
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
fun String.capitalizeEach() = this.split(" ").joinToString(" ") { s ->
|
|
||||||
s.replaceFirstChar { sr ->
|
|
||||||
if (sr.isLowerCase()) sr.titlecase(Locale.getDefault()) else sr.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return SManga.create().apply {
|
|
||||||
val authors = document.select("a[href*=/groups/]").eachText().joinToString()
|
|
||||||
val artists = document.select("a[href*=/artists/]").eachText().joinToString()
|
|
||||||
initialized = true
|
|
||||||
title = document.select("h1 > span").text()
|
|
||||||
author = authors.ifEmpty { artists }
|
|
||||||
artist = artists.ifEmpty { authors }
|
|
||||||
genre = document.select("a[href*=/tags/]").eachText().joinToString {
|
|
||||||
val capitalized = it.capitalizeEach()
|
|
||||||
if (capitalized.contains("male")) {
|
|
||||||
capitalized.replace("(female)", "♀").replace("(male)", "♂")
|
|
||||||
} else {
|
|
||||||
"$capitalized ◊"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
description = buildString {
|
|
||||||
document.select("a[href*=/characters/]").eachText().joinToString().ifEmpty { null }?.let {
|
|
||||||
append("Characters: ", it.capitalizeEach(), "\n\n")
|
|
||||||
}
|
|
||||||
document.select("a[href*=/series/]").eachText().joinToString().ifEmpty { null }?.let {
|
|
||||||
append("Series: ", it.capitalizeEach(), "\n\n")
|
|
||||||
}
|
|
||||||
document.select("a[href*=/groups/]").eachText().joinToString().ifEmpty { null }?.let {
|
|
||||||
append("Groups: ", it.capitalizeEach(), "\n\n")
|
|
||||||
}
|
|
||||||
document.select("a[href*=/language/]").eachText().joinToString().ifEmpty { null }?.let {
|
|
||||||
append("Languages: ", it.capitalizeEach(), "\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
append(document.select("div.tag-container:contains(pages:)").text(), "\n")
|
|
||||||
}
|
|
||||||
thumbnail_url = document.selectFirst("img[src*=thumbnail].w-96")?.absUrl("src")
|
|
||||||
status = SManga.COMPLETED
|
|
||||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZZZ", Locale.ENGLISH).apply {
|
|
||||||
timeZone = TimeZone.getTimeZone("UTC")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chapters
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
return listOf(
|
|
||||||
SChapter.create().apply {
|
|
||||||
name = "Chapter"
|
|
||||||
setUrlWithoutDomain(response.request.url.toString())
|
|
||||||
date_upload = try {
|
|
||||||
dateFormat.parse(doc.select("time").text())!!.time
|
|
||||||
} catch (_: ParseException) {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pages
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val images = response.asJsoup().select("img:not([class], [src*=thumb], [src*=cover])")
|
|
||||||
return images.mapIndexed { index, image ->
|
|
||||||
val imageUrl = image.absUrl("src")
|
|
||||||
Page(index, imageUrl = imageUrl.replace(Regex("t(?=\\.)"), ""))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList() = getFilters()
|
|
||||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.hentai3
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
|
|
||||||
class Hentai3Factory : SourceFactory {
|
|
||||||
override fun createSources(): List<Source> = listOf(
|
|
||||||
Hentai3("all", ""),
|
|
||||||
Hentai3("en", "english"),
|
|
||||||
Hentai3("ja", "japanese"),
|
|
||||||
Hentai3("ko", "korean"),
|
|
||||||
Hentai3("zh", "chinese"),
|
|
||||||
Hentai3("mo", "mongolian"),
|
|
||||||
Hentai3("es", "spanish"),
|
|
||||||
Hentai3("pt", "Portuguese"),
|
|
||||||
Hentai3("id", "indonesian"),
|
|
||||||
Hentai3("jv", "javanese"),
|
|
||||||
Hentai3("tl", "tagalog"),
|
|
||||||
Hentai3("vi", "vietnamese"),
|
|
||||||
Hentai3("th", "thai"),
|
|
||||||
Hentai3("my", "burmese"),
|
|
||||||
Hentai3("tr", "turkish"),
|
|
||||||
Hentai3("ru", "russian"),
|
|
||||||
Hentai3("uk", "ukrainian"),
|
|
||||||
Hentai3("po", "polish"),
|
|
||||||
Hentai3("fi", "finnish"),
|
|
||||||
Hentai3("de", "german"),
|
|
||||||
Hentai3("it", "italian"),
|
|
||||||
Hentai3("fr", "french"),
|
|
||||||
Hentai3("nl", "dutch"),
|
|
||||||
Hentai3("cs", "czech"),
|
|
||||||
Hentai3("hu", "hungarian"),
|
|
||||||
Hentai3("bg", "bulgarian"),
|
|
||||||
Hentai3("is", "icelandic"),
|
|
||||||
Hentai3("la", "latin"),
|
|
||||||
Hentai3("ar", "arabic"),
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.hentai3
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
|
|
||||||
fun getFilters(): FilterList {
|
|
||||||
return FilterList(
|
|
||||||
SelectFilter("Sort by", getSortsList),
|
|
||||||
Filter.Separator(),
|
|
||||||
Filter.Header("Separate tags with commas (,)"),
|
|
||||||
Filter.Header("Prepend with dash (-) to exclude"),
|
|
||||||
Filter.Header("Use 'Male Tags' or 'Female Tags' for specific categories. 'Tags' searches all categories."),
|
|
||||||
TextFilter("Tags", "tags"),
|
|
||||||
TextFilter("Male Tags", "tags", "male"),
|
|
||||||
TextFilter("Female Tags", "tags", "female"),
|
|
||||||
TextFilter("Series", "series"),
|
|
||||||
TextFilter("Characters", "characters"),
|
|
||||||
TextFilter("Artists", "artist"),
|
|
||||||
TextFilter("Groups", "groups"),
|
|
||||||
TextFilter("Languages", "language"),
|
|
||||||
Filter.Separator(),
|
|
||||||
Filter.Header("Filter by pages, for example: (>20)"),
|
|
||||||
TextFilter("Pages", "page"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal open class TextFilter(name: String, val type: String, val specific: String = "") : Filter.Text(name)
|
|
||||||
|
|
||||||
internal open class SelectFilter(name: String, private val vals: List<Pair<String, String>>, state: Int = 0) :
|
|
||||||
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
|
|
||||||
fun getValue() = vals[state].second
|
|
||||||
}
|
|
||||||
|
|
||||||
private val getSortsList: List<Pair<String, String>> = listOf(
|
|
||||||
Pair("Recent", ""),
|
|
||||||
Pair("Popular: All Time", "popular"),
|
|
||||||
Pair("Popular: Week", "popular-7d"),
|
|
||||||
Pair("Popular: Today", "popular-24h"),
|
|
||||||
)
|
|
|
@ -1,10 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'HentaiEra'
|
|
||||||
extClass = '.HentaiEraFactory'
|
|
||||||
themePkg = 'galleryadults'
|
|
||||||
baseUrl = 'https://hentaiera.com'
|
|
||||||
overrideVersionCode = 1
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 13 KiB |
|
@ -1,105 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.hentaiera
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
|
|
||||||
import eu.kanade.tachiyomi.multisrc.galleryadults.Genre
|
|
||||||
import eu.kanade.tachiyomi.multisrc.galleryadults.SearchFlagFilter
|
|
||||||
import eu.kanade.tachiyomi.multisrc.galleryadults.imgAttr
|
|
||||||
import eu.kanade.tachiyomi.multisrc.galleryadults.toBinary
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
|
|
||||||
class HentaiEra(
|
|
||||||
lang: String = "all",
|
|
||||||
override val mangaLang: String = LANGUAGE_MULTI,
|
|
||||||
) : GalleryAdults(
|
|
||||||
"HentaiEra",
|
|
||||||
"https://hentaiera.com",
|
|
||||||
lang = lang,
|
|
||||||
) {
|
|
||||||
override val supportsLatest = true
|
|
||||||
override val useIntermediateSearch: Boolean = true
|
|
||||||
override val supportSpeechless: Boolean = true
|
|
||||||
|
|
||||||
override fun Element.mangaTitle(selector: String): String? =
|
|
||||||
mangaFullTitle(selector.replace("caption", "gallery_title")).let {
|
|
||||||
if (preferences.shortTitle) it?.shortenTitle() else it
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun Element.mangaLang() =
|
|
||||||
select("a:has(.g_flag)").attr("href")
|
|
||||||
.removeSuffix("/").substringAfterLast("/")
|
|
||||||
.let {
|
|
||||||
// Include Speechless in search results
|
|
||||||
if (it == LANGUAGE_SPEECHLESS) mangaLang else it
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
|
||||||
// Only for query string or multiple tags
|
|
||||||
val url = "$baseUrl/search/".toHttpUrl().newBuilder().apply {
|
|
||||||
addQueryParameter("pp", "1")
|
|
||||||
|
|
||||||
getLanguageURIs().forEach { pair ->
|
|
||||||
addQueryParameter(
|
|
||||||
pair.second,
|
|
||||||
toBinary(mangaLang == pair.first || mangaLang == LANGUAGE_MULTI),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
addPageUri(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
return GET(url.build(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Details */
|
|
||||||
override fun Element.getInfo(tag: String): String {
|
|
||||||
return select("li:has(.tags_text:contains($tag)) .tag .item_name")
|
|
||||||
.joinToString {
|
|
||||||
val name = it.ownText()
|
|
||||||
if (tag.contains(regexTag)) {
|
|
||||||
genres[name] = it.parent()!!.attr("href")
|
|
||||||
.removeSuffix("/").substringAfterLast('/')
|
|
||||||
}
|
|
||||||
listOf(
|
|
||||||
name,
|
|
||||||
it.select(".split_tag").text()
|
|
||||||
.trim()
|
|
||||||
.removePrefix("| "),
|
|
||||||
)
|
|
||||||
.filter { s -> s.isNotBlank() }
|
|
||||||
.joinToString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun Element.getCover() =
|
|
||||||
selectFirst(".left_cover img")?.imgAttr()
|
|
||||||
|
|
||||||
override fun tagsParser(document: Document): List<Genre> {
|
|
||||||
return document.select("h2.gallery_title a")
|
|
||||||
.mapNotNull {
|
|
||||||
Genre(
|
|
||||||
it.text(),
|
|
||||||
it.attr("href")
|
|
||||||
.removeSuffix("/").substringAfterLast('/'),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val mangaDetailInfoSelector = ".gallery_first"
|
|
||||||
|
|
||||||
/* Pages */
|
|
||||||
override val thumbnailSelector = ".gthumb"
|
|
||||||
override val pageUri = "view"
|
|
||||||
|
|
||||||
override fun getCategoryURIs() = listOf(
|
|
||||||
SearchFlagFilter("Manga", "mg"),
|
|
||||||
SearchFlagFilter("Doujinshi", "dj"),
|
|
||||||
SearchFlagFilter("Western", "ws"),
|
|
||||||
SearchFlagFilter("Image Set", "is"),
|
|
||||||
SearchFlagFilter("Artist CG", "ac"),
|
|
||||||
SearchFlagFilter("Game CG", "gc"),
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.hentaiera
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
|
|
||||||
class HentaiEraFactory : SourceFactory {
|
|
||||||
|
|
||||||
override fun createSources(): List<Source> = listOf(
|
|
||||||
HentaiEra("en", GalleryAdults.LANGUAGE_ENGLISH),
|
|
||||||
HentaiEra("ja", GalleryAdults.LANGUAGE_JAPANESE),
|
|
||||||
HentaiEra("es", GalleryAdults.LANGUAGE_SPANISH),
|
|
||||||
HentaiEra("fr", GalleryAdults.LANGUAGE_FRENCH),
|
|
||||||
HentaiEra("ko", GalleryAdults.LANGUAGE_KOREAN),
|
|
||||||
HentaiEra("de", GalleryAdults.LANGUAGE_GERMAN),
|
|
||||||
HentaiEra("ru", GalleryAdults.LANGUAGE_RUSSIAN),
|
|
||||||
HentaiEra("all", GalleryAdults.LANGUAGE_MULTI),
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Hitomi'
|
extName = 'Hitomi'
|
||||||
extClass = '.HitomiFactory'
|
extClass = '.HitomiFactory'
|
||||||
extVersionCode = 31
|
extVersionCode = 28
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,50 +3,79 @@ package eu.kanade.tachiyomi.extension.all.hitomi
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
|
||||||
fun getFilters(): FilterList {
|
typealias OrderType = Pair<String?, String>
|
||||||
return FilterList(
|
typealias ParsedFilter = Pair<String, OrderType>
|
||||||
SelectFilter("Sort by", getSortsList),
|
|
||||||
TypeFilter("Types"),
|
private fun parseFilter(query: StringBuilder, area: String, filterState: String) {
|
||||||
Filter.Separator(),
|
filterState
|
||||||
|
.trim()
|
||||||
|
.split(',')
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.forEach {
|
||||||
|
val trimmed = it.trim()
|
||||||
|
val negativePrefix = if (trimmed.startsWith("-")) "-" else ""
|
||||||
|
query.append(" $negativePrefix$area:${trimmed.removePrefix("-").replace(" ", "_")}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseFilters(filters: FilterList): ParsedFilter {
|
||||||
|
val query = StringBuilder()
|
||||||
|
var order: OrderType = Pair("date", "added")
|
||||||
|
filters.forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is SortFilter -> {
|
||||||
|
order = filter.getOrder
|
||||||
|
}
|
||||||
|
is AreaFilter -> {
|
||||||
|
parseFilter(query, filter.getAreaName, filter.state)
|
||||||
|
}
|
||||||
|
else -> { /* Do Nothing */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Pair(query.toString(), order)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OrderFilter(val name: String, val order: OrderType) {
|
||||||
|
val getFilterName: String
|
||||||
|
get() = name
|
||||||
|
val getOrder: OrderType
|
||||||
|
get() = order
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SortFilter : UriPartFilter(
|
||||||
|
"Sort By",
|
||||||
|
arrayOf(
|
||||||
|
OrderFilter("Date Added", Pair(null, "index")),
|
||||||
|
OrderFilter("Date Published", Pair("date", "published")),
|
||||||
|
OrderFilter("Popular: Today", Pair("popular", "today")),
|
||||||
|
OrderFilter("Popular: Week", Pair("popular", "week")),
|
||||||
|
OrderFilter("Popular: Month", Pair("popular", "month")),
|
||||||
|
OrderFilter("Popular: Year", Pair("popular", "year")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private open class UriPartFilter(displayName: String, val vals: Array<OrderFilter>) :
|
||||||
|
Filter.Select<String>(displayName, vals.map { it.getFilterName }.toTypedArray()) {
|
||||||
|
val getOrder: OrderType
|
||||||
|
get() = vals[state].getOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AreaFilter(displayName: String, val areaName: String) :
|
||||||
|
Filter.Text(displayName) {
|
||||||
|
val getAreaName: String
|
||||||
|
get() = areaName
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFilterListInternal(): FilterList = FilterList(
|
||||||
|
SortFilter(),
|
||||||
Filter.Header("Separate tags with commas (,)"),
|
Filter.Header("Separate tags with commas (,)"),
|
||||||
Filter.Header("Prepend with dash (-) to exclude"),
|
Filter.Header("Prepend with dash (-) to exclude"),
|
||||||
TextFilter("Groups", "group"),
|
AreaFilter("Artist(s)", "artist"),
|
||||||
TextFilter("Artists", "artist"),
|
AreaFilter("Character(s)", "character"),
|
||||||
TextFilter("Series", "series"),
|
AreaFilter("Group(s)", "group"),
|
||||||
TextFilter("Characters", "character"),
|
AreaFilter("Series", "series"),
|
||||||
TextFilter("Male Tags", "male"),
|
AreaFilter("Female Tag(s)", "female"),
|
||||||
TextFilter("Female Tags", "female"),
|
AreaFilter("Male Tag(s)", "male"),
|
||||||
Filter.Header("Please don't put Female/Male tags here, they won't work!"),
|
Filter.Header("Don't put Female/Male tags here, they won't work!"),
|
||||||
TextFilter("Tags", "tag"),
|
AreaFilter("Tag(s)", "tag"),
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal open class TextFilter(name: String, val type: String) : Filter.Text(name)
|
|
||||||
internal open class SelectFilter(name: String, val vals: List<Triple<String, String?, String>>, state: Int = 0) :
|
|
||||||
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
|
|
||||||
fun getArea() = vals[state].second
|
|
||||||
fun getValue() = vals[state].third
|
|
||||||
}
|
|
||||||
internal class TypeFilter(name: String) :
|
|
||||||
Filter.Group<CheckBoxFilter>(
|
|
||||||
name,
|
|
||||||
listOf(
|
|
||||||
Pair("Anime", "anime"),
|
|
||||||
Pair("Artist CG", "artistcg"),
|
|
||||||
Pair("Doujinshi", "doujinshi"),
|
|
||||||
Pair("Game CG", "gamecg"),
|
|
||||||
Pair("Image Set", "imageset"),
|
|
||||||
Pair("Manga", "manga"),
|
|
||||||
).map { CheckBoxFilter(it.first, it.second, true) },
|
|
||||||
)
|
|
||||||
internal open class CheckBoxFilter(name: String, val value: String, state: Boolean) : Filter.CheckBox(name, state)
|
|
||||||
|
|
||||||
private val getSortsList: List<Triple<String, String?, String>> = listOf(
|
|
||||||
Triple("Date Added", null, "index"),
|
|
||||||
Triple("Date Published", "date", "published"),
|
|
||||||
Triple("Popular: Today", "popular", "today"),
|
|
||||||
Triple("Popular: Week", "popular", "week"),
|
|
||||||
Triple("Popular: Month", "popular", "month"),
|
|
||||||
Triple("Popular: Year", "popular", "year"),
|
|
||||||
Triple("Random", "popular", "year"),
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -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.PreferenceScreen
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
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
|
||||||
|
@ -17,16 +22,16 @@ import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.CacheControl
|
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
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
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
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
|
||||||
|
@ -36,7 +41,7 @@ import kotlin.math.min
|
||||||
class Hitomi(
|
class Hitomi(
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
private val nozomiLang: String,
|
private val nozomiLang: String,
|
||||||
) : HttpSource() {
|
) : ConfigurableSource, HttpSource() {
|
||||||
|
|
||||||
override val name = "Hitomi"
|
override val name = "Hitomi"
|
||||||
|
|
||||||
|
@ -52,13 +57,19 @@ class Hitomi(
|
||||||
|
|
||||||
override val client = network.cloudflareClient
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var iconified = preferences.getBoolean(PREF_TAG_GENDER_ICON, false)
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.set("referer", "$baseUrl/")
|
.set("referer", "$baseUrl/")
|
||||||
.set("origin", baseUrl)
|
.set("origin", baseUrl)
|
||||||
|
|
||||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> = Observable.fromCallable {
|
override fun fetchPopularManga(page: Int): Observable<MangasPage> = Observable.fromCallable {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val entries = getGalleryIDsFromNozomi("popular", "year", nozomiLang, page.nextPageRange())
|
val entries = getGalleryIDsFromNozomi("popular", "today", nozomiLang, page.nextPageRange())
|
||||||
.toMangaList()
|
.toMangaList()
|
||||||
|
|
||||||
MangasPage(entries, entries.size >= 24)
|
MangasPage(entries, entries.size >= 24)
|
||||||
|
@ -77,23 +88,26 @@ class Hitomi(
|
||||||
private lateinit var searchResponse: List<Int>
|
private lateinit var searchResponse: List<Int>
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = Observable.fromCallable {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = Observable.fromCallable {
|
||||||
|
val parsedFilter = parseFilters(filters)
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
if (page == 1) {
|
if (page == 1) {
|
||||||
searchResponse = hitomiSearch(
|
searchResponse = hitomiSearch(
|
||||||
query.trim(),
|
"$query${parsedFilter.first}".trim(),
|
||||||
filters,
|
parsedFilter.second,
|
||||||
nozomiLang,
|
nozomiLang,
|
||||||
)
|
).toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val end = min(page * 25, searchResponse.size)
|
val end = min(page * 25, searchResponse.size)
|
||||||
val entries = searchResponse.subList((page - 1) * 25, end)
|
val entries = searchResponse.subList((page - 1) * 25, end)
|
||||||
.toMangaList()
|
.toMangaList()
|
||||||
MangasPage(entries, end < searchResponse.size)
|
|
||||||
|
MangasPage(entries, end != searchResponse.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFilterList() = getFilters()
|
override fun getFilterList(): FilterList = getFilterListInternal()
|
||||||
|
|
||||||
private fun Int.nextPageRange(): LongRange {
|
private fun Int.nextPageRange(): LongRange {
|
||||||
val byteOffset = ((this - 1) * 25) * 4L
|
val byteOffset = ((this - 1) * 25) * 4L
|
||||||
|
@ -101,73 +115,27 @@ class Hitomi(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getRangedResponse(url: String, range: LongRange?): ByteArray {
|
private suspend fun getRangedResponse(url: String, range: LongRange?): ByteArray {
|
||||||
val request = when (range) {
|
val rangeHeaders = when (range) {
|
||||||
null -> GET(url, headers)
|
null -> headers
|
||||||
else -> {
|
else -> headersBuilder()
|
||||||
val rangeHeaders = headersBuilder()
|
|
||||||
.set("Range", "bytes=${range.first}-${range.last}")
|
.set("Range", "bytes=${range.first}-${range.last}")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
GET(url, rangeHeaders, CacheControl.FORCE_NETWORK)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.newCall(request).awaitSuccess().use { it.body.bytes() }
|
return client.newCall(GET(url, rangeHeaders)).awaitSuccess().use { it.body.bytes() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun hitomiSearch(
|
private suspend fun hitomiSearch(
|
||||||
query: String,
|
query: String,
|
||||||
filters: FilterList,
|
order: OrderType,
|
||||||
language: String = "all",
|
language: String = "all",
|
||||||
): List<Int> =
|
): Set<Int> =
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
var sortBy: Pair<String?, String> = Pair(null, "index")
|
|
||||||
var random = false
|
|
||||||
|
|
||||||
val terms = query
|
val terms = query
|
||||||
.trim()
|
.trim()
|
||||||
|
.replace(Regex("""^\?"""), "")
|
||||||
.lowercase()
|
.lowercase()
|
||||||
.split(Regex("\\s+"))
|
.split(Regex("\\s+"))
|
||||||
.toMutableList()
|
|
||||||
|
|
||||||
filters.forEach {
|
|
||||||
when (it) {
|
|
||||||
is SelectFilter -> {
|
|
||||||
sortBy = Pair(it.getArea(), it.getValue())
|
|
||||||
random = (it.vals[it.state].first == "Random")
|
|
||||||
}
|
|
||||||
|
|
||||||
is TypeFilter -> {
|
|
||||||
val (activeFilter, inactiveFilters) = it.state.partition { stIt -> stIt.state }
|
|
||||||
terms += when {
|
|
||||||
inactiveFilters.size < 5 -> inactiveFilters.map { fil -> "-type:${fil.value}" }
|
|
||||||
inactiveFilters.size == 5 -> listOf("type:${activeFilter[0].value}")
|
|
||||||
else -> listOf("type: none")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is TextFilter -> {
|
|
||||||
if (it.state.isNotEmpty()) {
|
|
||||||
terms += it.state.split(",").filter(String::isNotBlank).map { tag ->
|
|
||||||
val trimmed = tag.trim()
|
|
||||||
buildString {
|
|
||||||
if (trimmed.startsWith('-')) {
|
|
||||||
append("-")
|
|
||||||
}
|
|
||||||
append(it.type)
|
|
||||||
append(":")
|
|
||||||
append(trimmed.lowercase().removePrefix("-"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (language != "all" && sortBy == Pair(null, "index") && !terms.any { it.contains(":") }) {
|
|
||||||
terms += "language:$language"
|
|
||||||
}
|
|
||||||
|
|
||||||
val positiveTerms = LinkedList<String>()
|
val positiveTerms = LinkedList<String>()
|
||||||
val negativeTerms = LinkedList<String>()
|
val negativeTerms = LinkedList<String>()
|
||||||
|
@ -182,35 +150,22 @@ class Hitomi(
|
||||||
|
|
||||||
val positiveResults = positiveTerms.map {
|
val positiveResults = positiveTerms.map {
|
||||||
async {
|
async {
|
||||||
try {
|
runCatching {
|
||||||
getGalleryIDsForQuery(it, language)
|
getGalleryIDsForQuery(it, language, order)
|
||||||
} catch (e: IllegalArgumentException) {
|
}.getOrDefault(emptySet())
|
||||||
if (e.message?.equals("HTTP error 404") == true) {
|
|
||||||
throw Exception("Unknown query: \"$it\"")
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val negativeResults = negativeTerms.map {
|
val negativeResults = negativeTerms.map {
|
||||||
async {
|
async {
|
||||||
try {
|
runCatching {
|
||||||
getGalleryIDsForQuery(it, language)
|
getGalleryIDsForQuery(it, language, order)
|
||||||
} catch (e: IllegalArgumentException) {
|
}.getOrDefault(emptySet())
|
||||||
if (e.message?.equals("HTTP error 404") == true) {
|
|
||||||
throw Exception("Unknown query: \"$it\"")
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val results = when {
|
val results = when {
|
||||||
positiveTerms.isEmpty() || sortBy != Pair(null, "index")
|
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(order.first, order.second, language)
|
||||||
-> getGalleryIDsFromNozomi(sortBy.first, sortBy.second, language)
|
|
||||||
else -> emptySet()
|
else -> emptySet()
|
||||||
}.toMutableSet()
|
}.toMutableSet()
|
||||||
|
|
||||||
|
@ -235,17 +190,14 @@ class Hitomi(
|
||||||
filterNegative(it.await())
|
filterNegative(it.await())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (random) {
|
results
|
||||||
results.toList().shuffled()
|
|
||||||
} else {
|
|
||||||
results.toList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// search.js
|
// search.js
|
||||||
private suspend fun getGalleryIDsForQuery(
|
private suspend fun getGalleryIDsForQuery(
|
||||||
query: String,
|
query: String,
|
||||||
language: String = "all",
|
language: String = "all",
|
||||||
|
order: OrderType,
|
||||||
): Set<Int> {
|
): Set<Int> {
|
||||||
query.replace("_", " ").let {
|
query.replace("_", " ").let {
|
||||||
if (it.indexOf(':') > -1) {
|
if (it.indexOf(':') > -1) {
|
||||||
|
@ -268,6 +220,20 @@ class Hitomi(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (area != null) {
|
||||||
|
if (order.first != null) {
|
||||||
|
area = "$area/${order.first}"
|
||||||
|
if (tag.isBlank()) {
|
||||||
|
tag = order.second
|
||||||
|
} else {
|
||||||
|
area = "$area/${order.second}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
area = order.first
|
||||||
|
tag = order.second
|
||||||
|
}
|
||||||
|
|
||||||
return getGalleryIDsFromNozomi(area, tag, lang)
|
return getGalleryIDsFromNozomi(area, tag, lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -469,18 +435,12 @@ class Hitomi(
|
||||||
private suspend fun Collection<Int>.toMangaList() = coroutineScope {
|
private suspend fun Collection<Int>.toMangaList() = coroutineScope {
|
||||||
map { id ->
|
map { id ->
|
||||||
async {
|
async {
|
||||||
try {
|
runCatching {
|
||||||
client.newCall(GET("$ltnUrl/galleries/$id.js", headers))
|
client.newCall(GET("$ltnUrl/galleries/$id.js", headers))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseScriptAs<Gallery>()
|
.parseScriptAs<Gallery>()
|
||||||
.toSManga()
|
.toSManga()
|
||||||
} catch (e: IllegalArgumentException) {
|
}.getOrNull()
|
||||||
if (e.message?.equals("HTTP error 404") == true) {
|
|
||||||
return@async null
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.awaitAll().filterNotNull()
|
}.awaitAll().filterNotNull()
|
||||||
}
|
}
|
||||||
|
@ -490,7 +450,7 @@ class Hitomi(
|
||||||
url = galleryurl
|
url = galleryurl
|
||||||
author = groups?.joinToString { it.formatted }
|
author = groups?.joinToString { it.formatted }
|
||||||
artist = artists?.joinToString { it.formatted }
|
artist = artists?.joinToString { it.formatted }
|
||||||
genre = tags?.joinToString { it.formatted }
|
genre = tags?.joinToString { it.getFormatted(iconified) }
|
||||||
thumbnail_url = files.first().let {
|
thumbnail_url = files.first().let {
|
||||||
val hash = it.hash
|
val hash = it.hash
|
||||||
val imageId = imageIdFromHash(hash)
|
val imageId = imageIdFromHash(hash)
|
||||||
|
@ -499,15 +459,14 @@ class Hitomi(
|
||||||
"https://${subDomain}tn.$domain/webpbigtn/${thumbPathFromHash(hash)}/$hash.webp"
|
"https://${subDomain}tn.$domain/webpbigtn/${thumbPathFromHash(hash)}/$hash.webp"
|
||||||
}
|
}
|
||||||
description = buildString {
|
description = buildString {
|
||||||
parodys?.joinToString { it.formatted }?.let {
|
|
||||||
append("Series: ", it, "\n")
|
|
||||||
}
|
|
||||||
characters?.joinToString { it.formatted }?.let {
|
characters?.joinToString { it.formatted }?.let {
|
||||||
append("Characters: ", it, "\n")
|
append("Characters: ", it, "\n")
|
||||||
}
|
}
|
||||||
append("Type: ", type, "\n")
|
parodys?.joinToString { it.formatted }?.let {
|
||||||
|
append("Parodies: ", it, "\n")
|
||||||
|
}
|
||||||
append("Pages: ", files.size, "\n")
|
append("Pages: ", files.size, "\n")
|
||||||
language?.let { append("Language: ", language) }
|
append("Language: ", language)
|
||||||
}
|
}
|
||||||
status = SManga.COMPLETED
|
status = SManga.COMPLETED
|
||||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||||
|
@ -528,21 +487,26 @@ class Hitomi(
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
|
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
|
val id = manga.url
|
||||||
|
.substringAfterLast("-")
|
||||||
|
.substringBefore(".")
|
||||||
|
|
||||||
|
return GET("$ltnUrl/galleries/$id.js#${manga.url}", headers)
|
||||||
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val gallery = response.parseScriptAs<Gallery>()
|
val gallery = response.parseScriptAs<Gallery>()
|
||||||
|
val mangaUrl = response.request.url.fragment!!
|
||||||
|
|
||||||
return listOf(
|
return listOf(
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
name = "Chapter"
|
name = "Chapter"
|
||||||
url = gallery.galleryurl
|
url = mangaUrl
|
||||||
scanlator = gallery.type
|
scanlator = gallery.type
|
||||||
date_upload = try {
|
date_upload = runCatching {
|
||||||
dateFormat.parse(gallery.date.substringBeforeLast("-"))!!.time
|
dateFormat.parse(gallery.date.substringBeforeLast("-"))!!.time
|
||||||
} catch (_: ParseException) {
|
}.getOrDefault(0L)
|
||||||
0L
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -561,9 +525,6 @@ class Hitomi(
|
||||||
|
|
||||||
override fun pageListParse(response: Response) = runBlocking {
|
override fun pageListParse(response: Response) = runBlocking {
|
||||||
val gallery = response.parseScriptAs<Gallery>()
|
val gallery = response.parseScriptAs<Gallery>()
|
||||||
val id = gallery.galleryurl
|
|
||||||
.substringAfterLast("-")
|
|
||||||
.substringBefore(".")
|
|
||||||
|
|
||||||
gallery.files.mapIndexed { idx, img ->
|
gallery.files.mapIndexed { idx, img ->
|
||||||
val hash = img.hash
|
val hash = img.hash
|
||||||
|
@ -659,9 +620,28 @@ class Hitomi(
|
||||||
|
|
||||||
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 setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
SwitchPreferenceCompat(screen.context).apply {
|
||||||
|
key = PREF_TAG_GENDER_ICON
|
||||||
|
title = "Show gender as text or icon in tags (requires refresh)"
|
||||||
|
summaryOff = "Show gender as text"
|
||||||
|
summaryOn = "Show gender as icon"
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
iconified = newValue == true
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
}
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||||
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
|
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
|
||||||
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 {
|
||||||
|
private const val PREF_TAG_GENDER_ICON = "pref_tag_gender_icon"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,12 @@ import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Gallery(
|
data class Gallery(
|
||||||
val galleryurl: String,
|
val galleryurl: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val date: String,
|
val date: String,
|
||||||
val type: String?,
|
val type: String,
|
||||||
val language: String?,
|
val language: String,
|
||||||
val tags: List<Tag>?,
|
val tags: List<Tag>?,
|
||||||
val artists: List<Artist>?,
|
val artists: List<Artist>?,
|
||||||
val groups: List<Group>?,
|
val groups: List<Group>?,
|
||||||
|
@ -19,49 +19,49 @@ class Gallery(
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class ImageFile(
|
data class ImageFile(
|
||||||
val hash: String,
|
val hash: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Tag(
|
data class Tag(
|
||||||
private val female: JsonPrimitive?,
|
val female: JsonPrimitive?,
|
||||||
private val male: JsonPrimitive?,
|
val male: JsonPrimitive?,
|
||||||
private val tag: String,
|
val tag: String,
|
||||||
) {
|
) {
|
||||||
val formatted get() = if (female?.content == "1") {
|
fun getFormatted(iconified: Boolean) = if (female?.content == "1") {
|
||||||
tag.toCamelCase() + " ♀"
|
tag.toCamelCase() + if (iconified) " ♀" else " (Female)"
|
||||||
} else if (male?.content == "1") {
|
} else if (male?.content == "1") {
|
||||||
tag.toCamelCase() + " ♂"
|
tag.toCamelCase() + if (iconified) " ♂" else " (Male)"
|
||||||
} else {
|
} else {
|
||||||
tag.toCamelCase()
|
tag.toCamelCase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Artist(
|
data class Artist(
|
||||||
private val artist: String,
|
val artist: String,
|
||||||
) {
|
) {
|
||||||
val formatted get() = artist.toCamelCase()
|
val formatted get() = artist.toCamelCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Group(
|
data class Group(
|
||||||
private val group: String,
|
val group: String,
|
||||||
) {
|
) {
|
||||||
val formatted get() = group.toCamelCase()
|
val formatted get() = group.toCamelCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Character(
|
data class Character(
|
||||||
private val character: String,
|
val character: String,
|
||||||
) {
|
) {
|
||||||
val formatted get() = character.toCamelCase()
|
val formatted get() = character.toCamelCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Parody(
|
data class Parody(
|
||||||
private val parody: String,
|
val parody: String,
|
||||||
) {
|
) {
|
||||||
val formatted get() = parody.toCamelCase()
|
val formatted get() = parody.toCamelCase()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.MangaFireFactory'
|
extClass = '.MangaFireFactory'
|
||||||
themePkg = 'mangareader'
|
themePkg = 'mangareader'
|
||||||
baseUrl = 'https://mangafire.to'
|
baseUrl = 'https://mangafire.to'
|
||||||
overrideVersionCode = 5
|
overrideVersionCode = 3
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -138,13 +138,13 @@ open class MangaFire(
|
||||||
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
|
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
|
||||||
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
|
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
|
||||||
val document = Jsoup.parse(result)
|
val document = Jsoup.parse(result)
|
||||||
val selector = if (isVolume) "div.unit" else "ul li"
|
|
||||||
val elements = document.select(selector)
|
val elements = document.select("ul li")
|
||||||
if (elements.size > 0) {
|
if (elements.size > 0) {
|
||||||
val linkToFirstChapter = elements[0].selectFirst(Evaluator.Tag("a"))!!.attr("href")
|
val linkToFirstChapter = elements[0].selectFirst(Evaluator.Tag("a"))!!.attr("href")
|
||||||
val mangaId = linkToFirstChapter.toString().substringAfter('.').substringBefore('/')
|
val mangaId = linkToFirstChapter.toString().substringAfter('.').substringBefore('/')
|
||||||
val type = if (isVolume) volumeType else chapterType
|
|
||||||
val request = GET("$baseUrl/ajax/read/$mangaId/$type/$langCode", headers)
|
val request = GET("$baseUrl/ajax/read/$mangaId/chapter/$langCode", headers)
|
||||||
val response = client.newCall(request).execute()
|
val response = client.newCall(request).execute()
|
||||||
val res = json.decodeFromString<ResponseDto<ChapterIdsDto>>(response.body.string()).result.html
|
val res = json.decodeFromString<ResponseDto<ChapterIdsDto>>(response.body.string()).result.html
|
||||||
val chapterInfoDocument = Jsoup.parse(res)
|
val chapterInfoDocument = Jsoup.parse(res)
|
||||||
|
@ -177,7 +177,6 @@ open class MangaFire(
|
||||||
val element = elements[i]
|
val element = elements[i]
|
||||||
val number = element.attr("data-number").toFloatOrNull() ?: -1f
|
val number = element.attr("data-number").toFloatOrNull() ?: -1f
|
||||||
if (chapter.chapter_number != number) throw Exception("Chapter number doesn't match. Try updating again.")
|
if (chapter.chapter_number != number) throw Exception("Chapter number doesn't match. Try updating again.")
|
||||||
chapter.name = element.select(Evaluator.Tag("span"))[0].ownText()
|
|
||||||
val date = element.select(Evaluator.Tag("span"))[1].ownText()
|
val date = element.select(Evaluator.Tag("span"))[1].ownText()
|
||||||
chapter.date_upload = try {
|
chapter.date_upload = try {
|
||||||
dateFormat.parse(date)!!.time
|
dateFormat.parse(date)!!.time
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'MangaPark'
|
extName = 'MangaPark'
|
||||||
extClass = '.MangaParkFactory'
|
extClass = '.MangaParkFactory'
|
||||||
extVersionCode = 20
|
extVersionCode = 19
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import android.app.Application
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
|
||||||
import eu.kanade.tachiyomi.lib.cookieinterceptor.CookieInterceptor
|
import eu.kanade.tachiyomi.lib.cookieinterceptor.CookieInterceptor
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
@ -18,14 +17,10 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
@ -33,8 +28,6 @@ import okhttp3.Response
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
|
|
||||||
class MangaPark(
|
class MangaPark(
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
|
@ -60,7 +53,6 @@ class MangaPark(
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
.addInterceptor(::siteSettingsInterceptor)
|
|
||||||
.addNetworkInterceptor(CookieInterceptor(domain, "nsfw" to "2"))
|
.addNetworkInterceptor(CookieInterceptor(domain, "nsfw" to "2"))
|
||||||
.rateLimitHost(apiUrl.toHttpUrl(), 1)
|
.rateLimitHost(apiUrl.toHttpUrl(), 1)
|
||||||
.build()
|
.build()
|
||||||
|
@ -98,6 +90,8 @@ class MangaPark(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
runCatching(::getGenres)
|
||||||
|
|
||||||
val result = response.parseAs<SearchResponse>()
|
val result = response.parseAs<SearchResponse>()
|
||||||
|
|
||||||
val entries = result.data.searchComics.items.map { it.data.toSManga() }
|
val entries = result.data.searchComics.items.map { it.data.toSManga() }
|
||||||
|
@ -132,10 +126,6 @@ class MangaPark(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFilterList(): FilterList {
|
override fun getFilterList(): FilterList {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
runCatching(::getGenres)
|
|
||||||
}
|
|
||||||
|
|
||||||
val filters = mutableListOf<Filter<*>>(
|
val filters = mutableListOf<Filter<*>>(
|
||||||
SortFilter(),
|
SortFilter(),
|
||||||
OriginalLanguageFilter(),
|
OriginalLanguageFilter(),
|
||||||
|
@ -185,13 +175,7 @@ class MangaPark(
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val result = response.parseAs<ChapterListResponse>()
|
val result = response.parseAs<ChapterListResponse>()
|
||||||
|
|
||||||
return if (preference.getBoolean(DUPLICATE_CHAPTER_PREF_KEY, false)) {
|
return result.data.chapterList.map { it.data.toSChapter() }.reversed()
|
||||||
result.data.chapterList.flatMap {
|
|
||||||
it.data.dupChapters.map { it.data.toSChapter() }
|
|
||||||
}.reversed()
|
|
||||||
} else {
|
|
||||||
result.data.chapterList.map { it.data.toSChapter() }.reversed()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast("#")
|
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast("#")
|
||||||
|
@ -227,13 +211,6 @@ class MangaPark(
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = DUPLICATE_CHAPTER_PREF_KEY
|
|
||||||
title = "Fetch Duplicate Chapters"
|
|
||||||
summary = "Refresh chapter list to apply changes"
|
|
||||||
setDefaultValue(false)
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T =
|
private inline fun <reified T> Response.parseAs(): T =
|
||||||
|
@ -245,35 +222,6 @@ class MangaPark(
|
||||||
private inline fun <reified T : Any> T.toJsonRequestBody() =
|
private inline fun <reified T : Any> T.toJsonRequestBody() =
|
||||||
json.encodeToString(this).toRequestBody(JSON_MEDIA_TYPE)
|
json.encodeToString(this).toRequestBody(JSON_MEDIA_TYPE)
|
||||||
|
|
||||||
private val cookiesNotSet = AtomicBoolean(true)
|
|
||||||
private val latch = CountDownLatch(1)
|
|
||||||
|
|
||||||
// sets necessary cookies to not block genres like `Hentai`
|
|
||||||
private fun siteSettingsInterceptor(chain: Interceptor.Chain): Response {
|
|
||||||
val request = chain.request()
|
|
||||||
|
|
||||||
val settingsUrl = "$baseUrl/aok/settings-save"
|
|
||||||
|
|
||||||
if (
|
|
||||||
request.url.toString() != settingsUrl &&
|
|
||||||
request.url.host == domain
|
|
||||||
) {
|
|
||||||
if (cookiesNotSet.getAndSet(false)) {
|
|
||||||
val payload =
|
|
||||||
"""{"data":{"general_autoLangs":[],"general_userLangs":[],"general_excGenres":[],"general_prefLangs":[]}}"""
|
|
||||||
.toRequestBody(JSON_MEDIA_TYPE)
|
|
||||||
|
|
||||||
client.newCall(POST(settingsUrl, headers, payload)).execute().close()
|
|
||||||
|
|
||||||
latch.countDown()
|
|
||||||
} else {
|
|
||||||
latch.await()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return chain.proceed(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String {
|
override fun imageUrlParse(response: Response): String {
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
@ -300,7 +248,5 @@ class MangaPark(
|
||||||
"parkmanga.org",
|
"parkmanga.org",
|
||||||
"mpark.to",
|
"mpark.to",
|
||||||
)
|
)
|
||||||
|
|
||||||
private const val DUPLICATE_CHAPTER_PREF_KEY = "pref_dup_chapters"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,34 +12,34 @@ typealias ChapterListResponse = Data<ChapterList>
|
||||||
typealias PageListResponse = Data<ChapterPages>
|
typealias PageListResponse = Data<ChapterPages>
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Data<T>(val data: T)
|
data class Data<T>(val data: T)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Items<T>(val items: List<T>)
|
data class Items<T>(val items: List<T>)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class SearchComics(
|
data class SearchComics(
|
||||||
@SerialName("get_searchComic") val searchComics: Items<Data<MangaParkComic>>,
|
@SerialName("get_searchComic") val searchComics: Items<Data<MangaParkComic>>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class ComicNode(
|
data class ComicNode(
|
||||||
@SerialName("get_comicNode") val comic: Data<MangaParkComic>,
|
@SerialName("get_comicNode") val comic: Data<MangaParkComic>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class MangaParkComic(
|
data class MangaParkComic(
|
||||||
private val id: String,
|
val id: String,
|
||||||
private val name: String,
|
val name: String,
|
||||||
private val altNames: List<String>? = null,
|
val altNames: List<String>? = null,
|
||||||
private val authors: List<String>? = null,
|
val authors: List<String>? = null,
|
||||||
private val artists: List<String>? = null,
|
val artists: List<String>? = null,
|
||||||
private val genres: List<String>? = null,
|
val genres: List<String>? = null,
|
||||||
private val originalStatus: String? = null,
|
val originalStatus: String? = null,
|
||||||
private val uploadStatus: String? = null,
|
val uploadStatus: String? = null,
|
||||||
private val summary: String? = null,
|
val summary: String? = null,
|
||||||
@SerialName("urlCoverOri") private val cover: String? = null,
|
@SerialName("urlCoverOri") val cover: String? = null,
|
||||||
private val urlPath: String,
|
val urlPath: String,
|
||||||
) {
|
) {
|
||||||
fun toSManga() = SManga.create().apply {
|
fun toSManga() = SManga.create().apply {
|
||||||
url = "$urlPath#$id"
|
url = "$urlPath#$id"
|
||||||
|
@ -100,21 +100,18 @@ class MangaParkComic(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class ChapterList(
|
data class ChapterList(
|
||||||
@SerialName("get_comicChapterList") val chapterList: List<Data<MangaParkChapter>>,
|
@SerialName("get_comicChapterList") val chapterList: List<Data<MangaParkChapter>>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class MangaParkChapter(
|
data class MangaParkChapter(
|
||||||
private val id: String,
|
val id: String,
|
||||||
@SerialName("dname") private val displayName: String,
|
@SerialName("dname") val displayName: String,
|
||||||
private val title: String? = null,
|
val title: String? = null,
|
||||||
private val dateCreate: Long? = null,
|
val dateCreate: Long? = null,
|
||||||
private val dateModify: Long? = null,
|
val dateModify: Long? = null,
|
||||||
private val urlPath: String,
|
val urlPath: String,
|
||||||
private val srcTitle: String? = null,
|
|
||||||
private val userNode: Data<Name>? = null,
|
|
||||||
val dupChapters: List<Data<MangaParkChapter>> = emptyList(),
|
|
||||||
) {
|
) {
|
||||||
fun toSChapter() = SChapter.create().apply {
|
fun toSChapter() = SChapter.create().apply {
|
||||||
url = "$urlPath#$id"
|
url = "$urlPath#$id"
|
||||||
|
@ -123,24 +120,20 @@ class MangaParkChapter(
|
||||||
title?.let { append(": ", it) }
|
title?.let { append(": ", it) }
|
||||||
}
|
}
|
||||||
date_upload = dateModify ?: dateCreate ?: 0L
|
date_upload = dateModify ?: dateCreate ?: 0L
|
||||||
scanlator = userNode?.data?.name ?: srcTitle ?: "Unknown"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Name(val name: String)
|
data class ChapterPages(
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class ChapterPages(
|
|
||||||
@SerialName("get_chapterNode") val chapterPages: Data<ImageFiles>,
|
@SerialName("get_chapterNode") val chapterPages: Data<ImageFiles>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class ImageFiles(
|
data class ImageFiles(
|
||||||
val imageFile: UrlList,
|
val imageFile: UrlList,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class UrlList(
|
data class UrlList(
|
||||||
val urlList: List<String>,
|
val urlList: List<String>,
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,28 +4,28 @@ import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class GraphQL<T>(
|
data class GraphQL<T>(
|
||||||
private val variables: T,
|
val variables: T,
|
||||||
private val query: String,
|
val query: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class SearchVariables(private val select: SearchPayload)
|
data class SearchVariables(val select: SearchPayload)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class SearchPayload(
|
data class SearchPayload(
|
||||||
@SerialName("word") private val query: String? = null,
|
@SerialName("word") val query: String? = null,
|
||||||
private val incGenres: List<String>? = null,
|
val incGenres: List<String>? = null,
|
||||||
private val excGenres: List<String>? = null,
|
val excGenres: List<String>? = null,
|
||||||
private val incTLangs: List<String>? = null,
|
val incTLangs: List<String>? = null,
|
||||||
private val incOLangs: List<String>? = null,
|
val incOLangs: List<String>? = null,
|
||||||
private val sortby: String? = null,
|
val sortby: String? = null,
|
||||||
private val chapCount: String? = null,
|
val chapCount: String? = null,
|
||||||
private val origStatus: String? = null,
|
val origStatus: String? = null,
|
||||||
private val siteStatus: String? = null,
|
val siteStatus: String? = null,
|
||||||
private val page: Int,
|
val page: Int,
|
||||||
private val size: Int,
|
val size: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class IdVariables(private val id: String)
|
data class IdVariables(val id: String)
|
||||||
|
|
|
@ -75,28 +75,6 @@ val CHAPTERS_QUERY = buildQuery {
|
||||||
dateModify
|
dateModify
|
||||||
dateCreate
|
dateCreate
|
||||||
urlPath
|
urlPath
|
||||||
srcTitle
|
|
||||||
userNode {
|
|
||||||
data {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dupChapters {
|
|
||||||
data {
|
|
||||||
id
|
|
||||||
dname
|
|
||||||
title
|
|
||||||
dateModify
|
|
||||||
dateCreate
|
|
||||||
urlPath
|
|
||||||
srcTitle
|
|
||||||
userNode {
|
|
||||||
data {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Meitua.top'
|
extName = 'Meitua.top'
|
||||||
extClass = '.MeituaTop'
|
extClass = '.MeituaTop'
|
||||||
extVersionCode = 6
|
extVersionCode = 5
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ class MeituaTop : HttpSource() {
|
||||||
override val lang = "all"
|
override val lang = "all"
|
||||||
override val supportsLatest = false
|
override val supportsLatest = false
|
||||||
|
|
||||||
override val baseUrl = "https://mt1.meitu1.sbs"
|
override val baseUrl = "https://meitu1.xyz"
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/arttype/0b-$page.html", headers)
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/arttype/0b-$page.html", headers)
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 17 KiB |
|
@ -1,253 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.pandachaika
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.Protocol
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.lang.String.CASE_INSENSITIVE_ORDER
|
|
||||||
import java.math.BigInteger
|
|
||||||
|
|
||||||
class PandaChaika(
|
|
||||||
override val lang: String = "all",
|
|
||||||
private val searchLang: String = "",
|
|
||||||
) : HttpSource() {
|
|
||||||
|
|
||||||
override val name = "PandaChaika"
|
|
||||||
|
|
||||||
override val baseUrl = "https://panda.chaika.moe"
|
|
||||||
|
|
||||||
private val baseSearchUrl = "$baseUrl/search"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient
|
|
||||||
.newBuilder()
|
|
||||||
.addInterceptor(::Intercept)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
// Popular
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
|
||||||
return GET("$baseSearchUrl/?tags=$searchLang&sort=rating&apply=&json=&page=$page", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response)
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
|
|
||||||
|
|
||||||
// Latest
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
return GET("$baseSearchUrl/?tags=$searchLang&sort=public_date&apply=&json=&page=$page", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parsePageRange(query: String, minPages: Int = 1, maxPages: Int = 9999): Pair<Int, Int> {
|
|
||||||
val num = query.filter(Char::isDigit).toIntOrNull() ?: -1
|
|
||||||
fun limitedNum(number: Int = num): Int = number.coerceIn(minPages, maxPages)
|
|
||||||
|
|
||||||
if (num < 0) return minPages to maxPages
|
|
||||||
return when (query.firstOrNull()) {
|
|
||||||
'<' -> 1 to if (query[1] == '=') limitedNum() else limitedNum(num + 1)
|
|
||||||
'>' -> limitedNum(if (query[1] == '=') num else num + 1) to maxPages
|
|
||||||
'=' -> when (query[1]) {
|
|
||||||
'>' -> limitedNum() to maxPages
|
|
||||||
'<' -> 1 to limitedNum(maxPages)
|
|
||||||
else -> limitedNum() to limitedNum()
|
|
||||||
}
|
|
||||||
else -> limitedNum() to limitedNum()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
val library = response.parseAs<ArchiveResponse>()
|
|
||||||
|
|
||||||
val mangas = library.archives.map(LongArchive::toSManga)
|
|
||||||
|
|
||||||
val hasNextPage = library.has_next
|
|
||||||
|
|
||||||
return MangasPage(mangas, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = baseSearchUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
val tags = mutableListOf<String>()
|
|
||||||
var reason = ""
|
|
||||||
var uploader = ""
|
|
||||||
var pagesMin = 1
|
|
||||||
var pagesMax = 9999
|
|
||||||
|
|
||||||
tags.add(searchLang)
|
|
||||||
|
|
||||||
filters.forEach {
|
|
||||||
when (it) {
|
|
||||||
is SortFilter -> {
|
|
||||||
addQueryParameter("sort", it.getValue())
|
|
||||||
addQueryParameter("asc_desc", if (it.state!!.ascending) "asc" else "desc")
|
|
||||||
}
|
|
||||||
|
|
||||||
is SelectFilter -> {
|
|
||||||
addQueryParameter("category", it.vals[it.state].replace("All", ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
is PageFilter -> {
|
|
||||||
if (it.state.isNotBlank()) {
|
|
||||||
val (min, max) = parsePageRange(it.state)
|
|
||||||
pagesMin = min
|
|
||||||
pagesMax = max
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is TextFilter -> {
|
|
||||||
if (it.state.isNotEmpty()) {
|
|
||||||
when (it.type) {
|
|
||||||
"reason" -> reason = it.state
|
|
||||||
"uploader" -> uploader = it.state
|
|
||||||
else -> {
|
|
||||||
it.state.split(",").filter(String::isNotBlank).map { tag ->
|
|
||||||
val trimmed = tag.trim()
|
|
||||||
tags.add(
|
|
||||||
buildString {
|
|
||||||
if (trimmed.startsWith('-')) append("-")
|
|
||||||
append(it.type)
|
|
||||||
if (it.type.isNotBlank()) append(":")
|
|
||||||
append(trimmed.lowercase().removePrefix("-"))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addQueryParameter("title", query)
|
|
||||||
addQueryParameter("tags", tags.joinToString())
|
|
||||||
addQueryParameter("filecount_from", pagesMin.toString())
|
|
||||||
addQueryParameter("filecount_to", pagesMax.toString())
|
|
||||||
addQueryParameter("reason", reason)
|
|
||||||
addQueryParameter("uploader", uploader)
|
|
||||||
addQueryParameter("page", page.toString())
|
|
||||||
addQueryParameter("apply", "")
|
|
||||||
addQueryParameter("json", "")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
|
||||||
return GET("$baseUrl/api?archive=${manga.url}", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList() = getFilters()
|
|
||||||
|
|
||||||
// Details
|
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
return Observable.just(manga.apply { initialized = true })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chapters
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val archive = response.parseAs<Archive>()
|
|
||||||
|
|
||||||
return listOf(
|
|
||||||
SChapter.create().apply {
|
|
||||||
name = "Chapter"
|
|
||||||
url = archive.download.substringBefore("/download/")
|
|
||||||
date_upload = archive.posted * 1000
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga) = "$baseUrl/archive/${manga.url}"
|
|
||||||
override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url}"
|
|
||||||
|
|
||||||
// Pages
|
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
|
||||||
fun List<String>.sort() = this.sortedWith(compareBy(CASE_INSENSITIVE_ORDER) { it })
|
|
||||||
val url = "$baseUrl${chapter.url}/download/"
|
|
||||||
val (fileType, contentLength) = getZipType(url)
|
|
||||||
|
|
||||||
val remoteZip = ZipHandler(url, client, headers, fileType, contentLength).populate()
|
|
||||||
val fileListing = remoteZip.files().sort()
|
|
||||||
|
|
||||||
val files = remoteZip.toJson()
|
|
||||||
return Observable.just(
|
|
||||||
fileListing.mapIndexed { index, filename ->
|
|
||||||
Page(index, imageUrl = "https://127.0.0.1/#$filename&$files")
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getZipType(url: String): Pair<String, BigInteger> {
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.headers(headers)
|
|
||||||
.method("HEAD", null)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val contentLength = (
|
|
||||||
client.newCall(request).execute().header("content-length")
|
|
||||||
?: throw Exception("Could not get Content-Length of URL")
|
|
||||||
)
|
|
||||||
.toBigInteger()
|
|
||||||
|
|
||||||
return (if (contentLength > Int.MAX_VALUE.toBigInteger()) "zip64" else "zip") to contentLength
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Intercept(chain: Interceptor.Chain): Response {
|
|
||||||
val url = chain.request().url.toString()
|
|
||||||
return if (url.startsWith("https://127.0.0.1/#")) {
|
|
||||||
val fragment = url.toHttpUrl().fragment!!
|
|
||||||
val remoteZip = fragment.substringAfter("&").parseAs<Zip>()
|
|
||||||
val filename = fragment.substringBefore("&")
|
|
||||||
|
|
||||||
val byteArray = remoteZip.fetch(filename, client)
|
|
||||||
var type = filename.substringAfterLast('.').lowercase()
|
|
||||||
type = if (type == "jpg") "jpeg" else type
|
|
||||||
|
|
||||||
Response.Builder().body(byteArray.toResponseBody("image/$type".toMediaType()))
|
|
||||||
.request(chain.request())
|
|
||||||
.protocol(Protocol.HTTP_1_0)
|
|
||||||
.code(200)
|
|
||||||
.message("")
|
|
||||||
.build()
|
|
||||||
} else {
|
|
||||||
chain.proceed(chain.request())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T {
|
|
||||||
return json.decodeFromString(body.string())
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified T> String.parseAs(): T {
|
|
||||||
return json.decodeFromString(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Zip.toJson(): String {
|
|
||||||
return json.encodeToString(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
|
||||||
override fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException()
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.pandachaika
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
val dateReformat = SimpleDateFormat("EEEE, d MMM yyyy HH:mm (z)", Locale.ENGLISH)
|
|
||||||
fun filterTags(include: String = "", exclude: List<String> = emptyList(), tags: List<String>): String {
|
|
||||||
return tags.filter { it.startsWith("$include:") && exclude.none { substring -> it.startsWith("$substring:") } }
|
|
||||||
.joinToString {
|
|
||||||
it.substringAfter(":").replace("_", " ").split(" ").joinToString(" ") { s ->
|
|
||||||
s.replaceFirstChar { sr ->
|
|
||||||
if (sr.isLowerCase()) sr.titlecase(Locale.getDefault()) else sr.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun getReadableSize(bytes: Double): String {
|
|
||||||
return when {
|
|
||||||
bytes >= 300 * 1024 * 1024 -> "${"%.2f".format(bytes / (1024.0 * 1024.0 * 1024.0))} GB"
|
|
||||||
bytes >= 100 * 1024 -> "${"%.2f".format(bytes / (1024.0 * 1024.0))} MB"
|
|
||||||
bytes >= 1024 -> "${"%.2f".format(bytes / (1024.0))} KB"
|
|
||||||
else -> "$bytes B"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Archive(
|
|
||||||
val download: String,
|
|
||||||
val posted: Long,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class LongArchive(
|
|
||||||
private val thumbnail: String,
|
|
||||||
private val title: String,
|
|
||||||
private val id: Int,
|
|
||||||
private val posted: Long?,
|
|
||||||
private val public_date: Long?,
|
|
||||||
private val filecount: Int,
|
|
||||||
private val filesize: Double,
|
|
||||||
private val tags: List<String>,
|
|
||||||
private val title_jpn: String?,
|
|
||||||
private val uploader: String,
|
|
||||||
) {
|
|
||||||
fun toSManga() = SManga.create().apply {
|
|
||||||
val groups = filterTags("group", tags = tags)
|
|
||||||
val artists = filterTags("artist", tags = tags)
|
|
||||||
val publishers = filterTags("publisher", tags = tags)
|
|
||||||
val male = filterTags("male", tags = tags)
|
|
||||||
val female = filterTags("female", tags = tags)
|
|
||||||
val others = filterTags(exclude = listOf("female", "male", "artist", "publisher", "group", "parody"), tags = tags)
|
|
||||||
val parodies = filterTags("parody", tags = tags)
|
|
||||||
url = id.toString()
|
|
||||||
title = this@LongArchive.title
|
|
||||||
thumbnail_url = thumbnail
|
|
||||||
author = groups.ifEmpty { artists }
|
|
||||||
artist = artists
|
|
||||||
genre = listOf(male, female, others).joinToString()
|
|
||||||
description = buildString {
|
|
||||||
append("Uploader: ", uploader.ifEmpty { "Anonymous" }, "\n")
|
|
||||||
publishers.takeIf { it.isNotBlank() }?.let {
|
|
||||||
append("Publishers: ", it, "\n\n")
|
|
||||||
}
|
|
||||||
parodies.takeIf { it.isNotBlank() }?.let {
|
|
||||||
append("Parodies: ", it, "\n\n")
|
|
||||||
}
|
|
||||||
male.takeIf { it.isNotBlank() }?.let {
|
|
||||||
append("Male tags: ", it, "\n\n")
|
|
||||||
}
|
|
||||||
female.takeIf { it.isNotBlank() }?.let {
|
|
||||||
append("Female tags: ", it, "\n\n")
|
|
||||||
}
|
|
||||||
others.takeIf { it.isNotBlank() }?.let {
|
|
||||||
append("Other tags: ", it, "\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
title_jpn?.let { append("Japanese Title: ", it, "\n") }
|
|
||||||
append("Pages: ", filecount, "\n")
|
|
||||||
append("File Size: ", getReadableSize(filesize), "\n")
|
|
||||||
|
|
||||||
try {
|
|
||||||
append("Public Date: ", dateReformat.format(Date(public_date!! * 1000)), "\n")
|
|
||||||
} catch (_: Exception) {}
|
|
||||||
try {
|
|
||||||
append("Posted: ", dateReformat.format(Date(posted!! * 1000)), "\n")
|
|
||||||
} catch (_: Exception) {}
|
|
||||||
}
|
|
||||||
status = SManga.COMPLETED
|
|
||||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class ArchiveResponse(
|
|
||||||
val archives: List<LongArchive>,
|
|
||||||
val has_next: Boolean,
|
|
||||||
)
|
|
|
@ -1,29 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.pandachaika
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
|
|
||||||
class PandaChaikaFactory : SourceFactory {
|
|
||||||
override fun createSources(): List<Source> = listOf(
|
|
||||||
PandaChaika(),
|
|
||||||
PandaChaika("en", "english"),
|
|
||||||
PandaChaika("zh", "chinese"),
|
|
||||||
PandaChaika("ko", "korean"),
|
|
||||||
PandaChaika("es", "spanish"),
|
|
||||||
PandaChaika("ru", "russian"),
|
|
||||||
PandaChaika("pt", "portuguese"),
|
|
||||||
PandaChaika("fr", "french"),
|
|
||||||
PandaChaika("th", "thai"),
|
|
||||||
PandaChaika("vi", "vietnamese"),
|
|
||||||
PandaChaika("ja", "japanese"),
|
|
||||||
PandaChaika("id", "indonesian"),
|
|
||||||
PandaChaika("ar", "arabic"),
|
|
||||||
PandaChaika("uk", "ukrainian"),
|
|
||||||
PandaChaika("tr", "turkish"),
|
|
||||||
PandaChaika("cs", "czech"),
|
|
||||||
PandaChaika("tl", "tagalog"),
|
|
||||||
PandaChaika("fi", "finnish"),
|
|
||||||
PandaChaika("jv", "javanese"),
|
|
||||||
PandaChaika("el", "greek"),
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.pandachaika
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter.Sort.Selection
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
|
|
||||||
fun getFilters(): FilterList {
|
|
||||||
return FilterList(
|
|
||||||
SortFilter("Sort by", Selection(0, false), getSortsList),
|
|
||||||
SelectFilter("Types", getTypes),
|
|
||||||
Filter.Separator(),
|
|
||||||
Filter.Header("Separate tags with commas (,)"),
|
|
||||||
Filter.Header("Prepend with dash (-) to exclude"),
|
|
||||||
Filter.Header("Use 'Male Tags' or 'Female Tags' for specific categories. 'Tags' searches all categories."),
|
|
||||||
TextFilter("Tags", ""),
|
|
||||||
TextFilter("Male Tags", "male"),
|
|
||||||
TextFilter("Female Tags", "female"),
|
|
||||||
TextFilter("Artists", "artist"),
|
|
||||||
TextFilter("Parodies", "parody"),
|
|
||||||
Filter.Separator(),
|
|
||||||
TextFilter("Reason", "reason"),
|
|
||||||
TextFilter("Uploader", "reason"),
|
|
||||||
Filter.Separator(),
|
|
||||||
Filter.Header("Filter by pages, for example: (>20)"),
|
|
||||||
PageFilter("Pages"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal open class PageFilter(name: String) : Filter.Text(name)
|
|
||||||
|
|
||||||
internal open class TextFilter(name: String, val type: String) : Filter.Text(name)
|
|
||||||
|
|
||||||
internal open class SelectFilter(name: String, val vals: List<String>, state: Int = 0) :
|
|
||||||
Filter.Select<String>(name, vals.map { it }.toTypedArray(), state)
|
|
||||||
|
|
||||||
internal open class SortFilter(name: String, selection: Selection, private val vals: List<Pair<String, String>>) :
|
|
||||||
Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) {
|
|
||||||
fun getValue() = vals[state!!.index].second
|
|
||||||
}
|
|
||||||
|
|
||||||
private val getTypes = listOf(
|
|
||||||
"All",
|
|
||||||
"Doujinshi",
|
|
||||||
"Manga",
|
|
||||||
"Image Set",
|
|
||||||
"Artist CG",
|
|
||||||
"Game CG",
|
|
||||||
"Western",
|
|
||||||
"Non-H",
|
|
||||||
"Misc",
|
|
||||||
)
|
|
||||||
|
|
||||||
private val getSortsList: List<Pair<String, String>> = listOf(
|
|
||||||
Pair("Public Date", "public_date"),
|
|
||||||
Pair("Posted Date", "posted_date"),
|
|
||||||
Pair("Title", "title"),
|
|
||||||
Pair("Japanese Title", "title_jpn"),
|
|
||||||
Pair("Rating", "rating"),
|
|
||||||
Pair("Images", "images"),
|
|
||||||
Pair("File Size", "size"),
|
|
||||||
Pair("Category", "category"),
|
|
||||||
)
|
|
|
@ -1,287 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.pandachaika
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.inflateRaw
|
|
||||||
import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.parseAllCDs
|
|
||||||
import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.parseEOCD
|
|
||||||
import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.parseEOCD64
|
|
||||||
import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.parseLocalFile
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.math.BigInteger
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder.LITTLE_ENDIAN
|
|
||||||
import java.util.zip.Inflater
|
|
||||||
import kotlin.text.Charsets.UTF_8
|
|
||||||
|
|
||||||
const val CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE = 0x02014b50
|
|
||||||
const val END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50
|
|
||||||
const val END_OF_CENTRAL_DIRECTORY_64_SIGNATURE = 0x06064b50
|
|
||||||
const val LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50
|
|
||||||
|
|
||||||
class EndOfCentralDirectory(
|
|
||||||
val centralDirectoryByteSize: BigInteger,
|
|
||||||
val centralDirectoryByteOffset: BigInteger,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class CentralDirectoryRecord(
|
|
||||||
val length: Int,
|
|
||||||
val compressedSize: Int,
|
|
||||||
val localFileHeaderRelativeOffset: Int,
|
|
||||||
val filename: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
class LocalFileHeader(
|
|
||||||
val compressedData: ByteArray,
|
|
||||||
val compressionMethod: Int,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Zip(
|
|
||||||
private val url: String,
|
|
||||||
private val centralDirectoryRecords: List<CentralDirectoryRecord>,
|
|
||||||
) {
|
|
||||||
fun files(): List<String> {
|
|
||||||
return centralDirectoryRecords.map {
|
|
||||||
it.filename
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetch(path: String, client: OkHttpClient): ByteArray {
|
|
||||||
val file = centralDirectoryRecords.find { it.filename == path }
|
|
||||||
?: throw Exception("File not found in ZIP: $path")
|
|
||||||
|
|
||||||
val MAX_LOCAL_FILE_HEADER_SIZE = 256 + 32 + 30 + 100
|
|
||||||
|
|
||||||
val headersBuilder = Headers.Builder()
|
|
||||||
.set(
|
|
||||||
"Range",
|
|
||||||
"bytes=${file.localFileHeaderRelativeOffset}-${
|
|
||||||
file.localFileHeaderRelativeOffset +
|
|
||||||
file.compressedSize +
|
|
||||||
MAX_LOCAL_FILE_HEADER_SIZE
|
|
||||||
}",
|
|
||||||
).build()
|
|
||||||
|
|
||||||
val request = GET(url, headersBuilder)
|
|
||||||
|
|
||||||
val response = client.newCall(request).execute()
|
|
||||||
|
|
||||||
val byteArray = response.body.byteStream().use { it.readBytes() }
|
|
||||||
|
|
||||||
val localFile = parseLocalFile(byteArray, file.compressedSize)
|
|
||||||
?: throw Exception("Failed to parse local file header in ZIP")
|
|
||||||
|
|
||||||
return if (localFile.compressionMethod == 0) {
|
|
||||||
localFile.compressedData
|
|
||||||
} else {
|
|
||||||
inflateRaw(localFile.compressedData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ZipHandler(
|
|
||||||
private val url: String,
|
|
||||||
private val client: OkHttpClient,
|
|
||||||
private val additionalHeaders: Headers = Headers.Builder().build(),
|
|
||||||
private val zipType: String = "zip",
|
|
||||||
private val contentLength: BigInteger,
|
|
||||||
) {
|
|
||||||
fun populate(): Zip {
|
|
||||||
val endOfCentralDirectory = fetchEndOfCentralDirectory(contentLength, zipType)
|
|
||||||
val centralDirectoryRecords = fetchCentralDirectoryRecords(endOfCentralDirectory)
|
|
||||||
|
|
||||||
return Zip(
|
|
||||||
url,
|
|
||||||
centralDirectoryRecords,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchEndOfCentralDirectory(zipByteLength: BigInteger, zipType: String): EndOfCentralDirectory {
|
|
||||||
val EOCD_MAX_BYTES = 128.toBigInteger()
|
|
||||||
val eocdInitialOffset = maxOf(0.toBigInteger(), zipByteLength - EOCD_MAX_BYTES)
|
|
||||||
|
|
||||||
val headers = additionalHeaders
|
|
||||||
.newBuilder()
|
|
||||||
.set("Range", "bytes=$eocdInitialOffset-$zipByteLength")
|
|
||||||
.build()
|
|
||||||
val request = GET(url, headers)
|
|
||||||
|
|
||||||
val response = client.newCall(request).execute()
|
|
||||||
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
throw Exception("Could not fetch ZIP: HTTP status ${response.code}")
|
|
||||||
}
|
|
||||||
|
|
||||||
val eocdBuffer = response.body.byteStream().use { it.readBytes() }
|
|
||||||
|
|
||||||
if (eocdBuffer.isEmpty()) throw Exception("Could not get Range request to start looking for EOCD")
|
|
||||||
|
|
||||||
val eocd =
|
|
||||||
(if (zipType == "zip64") parseEOCD64(eocdBuffer) else parseEOCD(eocdBuffer))
|
|
||||||
?: throw Exception("Could not get EOCD record of the ZIP")
|
|
||||||
|
|
||||||
return eocd
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchCentralDirectoryRecords(endOfCentralDirectory: EndOfCentralDirectory): List<CentralDirectoryRecord> {
|
|
||||||
val headersBuilder = Headers.Builder()
|
|
||||||
.set(
|
|
||||||
"Range",
|
|
||||||
"bytes=${endOfCentralDirectory.centralDirectoryByteOffset}-${
|
|
||||||
endOfCentralDirectory.centralDirectoryByteOffset +
|
|
||||||
endOfCentralDirectory.centralDirectoryByteSize
|
|
||||||
}",
|
|
||||||
).build()
|
|
||||||
|
|
||||||
val request = GET(url, headersBuilder)
|
|
||||||
|
|
||||||
val response = client.newCall(request).execute()
|
|
||||||
|
|
||||||
val cdBuffer = response.body.byteStream().use { it.readBytes() }
|
|
||||||
|
|
||||||
return parseAllCDs(cdBuffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object ZipParser {
|
|
||||||
|
|
||||||
fun parseAllCDs(buffer: ByteArray): List<CentralDirectoryRecord> {
|
|
||||||
val cds = ArrayList<CentralDirectoryRecord>()
|
|
||||||
val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN)
|
|
||||||
|
|
||||||
var i = 0
|
|
||||||
while (i <= buffer.size - 4) {
|
|
||||||
val signature = view.getInt(i)
|
|
||||||
if (signature == CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE) {
|
|
||||||
val cd = parseCD(buffer.sliceArray(i until buffer.size))
|
|
||||||
if (cd != null) {
|
|
||||||
cds.add(cd)
|
|
||||||
i += cd.length - 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else if (signature == END_OF_CENTRAL_DIRECTORY_SIGNATURE) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
return cds
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseCD(buffer: ByteArray): CentralDirectoryRecord? {
|
|
||||||
val MIN_CD_LENGTH = 46
|
|
||||||
val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN)
|
|
||||||
|
|
||||||
for (i in 0..buffer.size - MIN_CD_LENGTH) {
|
|
||||||
if (view.getInt(i) == CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE) {
|
|
||||||
val filenameLength = view.getShort(i + 28).toInt()
|
|
||||||
val extraFieldLength = view.getShort(i + 30).toInt()
|
|
||||||
val fileCommentLength = view.getShort(i + 32).toInt()
|
|
||||||
|
|
||||||
return CentralDirectoryRecord(
|
|
||||||
length = 46 + filenameLength + extraFieldLength + fileCommentLength,
|
|
||||||
compressedSize = view.getInt(i + 20),
|
|
||||||
localFileHeaderRelativeOffset = view.getInt(i + 42),
|
|
||||||
filename = buffer.sliceArray(i + 46 until i + 46 + filenameLength).toString(UTF_8),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseEOCD(buffer: ByteArray): EndOfCentralDirectory? {
|
|
||||||
val MIN_EOCD_LENGTH = 22
|
|
||||||
val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN)
|
|
||||||
|
|
||||||
for (i in 0 until buffer.size - MIN_EOCD_LENGTH + 1) {
|
|
||||||
if (view.getInt(i) == END_OF_CENTRAL_DIRECTORY_SIGNATURE) {
|
|
||||||
return EndOfCentralDirectory(
|
|
||||||
centralDirectoryByteSize = view.getInt(i + 12).toBigInteger(),
|
|
||||||
centralDirectoryByteOffset = view.getInt(i + 16).toBigInteger(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseEOCD64(buffer: ByteArray): EndOfCentralDirectory? {
|
|
||||||
val MIN_EOCD_LENGTH = 56
|
|
||||||
val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN)
|
|
||||||
|
|
||||||
for (i in 0 until buffer.size - MIN_EOCD_LENGTH + 1) {
|
|
||||||
if (view.getInt(i) == END_OF_CENTRAL_DIRECTORY_64_SIGNATURE) {
|
|
||||||
return EndOfCentralDirectory(
|
|
||||||
centralDirectoryByteSize = view.getLong(i + 40).toBigInteger(),
|
|
||||||
centralDirectoryByteOffset = view.getLong(i + 48).toBigInteger(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseLocalFile(buffer: ByteArray, compressedSizeOverride: Int = 0): LocalFileHeader? {
|
|
||||||
val MIN_LOCAL_FILE_LENGTH = 30
|
|
||||||
|
|
||||||
val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN)
|
|
||||||
|
|
||||||
for (i in 0..buffer.size - MIN_LOCAL_FILE_LENGTH) {
|
|
||||||
if (view.getInt(i) == LOCAL_FILE_HEADER_SIGNATURE) {
|
|
||||||
val filenameLength = view.getShort(i + 26).toInt() and 0xFFFF
|
|
||||||
val extraFieldLength = view.getShort(i + 28).toInt() and 0xFFFF
|
|
||||||
|
|
||||||
val bitflags = view.getShort(i + 6).toInt() and 0xFFFF
|
|
||||||
val hasDataDescriptor = (bitflags shr 3) and 1 != 0
|
|
||||||
|
|
||||||
val headerEndOffset = i + 30 + filenameLength + extraFieldLength
|
|
||||||
val regularCompressedSize = view.getInt(i + 18)
|
|
||||||
|
|
||||||
val compressedData = if (hasDataDescriptor) {
|
|
||||||
buffer.copyOfRange(
|
|
||||||
headerEndOffset,
|
|
||||||
headerEndOffset + compressedSizeOverride,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
buffer.copyOfRange(
|
|
||||||
headerEndOffset,
|
|
||||||
headerEndOffset + regularCompressedSize,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return LocalFileHeader(
|
|
||||||
compressedData = compressedData,
|
|
||||||
compressionMethod = view.getShort(i + 8).toInt(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun inflateRaw(compressedData: ByteArray): ByteArray {
|
|
||||||
val inflater = Inflater(true)
|
|
||||||
inflater.setInput(compressedData)
|
|
||||||
|
|
||||||
val buffer = ByteArray(8192)
|
|
||||||
val output = ByteArrayOutputStream()
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (!inflater.finished()) {
|
|
||||||
val count = inflater.inflate(buffer)
|
|
||||||
if (count > 0) {
|
|
||||||
output.write(buffer, 0, count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw Exception("Invalid compressed data format: ${e.message}", e)
|
|
||||||
} finally {
|
|
||||||
inflater.end()
|
|
||||||
output.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return output.toByteArray()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Pururin'
|
extName = 'Pururin'
|
||||||
extClass = '.PururinFactory'
|
extClass = '.PururinFactory'
|
||||||
extVersionCode = 9
|
extVersionCode = 8
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,33 +7,27 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
abstract class Pururin(
|
abstract class Pururin(
|
||||||
override val lang: String = "all",
|
override val lang: String = "all",
|
||||||
private val searchLang: Pair<String, String>? = null,
|
private val searchLang: String? = null,
|
||||||
private val langPath: String = "",
|
private val langPath: String = "",
|
||||||
) : ParsedHttpSource() {
|
) : ParsedHttpSource() {
|
||||||
override val name = "Pururin"
|
override val name = "Pururin"
|
||||||
|
|
||||||
final override val baseUrl = "https://pururin.to"
|
override val baseUrl = "https://pururin.to"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override val client = network.cloudflareClient
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
// Popular
|
// Popular
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
return GET("$baseUrl/browse$langPath?sort=most-popular&page=$page", headers)
|
return GET("$baseUrl/browse$langPath?sort=most-popular&page=$page", headers)
|
||||||
}
|
}
|
||||||
|
@ -51,6 +45,7 @@ abstract class Pururin(
|
||||||
override fun popularMangaNextPageSelector(): String = ".page-item [rel=next]"
|
override fun popularMangaNextPageSelector(): String = ".page-item [rel=next]"
|
||||||
|
|
||||||
// Latest
|
// Latest
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
return GET("$baseUrl/browse$langPath?page=$page", headers)
|
return GET("$baseUrl/browse$langPath?page=$page", headers)
|
||||||
}
|
}
|
||||||
|
@ -63,131 +58,40 @@ abstract class Pururin(
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
|
|
||||||
private fun List<Pair<String, String>>.toValue(): String {
|
private fun List<String>.toValue(): String {
|
||||||
return "[${this.joinToString(",") { "{\"id\":${it.first},\"name\":\"${it.second}\"}" }}]"
|
return "[${this.joinToString(",")}]"
|
||||||
}
|
|
||||||
|
|
||||||
private fun parsePageRange(query: String, minPages: Int = 1, maxPages: Int = 9999): Pair<Int, Int> {
|
|
||||||
val num = query.filter(Char::isDigit).toIntOrNull() ?: -1
|
|
||||||
fun limitedNum(number: Int = num): Int = number.coerceIn(minPages, maxPages)
|
|
||||||
|
|
||||||
if (num < 0) return minPages to maxPages
|
|
||||||
return when (query.firstOrNull()) {
|
|
||||||
'<' -> 1 to if (query[1] == '=') limitedNum() else limitedNum(num + 1)
|
|
||||||
'>' -> limitedNum(if (query[1] == '=') num else num + 1) to maxPages
|
|
||||||
'=' -> when (query[1]) {
|
|
||||||
'>' -> limitedNum() to maxPages
|
|
||||||
'<' -> 1 to limitedNum(maxPages)
|
|
||||||
else -> limitedNum() to limitedNum()
|
|
||||||
}
|
|
||||||
else -> limitedNum() to limitedNum()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Tag(
|
|
||||||
val id: Int,
|
|
||||||
val name: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun findTagByNameSubstring(tags: List<Tag>, substring: String): Pair<String, String>? {
|
|
||||||
val tag = tags.find { it.name.contains(substring, ignoreCase = true) }
|
|
||||||
return tag?.let { Pair(tag.id.toString(), tag.name) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun tagSearch(tag: String, type: String): Pair<String, String>? {
|
|
||||||
val requestBody = FormBody.Builder()
|
|
||||||
.add("text", tag)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url("$baseUrl/api/get/tags/search")
|
|
||||||
.headers(headers)
|
|
||||||
.post(requestBody)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val response = client.newCall(request).execute()
|
|
||||||
return findTagByNameSubstring(response.parseAs<List<Tag>>(), type)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val includeTags = mutableListOf<Pair<String, String>>()
|
val includeTags = mutableListOf<String>()
|
||||||
val excludeTags = mutableListOf<Pair<String, String>>()
|
val excludeTags = mutableListOf<String>()
|
||||||
var pagesMin = 1
|
var pagesMin: Int
|
||||||
var pagesMax = 9999
|
var pagesMax: Int
|
||||||
var sortBy = "newest"
|
|
||||||
|
|
||||||
if (searchLang != null) includeTags.add(searchLang)
|
if (searchLang != null) includeTags.add(searchLang)
|
||||||
|
|
||||||
filters.forEach {
|
filters.filterIsInstance<TagGroup<*>>().map { group ->
|
||||||
when (it) {
|
group.state.map {
|
||||||
is SelectFilter -> sortBy = it.getValue()
|
if (it.isIncluded()) includeTags.add(it.id)
|
||||||
|
if (it.isExcluded()) excludeTags.add(it.id)
|
||||||
is TypeFilter -> {
|
|
||||||
val (_, inactiveFilters) = it.state.partition { stIt -> stIt.state }
|
|
||||||
excludeTags += inactiveFilters.map { fil -> Pair(fil.value, "${fil.name} [Category]") }
|
|
||||||
}
|
|
||||||
|
|
||||||
is PageFilter -> {
|
|
||||||
if (it.state.isNotEmpty()) {
|
|
||||||
val (min, max) = parsePageRange(it.state)
|
|
||||||
pagesMin = min
|
|
||||||
pagesMax = max
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is TextFilter -> {
|
filters.find<PagesGroup>().range.let {
|
||||||
if (it.state.isNotEmpty()) {
|
pagesMin = it.first
|
||||||
it.state.split(",").filter(String::isNotBlank).map { tag ->
|
pagesMax = it.last
|
||||||
val trimmed = tag.trim()
|
|
||||||
if (trimmed.startsWith('-')) {
|
|
||||||
tagSearch(trimmed.lowercase().removePrefix("-"), it.type)?.let { tagInfo ->
|
|
||||||
excludeTags.add(tagInfo)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tagSearch(trimmed.lowercase(), it.type)?.let { tagInfo ->
|
|
||||||
includeTags.add(tagInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Searching with just one tag usually gives wrong results
|
|
||||||
if (query.isEmpty()) {
|
|
||||||
when {
|
|
||||||
excludeTags.size == 1 && includeTags.isEmpty() -> excludeTags.addAll(excludeTags)
|
|
||||||
includeTags.size == 1 && excludeTags.isEmpty() -> {
|
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
addPathSegment("browse")
|
|
||||||
addPathSegment("tags")
|
|
||||||
addPathSegment("content")
|
|
||||||
addPathSegment(includeTags[0].first)
|
|
||||||
addQueryParameter("sort", sortBy)
|
|
||||||
addQueryParameter("start_page", pagesMin.toString())
|
|
||||||
addQueryParameter("last_page", pagesMax.toString())
|
|
||||||
if (page > 1) addQueryParameter("page", page.toString())
|
|
||||||
}.build()
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
addPathSegment("search")
|
addPathSegment("search")
|
||||||
addQueryParameter("q", query)
|
addQueryParameter("q", query)
|
||||||
addQueryParameter("sort", sortBy)
|
|
||||||
addQueryParameter("start_page", pagesMin.toString())
|
addQueryParameter("start_page", pagesMin.toString())
|
||||||
addQueryParameter("last_page", pagesMax.toString())
|
addQueryParameter("last_page", pagesMax.toString())
|
||||||
if (includeTags.isNotEmpty()) addQueryParameter("included_tags", includeTags.toValue())
|
if (includeTags.isNotEmpty()) addQueryParameter("included_tags", includeTags.toValue())
|
||||||
if (excludeTags.isNotEmpty()) addQueryParameter("excluded_tags", excludeTags.toValue())
|
if (excludeTags.isNotEmpty()) addQueryParameter("excluded_tags", excludeTags.toValue())
|
||||||
if (page > 1) addQueryParameter("page", page.toString())
|
if (page > 1) addQueryParameter("page", page.toString())
|
||||||
}.build()
|
}
|
||||||
|
return GET(url.build().toString(), headers)
|
||||||
return GET(url, headers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaSelector(): String = popularMangaSelector()
|
override fun searchMangaSelector(): String = popularMangaSelector()
|
||||||
|
@ -203,13 +107,8 @@ abstract class Pururin(
|
||||||
document.select(".box-gallery").let { e ->
|
document.select(".box-gallery").let { e ->
|
||||||
initialized = true
|
initialized = true
|
||||||
title = e.select(".title").text()
|
title = e.select(".title").text()
|
||||||
author = e.select("a[href*=/circle/]").text().ifEmpty { e.select("[itemprop=author]").text() }
|
author = e.select("[itemprop=author]").text()
|
||||||
artist = e.select("[itemprop=author]").text()
|
|
||||||
genre = e.select("a[href*=/content/]").text()
|
|
||||||
description = e.select(".box-gallery .table-info tr")
|
description = e.select(".box-gallery .table-info tr")
|
||||||
.filter { tr ->
|
|
||||||
tr.select("td").none { it.text().contains("content", ignoreCase = true) || it.text().contains("ratings", ignoreCase = true) }
|
|
||||||
}
|
|
||||||
.joinToString("\n") { tr ->
|
.joinToString("\n") { tr ->
|
||||||
tr.select("td")
|
tr.select("td")
|
||||||
.joinToString(": ") { it.text() }
|
.joinToString(": ") { it.text() }
|
||||||
|
@ -257,8 +156,8 @@ abstract class Pururin(
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T {
|
override fun getFilterList() = FilterList(
|
||||||
return json.decodeFromString(body.string())
|
CategoryGroup(),
|
||||||
}
|
PagesGroup(),
|
||||||
override fun getFilterList() = getFilters()
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,11 @@ class PururinFactory : SourceFactory {
|
||||||
class PururinAll : Pururin()
|
class PururinAll : Pururin()
|
||||||
class PururinEN : Pururin(
|
class PururinEN : Pururin(
|
||||||
"en",
|
"en",
|
||||||
Pair("13010", "english"),
|
"{\"id\":13010,\"name\":\"English [Language]\"}",
|
||||||
"/tags/language/13010/english",
|
"/tags/language/13010/english",
|
||||||
)
|
)
|
||||||
class PururinJA : Pururin(
|
class PururinJA : Pururin(
|
||||||
"ja",
|
"ja",
|
||||||
Pair("13011", "japanese"),
|
"{\"id\":13011,\"name\":\"Japanese [Language]\"}",
|
||||||
"/tags/language/13011/japanese",
|
"/tags/language/13011/japanese",
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,57 +1,57 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.pururin
|
package eu.kanade.tachiyomi.extension.all.pururin
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
|
|
||||||
fun getFilters(): FilterList {
|
sealed class TagFilter(
|
||||||
return FilterList(
|
name: String,
|
||||||
SelectFilter("Sort by", getSortsList),
|
val id: String,
|
||||||
TypeFilter("Types"),
|
) : Filter.TriState(name)
|
||||||
Filter.Separator(),
|
|
||||||
Filter.Header("Separate tags with commas (,)"),
|
sealed class TagGroup<T : TagFilter>(
|
||||||
Filter.Header("Prepend with dash (-) to exclude"),
|
name: String,
|
||||||
TextFilter("Tags", "[Content]"),
|
values: List<T>,
|
||||||
TextFilter("Artists", "[Artist]"),
|
) : Filter.Group<T>(name, values)
|
||||||
TextFilter("Circles", "[Circle]"),
|
|
||||||
TextFilter("Parodies", "[Parody]"),
|
class Category(name: String, id: String) : TagFilter(name, id)
|
||||||
TextFilter("Languages", "[Language]"),
|
|
||||||
TextFilter("Scanlators", "[Scanlator]"),
|
class CategoryGroup(
|
||||||
TextFilter("Conventions", "[Convention]"),
|
values: List<Category> = categories,
|
||||||
TextFilter("Collections", "[Collections]"),
|
) : TagGroup<Category>("Categories", values) {
|
||||||
TextFilter("Categories", "[Category]"),
|
companion object {
|
||||||
TextFilter("Uploaders", "[Uploader]"),
|
private val categories get() = listOf(
|
||||||
Filter.Separator(),
|
Category("Doujinshi", "{\"id\":13003,\"name\":\"Doujinshi [Category]\"}"),
|
||||||
Filter.Header("Filter by pages, for example: (>20)"),
|
Category("Manga", "{\"id\":13004,\"name\":\"Manga [Category]\"}"),
|
||||||
PageFilter("Pages"),
|
Category("Artist CG", "{\"id\":13006,\"name\":\"Artist CG [Category]\"}"),
|
||||||
|
Category("Game CG", "{\"id\":13008,\"name\":\"Game CG [Category]\"}"),
|
||||||
|
Category("Artbook", "{\"id\":17783,\"name\":\"Artbook [Category]\"}"),
|
||||||
|
Category("Webtoon", "{\"id\":27939,\"name\":\"Webtoon [Category]\"}"),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
internal class TypeFilter(name: String) :
|
|
||||||
Filter.Group<CheckBoxFilter>(
|
class PagesFilter(
|
||||||
name,
|
name: String,
|
||||||
listOf(
|
default: Int,
|
||||||
Pair("Artbook", "17783"),
|
values: Array<Int> = range,
|
||||||
Pair("Artist CG", "13004"),
|
) : Filter.Select<Int>(name, values, default) {
|
||||||
Pair("Doujinshi", "13003"),
|
companion object {
|
||||||
Pair("Game CG", "13008"),
|
private val range get() = Array(301) { it }
|
||||||
Pair("Manga", "13004"),
|
}
|
||||||
Pair("Webtoon", "27939"),
|
}
|
||||||
).map { CheckBoxFilter(it.first, it.second, true) },
|
|
||||||
|
class PagesGroup(
|
||||||
|
values: List<PagesFilter> = minmax,
|
||||||
|
) : Filter.Group<PagesFilter>("Pages", values) {
|
||||||
|
inline val range get() = IntRange(state[0].state, state[1].state).also {
|
||||||
|
require(it.first <= it.last) { "'Minimum' cannot exceed 'Maximum'" }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val minmax get() = listOf(
|
||||||
|
PagesFilter("Minimum", 0),
|
||||||
|
PagesFilter("Maximum", 300),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
internal open class CheckBoxFilter(name: String, val value: String, state: Boolean) : Filter.CheckBox(name, state)
|
|
||||||
|
|
||||||
internal open class PageFilter(name: String) : Filter.Text(name)
|
|
||||||
|
|
||||||
internal open class TextFilter(name: String, val type: String) : Filter.Text(name)
|
|
||||||
|
|
||||||
internal open class SelectFilter(name: String, val vals: List<Pair<String, String>>, state: Int = 0) :
|
|
||||||
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
|
|
||||||
fun getValue() = vals[state].second
|
|
||||||
}
|
}
|
||||||
private val getSortsList: List<Pair<String, String>> = listOf(
|
|
||||||
Pair("Newest", "newest"),
|
inline fun <reified T> List<Filter<*>>.find() = find { it is T } as T
|
||||||
Pair("Most Popular", "most-popular"),
|
|
||||||
Pair("Highest Rated", "highest-rated"),
|
|
||||||
Pair("Most Viewed", "most-viewed"),
|
|
||||||
Pair("Title", "title"),
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Union Mangas'
|
extName = 'Union Mangas'
|
||||||
extClass = '.UnionMangasFactory'
|
extClass = '.UnionMangasFactory'
|
||||||
extVersionCode = 5
|
extVersionCode = 3
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 41 KiB |
|
@ -1,5 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.unionmangas
|
package eu.kanade.tachiyomi.extension.all.unionmangas
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
@ -9,16 +10,22 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||||
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
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.security.MessageDigest
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
|
class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
|
||||||
override val lang = langOption.lang
|
override val lang = langOption.lang
|
||||||
|
@ -31,12 +38,39 @@ class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
val langApiInfix = when (lang) {
|
||||||
|
"it" -> langOption.infix
|
||||||
|
else -> "v3/po"
|
||||||
|
}
|
||||||
|
|
||||||
override val client = network.client.newBuilder()
|
override val client = network.client.newBuilder()
|
||||||
.rateLimit(2)
|
.rateLimit(5, 2, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
private fun apiHeaders(url: String): Headers {
|
||||||
.set("Referer", "$baseUrl/")
|
val date = apiDateFormat.format(Date())
|
||||||
|
val path = url.toUrlWithoutDomain()
|
||||||
|
|
||||||
|
return headersBuilder()
|
||||||
|
.add("_hash", authorization(apiSeed, domain, date))
|
||||||
|
.add("_tranId", authorization(apiSeed, domain, date, path))
|
||||||
|
.add("_date", date)
|
||||||
|
.add("_domain", domain)
|
||||||
|
.add("_path", path)
|
||||||
|
.add("Origin", baseUrl)
|
||||||
|
.add("Host", apiUrl.removeProtocol())
|
||||||
|
.add("Referer", "$baseUrl/")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun authorization(vararg payloads: String): String {
|
||||||
|
val md = MessageDigest.getInstance("MD5")
|
||||||
|
val bytes = payloads.joinToString("").toByteArray()
|
||||||
|
val digest = md.digest(bytes)
|
||||||
|
return digest
|
||||||
|
.fold("") { str, byte -> str + "%02x".format(byte) }
|
||||||
|
.padStart(32, '0')
|
||||||
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
|
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
@ -45,103 +79,97 @@ class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
|
||||||
var currentPage = 0
|
var currentPage = 0
|
||||||
do {
|
do {
|
||||||
val chaptersDto = fetchChapterListPageable(manga, currentPage)
|
val chaptersDto = fetchChapterListPageable(manga, currentPage)
|
||||||
chapters += chaptersDto.data.map { chapter ->
|
chapters += chaptersDto.toSChapter(langOption)
|
||||||
SChapter.create().apply {
|
|
||||||
name = chapter.name
|
|
||||||
date_upload = chapter.date.toDate()
|
|
||||||
url = chapter.toChapterUrl(langOption.infix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentPage++
|
currentPage++
|
||||||
} while (chaptersDto.hasNextPage())
|
} while (chaptersDto.hasNextPage())
|
||||||
return Observable.just(chapters)
|
return Observable.just(chapters.reversed())
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchChapterListPageable(manga: SManga, page: Int): Pageable<ChapterDto> {
|
|
||||||
manga.apply {
|
|
||||||
url = getURLCompatibility(url)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun fetchChapterListPageable(manga: SManga, page: Int): ChapterPageDto {
|
||||||
val maxResult = 16
|
val maxResult = 16
|
||||||
val url = "$apiUrl/${langOption.infix}/GetChapterListFilter/${manga.slug()}/$maxResult/$page/all/ASC"
|
val url = "$apiUrl/api/$langApiInfix/GetChapterListFilter/${manga.slug()}/$maxResult/$page/all/ASC"
|
||||||
return client.newCall(GET(url, headers)).execute()
|
return client.newCall(GET(url, apiHeaders(url))).execute()
|
||||||
.parseAs<Pageable<ChapterDto>>()
|
.parseAs<ChapterPageDto>()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
val nextData = response.parseNextData<LatestUpdateProps>()
|
||||||
|
val dto = nextData.data.latestUpdateDto
|
||||||
|
val mangas = dto.mangas.map { mangaParse(it, nextData.query) }
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
val maxResult = 24
|
|
||||||
val url = "$apiUrl/${langOption.infix}/HomeLastUpdate".toHttpUrl().newBuilder()
|
|
||||||
.addPathSegment("$maxResult")
|
|
||||||
.addPathSegment("${page - 1}")
|
|
||||||
.build()
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga): String {
|
|
||||||
manga.apply {
|
|
||||||
url = getURLCompatibility(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseUrl + manga.url.replace(langOption.infix, langOption.mangaSubstring)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
|
||||||
manga.apply {
|
|
||||||
url = getURLCompatibility(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
val url = "$apiUrl/${langOption.infix}/getInfoManga".toHttpUrl().newBuilder()
|
|
||||||
.addPathSegment(manga.slug())
|
|
||||||
.build()
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
|
||||||
val dto = response.parseAs<MangaDetailsDto>()
|
|
||||||
return mangaParse(dto.details)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
val chapterSlug = getURLCompatibility(chapter.url)
|
|
||||||
.substringAfter(langOption.infix)
|
|
||||||
|
|
||||||
val url = "$apiUrl/${langOption.infix}/GetImageChapter$chapterSlug"
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val location = response.request.url.toString()
|
|
||||||
val dto = response.parseAs<PageDto>()
|
|
||||||
return dto.pages.mapIndexed { index, url ->
|
|
||||||
Page(index, location, imageUrl = url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
|
||||||
val dto = response.parseAs<Pageable<MangaDto>>()
|
|
||||||
val mangas = dto.data.map(::mangaParse)
|
|
||||||
return MangasPage(
|
return MangasPage(
|
||||||
mangas = mangas,
|
mangas = mangas,
|
||||||
hasNextPage = dto.hasNextPage(),
|
hasNextPage = dto.hasNextPage(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
val maxResult = 24
|
val url = "$baseUrl/${langOption.infix}/latest-releases".toHttpUrl().newBuilder()
|
||||||
return GET("$apiUrl/${langOption.infix}/HomeTopFllow/$maxResult/${page - 1}")
|
.addQueryParameter("page", "$page")
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val maxResult = 20
|
|
||||||
val url = "$apiUrl/${langOption.infix}/QuickSearch/".toHttpUrl().newBuilder()
|
|
||||||
.addPathSegment(query)
|
|
||||||
.addPathSegment("$maxResult")
|
|
||||||
.build()
|
.build()
|
||||||
return GET(url, headers)
|
return GET(url, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
val nextData = response.parseNextData<MangaDetailsProps>()
|
||||||
|
val dto = nextData.data.mangaDetailsDto
|
||||||
|
return SManga.create().apply {
|
||||||
|
title = dto.title
|
||||||
|
genre = dto.genres
|
||||||
|
thumbnail_url = dto.thumbnailUrl
|
||||||
|
url = mangaUrlParse(dto.slug, nextData.query.type)
|
||||||
|
status = dto.status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val chaptersDto = decryptChapters(response)
|
||||||
|
return chaptersDto.images.mapIndexed { index, imageUrl ->
|
||||||
|
Page(index, imageUrl = imageUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decryptChapters(response: Response): ChaptersDto {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val password = findChapterPassword(document)
|
||||||
|
val pageListData = document.parseNextData<ChaptersProps>().data.pageListData
|
||||||
|
val decodedData = CryptoAES.decrypt(pageListData, password)
|
||||||
|
return ChaptersDto(
|
||||||
|
data = json.decodeFromString<ChaptersDto>(decodedData).data,
|
||||||
|
delimiter = langOption.pageDelimiter,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findChapterPassword(document: Document): String {
|
||||||
|
val regxPasswordUrl = """\/pages\/%5Btype%5D\/%5Bidmanga%5D\/%5Biddetail%5D-.+\.js""".toRegex()
|
||||||
|
val regxFindPassword = """AES\.decrypt\(\w+,"(?<password>[^"]+)"\)""".toRegex(RegexOption.MULTILINE)
|
||||||
|
val jsDecryptUrl = document.select("script")
|
||||||
|
.map { it.absUrl("src") }
|
||||||
|
.first { regxPasswordUrl.find(it) != null }
|
||||||
|
val jsDecrypt = client.newCall(GET(jsDecryptUrl, headers)).execute().asJsoup().html()
|
||||||
|
return regxFindPassword.find(jsDecrypt)?.groups?.get("password")!!.value.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val dto = response.parseNextData<PopularMangaProps>()
|
||||||
|
val mangas = dto.data.mangas.map { it.details }.map { mangaParse(it, dto.query) }
|
||||||
|
return MangasPage(
|
||||||
|
mangas = mangas,
|
||||||
|
hasNextPage = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/${langOption.infix}")
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val maxResult = 6
|
||||||
|
val url = "$apiUrl/api/$langApiInfix/searchforms/$maxResult/".toHttpUrl().newBuilder()
|
||||||
|
.addPathSegment(query)
|
||||||
|
.addPathSegment("${page - 1}")
|
||||||
|
.build()
|
||||||
|
return GET(url, apiHeaders(url.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
if (query.startsWith(SEARCH_PREFIX)) {
|
if (query.startsWith(SEARCH_PREFIX)) {
|
||||||
val url = "$baseUrl/${langOption.infix}/${query.substringAfter(SEARCH_PREFIX)}"
|
val url = "$baseUrl/${langOption.infix}/${query.substringAfter(SEARCH_PREFIX)}"
|
||||||
|
@ -157,54 +185,52 @@ class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
|
||||||
override fun imageUrlParse(response: Response): String = ""
|
override fun imageUrlParse(response: Response): String = ""
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
val dto = response.parseAs<SearchDto>()
|
val mangasDto = response.parseAs<MangaListDto>().apply {
|
||||||
|
currentPage = response.request.url.pathSegments.last()
|
||||||
|
}
|
||||||
|
|
||||||
return MangasPage(
|
return MangasPage(
|
||||||
dto.mangas.map(::mangaParse),
|
mangas = mangasDto.toSManga(langOption.infix),
|
||||||
false,
|
hasNextPage = mangasDto.hasNextPage(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
private inline fun <reified T> Response.parseNextData() = asJsoup().parseNextData<T>()
|
||||||
* Keeps compatibility with pt-BR previous version
|
|
||||||
* */
|
|
||||||
private fun getURLCompatibility(url: String): String {
|
|
||||||
val slugSuffix = "-br"
|
|
||||||
val mangaSubString = "manga-br"
|
|
||||||
|
|
||||||
val oldSlug = url.substringAfter(mangaSubString)
|
private inline fun <reified T> Document.parseNextData(): NextData<T> {
|
||||||
.substring(1)
|
val jsonContent = selectFirst("script#__NEXT_DATA__")!!.html()
|
||||||
.split("/")
|
return json.decodeFromString<NextData<T>>(jsonContent)
|
||||||
.first()
|
|
||||||
|
|
||||||
val newSlug = oldSlug.substringBeforeLast(slugSuffix)
|
|
||||||
|
|
||||||
return url.replace(oldSlug, newSlug)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T {
|
private inline fun <reified T> Response.parseAs(): T {
|
||||||
return json.decodeFromString(body.string())
|
return json.decodeFromString(body.string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.removeProtocol() = trim().replace("https://", "")
|
||||||
|
|
||||||
private fun SManga.slug() = this.url.split("/").last()
|
private fun SManga.slug() = this.url.split("/").last()
|
||||||
|
|
||||||
private fun mangaParse(dto: MangaDto): SManga {
|
private fun String.toUrlWithoutDomain() = trim().replace(apiUrl, "")
|
||||||
|
|
||||||
|
private fun mangaParse(dto: MangaDto, query: QueryDto): SManga {
|
||||||
return SManga.create().apply {
|
return SManga.create().apply {
|
||||||
title = dto.title
|
title = dto.title
|
||||||
thumbnail_url = dto.thumbnailUrl
|
thumbnail_url = dto.thumbnailUrl
|
||||||
status = dto.status
|
status = dto.status
|
||||||
url = "/${langOption.infix}/${dto.slug}"
|
url = mangaUrlParse(dto.slug, query.type)
|
||||||
genre = dto.genres
|
genre = dto.genres
|
||||||
initialized = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.toDate(): Long =
|
private fun mangaUrlParse(slug: String, pathSegment: String) = "/$pathSegment/$slug"
|
||||||
try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
|
|
||||||
|
|
||||||
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.unionmanga.xyz"
|
||||||
val oldApiUrl = "https://api.unionmanga.xyz"
|
val apiSeed = "8e0550790c94d6abc71d738959a88d209690dc86"
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
|
val domain = "yaoi-chan.xyz"
|
||||||
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
|
||||||
|
val apiDateFormat = SimpleDateFormat("EE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.ENGLISH)
|
||||||
|
.apply { timeZone = TimeZone.getTimeZone("GMT") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,68 +1,149 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.unionmangas
|
package eu.kanade.tachiyomi.extension.all.unionmangas
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class MangaDetailsDto(private val data: Props) {
|
class NextData<T>(val props: Props<T>, val query: QueryDto) {
|
||||||
val details: MangaDto get() = data.details
|
val data get() = props.pageProps
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Props(
|
|
||||||
@SerialName("infoDoc") val details: MangaDto,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
open class Pageable<T>(
|
class Props<T>(val pageProps: T)
|
||||||
var currentPage: Int,
|
|
||||||
var totalPage: Int,
|
@Serializable
|
||||||
val data: List<T>,
|
class PopularMangaProps(@SerialName("data_popular") val mangas: List<PopularMangaDto>)
|
||||||
) {
|
|
||||||
fun hasNextPage() = (currentPage + 1) <= totalPage
|
@Serializable
|
||||||
|
class LatestUpdateProps(@SerialName("data_lastuppdate") val latestUpdateDto: MangaListDto)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MangaDetailsProps(@SerialName("dataManga") val mangaDetailsDto: MangaDetailsDto)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChaptersProps(@SerialName("data") val pageListData: String)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
abstract class Pageable {
|
||||||
|
abstract var currentPage: String?
|
||||||
|
abstract var totalPage: Int
|
||||||
|
|
||||||
|
fun hasNextPage() =
|
||||||
|
try { (currentPage!!.toInt() + 1) < totalPage } catch (_: Exception) { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterPageDto(
|
||||||
|
val totalRecode: Int = 0,
|
||||||
|
override var currentPage: String?,
|
||||||
|
override var totalPage: Int,
|
||||||
|
@SerialName("data") val chapters: List<ChapterDto> = emptyList(),
|
||||||
|
) : Pageable() {
|
||||||
|
fun toSChapter(langOption: LanguageOption): List<SChapter> =
|
||||||
|
chapters.map { chapter ->
|
||||||
|
SChapter.create().apply {
|
||||||
|
name = chapter.name
|
||||||
|
date_upload = chapter.date.toDate()
|
||||||
|
url = "/${langOption.infix}${chapter.toChapterUrl(langOption.chpPrefix)}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.toDate(): Long =
|
||||||
|
try { UnionMangas.dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
|
||||||
|
|
||||||
|
private fun ChapterDto.toChapterUrl(prefix: String) = "/${this.slugManga}/$prefix-${this.id}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class ChapterDto(
|
class ChapterDto(
|
||||||
val date: String,
|
val date: String,
|
||||||
|
val slug: String,
|
||||||
@SerialName("idDoc") val slugManga: String,
|
@SerialName("idDoc") val slugManga: String,
|
||||||
@SerialName("idDetail") val id: String,
|
@SerialName("idDetail") val id: String,
|
||||||
@SerialName("nameChapter") val name: String,
|
@SerialName("nameChapter") val name: String,
|
||||||
) {
|
)
|
||||||
fun toChapterUrl(lang: String) = "/$lang/${this.slugManga}/$id"
|
|
||||||
|
@Serializable
|
||||||
|
class QueryDto(
|
||||||
|
val type: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MangaListDto(
|
||||||
|
override var currentPage: String?,
|
||||||
|
override var totalPage: Int,
|
||||||
|
@SerialName("data") val mangas: List<MangaDto>,
|
||||||
|
) : Pageable() {
|
||||||
|
fun toSManga(siteLang: String) = mangas.map { dto ->
|
||||||
|
SManga.create().apply {
|
||||||
|
title = dto.title
|
||||||
|
thumbnail_url = dto.thumbnailUrl
|
||||||
|
status = dto.status
|
||||||
|
url = mangaUrlParse(dto.slug, siteLang)
|
||||||
|
genre = dto.genres
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PopularMangaDto(
|
||||||
|
@SerialName("document") val details: MangaDto,
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class MangaDto(
|
class MangaDto(
|
||||||
@SerialName("name") val title: String,
|
@SerialName("name") val title: String,
|
||||||
@SerialName("image") private val _thumbnailUrl: String,
|
@SerialName("image") private val _thumbnailUrl: String,
|
||||||
@SerialName("idDoc") val slug: String,
|
@SerialName("idDoc") val slug: String,
|
||||||
@SerialName("genresName") val genres: String,
|
@SerialName("genres") private val _genres: String,
|
||||||
@SerialName("status") val _status: String,
|
@SerialName("status") val _status: String,
|
||||||
) {
|
) {
|
||||||
val thumbnailUrl get() = "${UnionMangas.oldApiUrl}$_thumbnailUrl"
|
val thumbnailUrl get() = "${UnionMangas.apiUrl}$_thumbnailUrl"
|
||||||
|
val genres get() = _genres.split(",").joinToString { it.trim() }
|
||||||
|
val status get() = toSMangaStatus(_status)
|
||||||
|
}
|
||||||
|
|
||||||
val status get() = when (_status) {
|
@Serializable
|
||||||
|
class MangaDetailsDto(
|
||||||
|
@SerialName("name") val title: String,
|
||||||
|
@SerialName("image") private val _thumbnailUrl: String,
|
||||||
|
@SerialName("idDoc") val slug: String,
|
||||||
|
@SerialName("lsgenres") private val _genres: List<Prop>,
|
||||||
|
@SerialName("lsstatus") private val _status: List<Prop>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val thumbnailUrl get() = "${UnionMangas.apiUrl}$_thumbnailUrl"
|
||||||
|
val genres get() = _genres.joinToString { it.name }
|
||||||
|
val status get() = toSMangaStatus(_status.firstOrNull()?.name ?: "")
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Prop(
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChaptersDto(
|
||||||
|
@SerialName("dataManga") val data: PageDto,
|
||||||
|
private var delimiter: String = "",
|
||||||
|
) {
|
||||||
|
val images get() = data.getImages(delimiter)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PageDto(
|
||||||
|
@SerialName("source") private val imgData: String,
|
||||||
|
) {
|
||||||
|
fun getImages(delimiter: String): List<String> = imgData.split(delimiter)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mangaUrlParse(slug: String, pathSegment: String) = "/$pathSegment/$slug"
|
||||||
|
|
||||||
|
private fun toSMangaStatus(status: String) =
|
||||||
|
when (status.lowercase()) {
|
||||||
"ongoing" -> SManga.ONGOING
|
"ongoing" -> SManga.ONGOING
|
||||||
"completed" -> SManga.COMPLETED
|
"completed" -> SManga.COMPLETED
|
||||||
else -> SManga.UNKNOWN
|
else -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class SearchDto(
|
|
||||||
@SerialName("data")
|
|
||||||
val mangas: List<MangaDto>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class PageDto(val `data`: Data) {
|
|
||||||
val pages: List<String> get() = `data`.detailDocuments.source.split("#")
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Data(@SerialName("detail_documents") val detailDocuments: DetailDocuments)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class DetailDocuments(val source: String)
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,9 +7,9 @@ class UnionMangasFactory : SourceFactory {
|
||||||
override fun createSources(): List<Source> = languages.map { UnionMangas(it) }
|
override fun createSources(): List<Source> = languages.map { UnionMangas(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
class LanguageOption(val lang: String, val infix: String = lang, val mangaSubstring: String = infix)
|
class LanguageOption(val lang: String, val infix: String = lang, val chpPrefix: String, val pageDelimiter: String)
|
||||||
|
|
||||||
val languages = listOf(
|
val languages = listOf(
|
||||||
LanguageOption("pt-BR", "manga-br"),
|
LanguageOption("it", "italy", "leer", ","),
|
||||||
LanguageOption("ru", "manga-ru", "mangas"),
|
LanguageOption("pt-BR", "manga-br", "cap", "#"),
|
||||||
)
|
)
|
||||||
|
|