Compare commits
No commits in common. "480cb9d780ba4c8d24393340a5b13a1e2b74f3ee" and "b3f274dab5dc8a521911d7dfc55f4e7746b432fd" have entirely different histories.
480cb9d780
...
b3f274dab5
@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
@ -13,9 +13,5 @@ pref_show_paid_chapter_title=Display paid chapters
|
|||||||
pref_show_paid_chapter_summary_on=Paid chapters will appear.
|
pref_show_paid_chapter_summary_on=Paid chapters will appear.
|
||||||
pref_show_paid_chapter_summary_off=Only free chapters will be displayed.
|
pref_show_paid_chapter_summary_off=Only free chapters will be displayed.
|
||||||
url_changed_error=The URL of the series has changed. Migrate from %s to %s to update the URL
|
url_changed_error=The URL of the series has changed. Migrate from %s to %s to update the URL
|
||||||
pref_username_title=Username/Email
|
|
||||||
pref_password_title=Password
|
|
||||||
pref_credentials_summary=Ignored if empty.
|
|
||||||
login_failed_unknown_error=Unknown error occurred while logging in
|
|
||||||
paid_chapter_error=Paid chapter unavailable.
|
paid_chapter_error=Paid chapter unavailable.
|
||||||
id_not_found_error=Failed to get the ID for slug: %s
|
id_not_found_error=Failed to get the ID for slug: %s
|
||||||
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 22
|
baseVersionCode = 21
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:i18n"))
|
api(project(":lib:i18n"))
|
||||||
|
@ -2,12 +2,10 @@ package eu.kanade.tachiyomi.multisrc.heancms
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.EditTextPreference
|
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import eu.kanade.tachiyomi.lib.i18n.Intl
|
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
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
|
||||||
@ -16,9 +14,7 @@ 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 kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@ -47,8 +43,6 @@ abstract class HeanCms(
|
|||||||
|
|
||||||
protected open val useNewChapterEndpoint = false
|
protected open val useNewChapterEndpoint = false
|
||||||
|
|
||||||
protected open val enableLogin = false
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Json instance to make usage of `encodeDefaults`,
|
* Custom Json instance to make usage of `encodeDefaults`,
|
||||||
* which is not enabled on the injected instance of the app.
|
* which is not enabled on the injected instance of the app.
|
||||||
@ -76,44 +70,6 @@ abstract class HeanCms(
|
|||||||
.add("Origin", baseUrl)
|
.add("Origin", baseUrl)
|
||||||
.add("Referer", "$baseUrl/")
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
private fun authHeaders(): Headers {
|
|
||||||
val builder = headersBuilder()
|
|
||||||
if (enableLogin && preferences.user.isNotEmpty() && preferences.password.isNotEmpty()) {
|
|
||||||
val tokenData = preferences.tokenData
|
|
||||||
val token = if (tokenData.isExpired(tokenExpiredAtDateFormat)) {
|
|
||||||
getToken()
|
|
||||||
} else {
|
|
||||||
tokenData.token
|
|
||||||
}
|
|
||||||
if (token != null) {
|
|
||||||
builder.add("Authorization", "Bearer $token")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getToken(): String? {
|
|
||||||
val body = FormBody.Builder()
|
|
||||||
.add("email", preferences.user)
|
|
||||||
.add("password", preferences.password)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val response = client.newCall(POST("$apiUrl/login", headers, body)).execute()
|
|
||||||
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
val result = response.parseAs<HeanCmsErrorsDto>()
|
|
||||||
val message = result.errors?.firstOrNull()?.message ?: intl["login_failed_unknown_error"]
|
|
||||||
|
|
||||||
throw Exception(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = response.parseAs<HeanCmsTokenPayloadDto>()
|
|
||||||
|
|
||||||
preferences.tokenData = result
|
|
||||||
|
|
||||||
return result.token
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
val url = "$apiUrl/query".toHttpUrl().newBuilder()
|
val url = "$apiUrl/query".toHttpUrl().newBuilder()
|
||||||
.addQueryParameter("query_string", "")
|
.addQueryParameter("query_string", "")
|
||||||
@ -321,30 +277,24 @@ abstract class HeanCms(
|
|||||||
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast("#")
|
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast("#")
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter) =
|
override fun pageListRequest(chapter: SChapter) =
|
||||||
GET(apiUrl + chapter.url.replace("/$mangaSubDirectory/", "/chapter/"), authHeaders())
|
GET(apiUrl + chapter.url.replace("/$mangaSubDirectory/", "/chapter/"), headers)
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val result = response.parseAs<HeanCmsPagePayloadDto>()
|
val result = response.parseAs<HeanCmsPagePayloadDto>()
|
||||||
|
|
||||||
if (result.isPaywalled() && result.chapter.chapterData == null) {
|
if (result.isPaywalled()) throw Exception(intl["paid_chapter_error"])
|
||||||
throw Exception(intl["paid_chapter_error"])
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (useNewChapterEndpoint) {
|
return if (useNewChapterEndpoint) {
|
||||||
result.chapter.chapterData?.images.orEmpty().mapIndexed { i, img ->
|
result.chapter.chapterData?.images.orEmpty().mapIndexed { i, img ->
|
||||||
Page(i, imageUrl = img.toAbsoluteUrl())
|
Page(i, imageUrl = img)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result.data.orEmpty().mapIndexed { i, img ->
|
result.data.orEmpty().mapIndexed { i, img ->
|
||||||
Page(i, imageUrl = img.toAbsoluteUrl())
|
Page(i, imageUrl = img)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.toAbsoluteUrl(): String {
|
|
||||||
return if (startsWith("https://") || startsWith("http://")) this else "$apiUrl/$this"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
|
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String = ""
|
override fun imageUrlParse(response: Response): String = ""
|
||||||
@ -393,32 +343,6 @@ abstract class HeanCms(
|
|||||||
summaryOff = intl["pref_show_paid_chapter_summary_off"]
|
summaryOff = intl["pref_show_paid_chapter_summary_off"]
|
||||||
setDefaultValue(SHOW_PAID_CHAPTERS_DEFAULT)
|
setDefaultValue(SHOW_PAID_CHAPTERS_DEFAULT)
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
if (enableLogin) {
|
|
||||||
EditTextPreference(screen.context).apply {
|
|
||||||
key = USER_PREF
|
|
||||||
title = intl["pref_username_title"]
|
|
||||||
summary = intl["pref_credentials_summary"]
|
|
||||||
setDefaultValue("")
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, _ ->
|
|
||||||
preferences.tokenData = HeanCmsTokenPayloadDto()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
EditTextPreference(screen.context).apply {
|
|
||||||
key = PASSWORD_PREF
|
|
||||||
title = intl["pref_password_title"]
|
|
||||||
summary = intl["pref_credentials_summary"]
|
|
||||||
setDefaultValue("")
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, _ ->
|
|
||||||
preferences.tokenData = HeanCmsTokenPayloadDto()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected inline fun <reified T> Response.parseAs(): T = use {
|
protected inline fun <reified T> Response.parseAs(): T = use {
|
||||||
@ -433,21 +357,6 @@ abstract class HeanCms(
|
|||||||
private val SharedPreferences.showPaidChapters: Boolean
|
private val SharedPreferences.showPaidChapters: Boolean
|
||||||
get() = getBoolean(SHOW_PAID_CHAPTERS_PREF, SHOW_PAID_CHAPTERS_DEFAULT)
|
get() = getBoolean(SHOW_PAID_CHAPTERS_PREF, SHOW_PAID_CHAPTERS_DEFAULT)
|
||||||
|
|
||||||
private val SharedPreferences.user: String
|
|
||||||
get() = getString(USER_PREF, "") ?: ""
|
|
||||||
|
|
||||||
private val SharedPreferences.password: String
|
|
||||||
get() = getString(PASSWORD_PREF, "") ?: ""
|
|
||||||
|
|
||||||
private var SharedPreferences.tokenData: HeanCmsTokenPayloadDto
|
|
||||||
get() {
|
|
||||||
val jsonString = getString(TOKEN_PREF, "{}")!!
|
|
||||||
return json.decodeFromString(jsonString)
|
|
||||||
}
|
|
||||||
set(data) {
|
|
||||||
edit().putString(TOKEN_PREF, json.encodeToString(data)).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
|
private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
|
||||||
private const val ACCEPT_JSON = "application/json, text/plain, */*"
|
private const val ACCEPT_JSON = "application/json, text/plain, */*"
|
||||||
@ -458,12 +367,5 @@ abstract class HeanCms(
|
|||||||
|
|
||||||
private const val SHOW_PAID_CHAPTERS_PREF = "pref_show_paid_chap"
|
private const val SHOW_PAID_CHAPTERS_PREF = "pref_show_paid_chap"
|
||||||
private const val SHOW_PAID_CHAPTERS_DEFAULT = false
|
private const val SHOW_PAID_CHAPTERS_DEFAULT = false
|
||||||
|
|
||||||
private const val USER_PREF = "pref_user"
|
|
||||||
private const val PASSWORD_PREF = "pref_password"
|
|
||||||
|
|
||||||
private const val TOKEN_PREF = "pref_token"
|
|
||||||
|
|
||||||
private val tokenExpiredAtDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,33 +7,6 @@ import kotlinx.serialization.Serializable
|
|||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class HeanCmsTokenPayloadDto(
|
|
||||||
val token: String? = null,
|
|
||||||
private val expiresAt: String? = null,
|
|
||||||
) {
|
|
||||||
fun isExpired(dateFormat: SimpleDateFormat): Boolean {
|
|
||||||
val expiredTime = try {
|
|
||||||
// Reduce one day to prevent timezone issues
|
|
||||||
expiresAt?.let { dateFormat.parse(it)?.time?.minus(1000 * 60 * 60 * 24) } ?: 0L
|
|
||||||
} catch (_: Exception) {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
|
|
||||||
return System.currentTimeMillis() > expiredTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class HeanCmsErrorsDto(
|
|
||||||
val errors: List<HeanCmsErrorMessageDto>? = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class HeanCmsErrorMessageDto(
|
|
||||||
val message: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class HeanCmsQuerySearchDto(
|
class HeanCmsQuerySearchDto(
|
||||||
val data: List<HeanCmsSeriesDto> = emptyList(),
|
val data: List<HeanCmsSeriesDto> = emptyList(),
|
||||||
@ -156,7 +129,7 @@ class HeanCmsPageDataDto(
|
|||||||
)
|
)
|
||||||
|
|
||||||
private fun String.toAbsoluteThumbnailUrl(apiUrl: String, coverPath: String): String {
|
private fun String.toAbsoluteThumbnailUrl(apiUrl: String, coverPath: String): String {
|
||||||
return if (startsWith("https://") || startsWith("http://")) this else "$apiUrl/$coverPath$this"
|
return if (startsWith("https://")) this else "$apiUrl/$coverPath$this"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun String.toStatus(): Int = when (this) {
|
fun String.toStatus(): Int = when (this) {
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
search_length_error=The query must have at least 2 characters
|
|
||||||
comics_list_error=Comics list not found
|
|
||||||
comic_data_error=Comic data not found
|
|
||||||
sort_by_filter_title=Sort by
|
|
||||||
sort_by_filter_name=Name
|
|
||||||
sort_by_filter_views=Views
|
|
||||||
sort_by_filter_updated=Updated
|
|
||||||
sort_by_filter_added=Added
|
|
||||||
status_filter_title=Status
|
|
||||||
status_filter_ongoing=Ongoing
|
|
||||||
status_filter_hiatus=Hiatus
|
|
||||||
status_filter_dropped=Dropped
|
|
||||||
status_filter_completed=Completed
|
|
@ -1,13 +0,0 @@
|
|||||||
search_length_error=La búsqueda debe tener al menos 2 caracteres
|
|
||||||
comics_list_error=No se pudo encontrar la lista de comics
|
|
||||||
comic_data_error=No se pudo encontrar los datos del comic
|
|
||||||
sort_by_filter_title=Ordenar por
|
|
||||||
sort_by_filter_name=Nombre
|
|
||||||
sort_by_filter_views=Vistas
|
|
||||||
sort_by_filter_updated=Actualización
|
|
||||||
sort_by_filter_added=Agregado
|
|
||||||
status_filter_title=Estado
|
|
||||||
status_filter_ongoing=En curso
|
|
||||||
status_filter_hiatus=En pausa
|
|
||||||
status_filter_dropped=Abandonado
|
|
||||||
status_filter_completed=Finalizado
|
|
@ -1,9 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("lib-multisrc")
|
|
||||||
}
|
|
||||||
|
|
||||||
baseVersionCode = 1
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api(project(":lib:i18n"))
|
|
||||||
}
|
|
@ -1,248 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.mangaesp
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.lib.i18n.Intl
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
abstract class MangaEsp(
|
|
||||||
override val name: String,
|
|
||||||
override val baseUrl: String,
|
|
||||||
override val lang: String,
|
|
||||||
protected val apiBaseUrl: String = baseUrl.replace("://", "://apis."),
|
|
||||||
) : HttpSource() {
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
protected val json: Json by injectLazy()
|
|
||||||
|
|
||||||
protected val intl = Intl(
|
|
||||||
language = lang,
|
|
||||||
baseLanguage = "en",
|
|
||||||
availableLanguages = setOf("en", "es"),
|
|
||||||
classLoader = this::class.java.classLoader!!,
|
|
||||||
)
|
|
||||||
|
|
||||||
override val client: OkHttpClient = network.client.newBuilder()
|
|
||||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
|
||||||
.add("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request = GET("$apiBaseUrl/api/topSerie", headers)
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
|
||||||
val responseData = json.decodeFromString<TopSeriesDto>(response.body.string())
|
|
||||||
|
|
||||||
val topDaily = responseData.response.topDaily.flatten().map { it.data }
|
|
||||||
val topWeekly = responseData.response.topWeekly.flatten().map { it.data }
|
|
||||||
val topMonthly = responseData.response.topMonthly.flatten().map { it.data }
|
|
||||||
|
|
||||||
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }.map { it.toSManga() }
|
|
||||||
|
|
||||||
return MangasPage(mangas, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request = GET("$apiBaseUrl/api/lastUpdates", headers)
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
|
||||||
val responseData = json.decodeFromString<LastUpdatesDto>(response.body.string())
|
|
||||||
|
|
||||||
val mangas = responseData.response.map { it.toSManga() }
|
|
||||||
|
|
||||||
return MangasPage(mangas, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var comicsList = mutableListOf<SeriesDto>()
|
|
||||||
|
|
||||||
override fun fetchSearchManga(
|
|
||||||
page: Int,
|
|
||||||
query: String,
|
|
||||||
filters: FilterList,
|
|
||||||
): Observable<MangasPage> {
|
|
||||||
return if (comicsList.isEmpty()) {
|
|
||||||
client.newCall(searchMangaRequest(page, query, filters))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { searchMangaParse(it, page, query, filters) }
|
|
||||||
} else {
|
|
||||||
Observable.just(parseComicsList(page, query, filters))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/comics", headers)
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
private fun searchMangaParse(response: Response, page: Int, query: String, filters: FilterList): MangasPage {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() }
|
|
||||||
val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1)
|
|
||||||
?: throw Exception(intl["comics_list_error"])
|
|
||||||
val unescapedJson = jsonString.unescape()
|
|
||||||
comicsList = json.decodeFromString<List<SeriesDto>>(unescapedJson).toMutableList()
|
|
||||||
return parseComicsList(page, query, filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var filteredList = mutableListOf<SeriesDto>()
|
|
||||||
|
|
||||||
private fun parseComicsList(page: Int, query: String, filterList: FilterList): MangasPage {
|
|
||||||
if (page == 1) {
|
|
||||||
filteredList.clear()
|
|
||||||
|
|
||||||
if (query.isNotBlank()) {
|
|
||||||
if (query.length < 2) throw Exception(intl["search_length_error"])
|
|
||||||
filteredList.addAll(
|
|
||||||
comicsList.filter {
|
|
||||||
it.name.contains(query, ignoreCase = true) || it.alternativeName?.contains(query, ignoreCase = true) == true
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
filteredList.addAll(comicsList)
|
|
||||||
}
|
|
||||||
|
|
||||||
val statusFilter = filterList.firstInstanceOrNull<StatusFilter>()
|
|
||||||
|
|
||||||
if (statusFilter != null) {
|
|
||||||
filteredList = filteredList.filter { it.status == statusFilter.toUriPart() }.toMutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val sortByFilter = filterList.firstInstanceOrNull<SortByFilter>()
|
|
||||||
|
|
||||||
if (sortByFilter != null) {
|
|
||||||
if (sortByFilter.state?.ascending == true) {
|
|
||||||
when (sortByFilter.selected) {
|
|
||||||
"name" -> filteredList.sortBy { it.name }
|
|
||||||
"views" -> filteredList.sortBy { it.trending?.views }
|
|
||||||
"updated_at" -> filteredList.sortBy { it.lastChapterDate }
|
|
||||||
"created_at" -> filteredList.sortBy { it.createdAt }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
when (sortByFilter.selected) {
|
|
||||||
"name" -> filteredList.sortByDescending { it.name }
|
|
||||||
"views" -> filteredList.sortByDescending { it.trending?.views }
|
|
||||||
"updated_at" -> filteredList.sortByDescending { it.lastChapterDate }
|
|
||||||
"created_at" -> filteredList.sortByDescending { it.createdAt }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasNextPage = filteredList.size > page * MANGAS_PER_PAGE
|
|
||||||
|
|
||||||
return MangasPage(
|
|
||||||
filteredList.subList((page - 1) * MANGAS_PER_PAGE, min(page * MANGAS_PER_PAGE, filteredList.size))
|
|
||||||
.map { it.toSManga() },
|
|
||||||
hasNextPage,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
|
||||||
val responseBody = response.body.string()
|
|
||||||
val mangaDetailsJson = MANGA_DETAILS_REGEX.find(responseBody)?.groupValues?.get(1)
|
|
||||||
?: throw Exception(intl["comic_data_error"])
|
|
||||||
val unescapedJson = mangaDetailsJson.unescape()
|
|
||||||
|
|
||||||
return json.decodeFromString<SeriesDto>(unescapedJson).toSMangaDetails()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val responseBody = response.body.string()
|
|
||||||
val mangaDetailsJson = MANGA_DETAILS_REGEX.find(responseBody)?.groupValues?.get(1)
|
|
||||||
?: throw Exception(intl["comic_data_error"])
|
|
||||||
val unescapedJson = mangaDetailsJson.unescape()
|
|
||||||
val series = json.decodeFromString<SeriesDto>(unescapedJson)
|
|
||||||
return series.chapters.map { it.toSChapter(series.slug) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
return document.select("main.contenedor.read img").mapIndexed { i, img ->
|
|
||||||
Page(i, imageUrl = img.imgAttr())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
|
||||||
SortByFilter(intl["sort_by_filter_title"], getSortProperties()),
|
|
||||||
StatusFilter(intl["status_filter_title"], getStatusList()),
|
|
||||||
)
|
|
||||||
|
|
||||||
protected open fun getSortProperties(): List<SortProperty> = listOf(
|
|
||||||
SortProperty(intl["sort_by_filter_name"], "name"),
|
|
||||||
SortProperty(intl["sort_by_filter_views"], "views"),
|
|
||||||
SortProperty(intl["sort_by_filter_updated"], "updated_at"),
|
|
||||||
SortProperty(intl["sort_by_filter_added"], "created_at"),
|
|
||||||
)
|
|
||||||
|
|
||||||
data class SortProperty(val name: String, val value: String) {
|
|
||||||
override fun toString(): String = name
|
|
||||||
}
|
|
||||||
|
|
||||||
class SortByFilter(title: String, private val sortProperties: List<SortProperty>) : Filter.Sort(
|
|
||||||
title,
|
|
||||||
sortProperties.map { it.name }.toTypedArray(),
|
|
||||||
Selection(2, ascending = false),
|
|
||||||
) {
|
|
||||||
val selected: String
|
|
||||||
get() = sortProperties[state!!.index].value
|
|
||||||
}
|
|
||||||
|
|
||||||
private class StatusFilter(title: String, statusList: Array<Pair<String, Int>>) : UriPartFilter(
|
|
||||||
title,
|
|
||||||
statusList,
|
|
||||||
)
|
|
||||||
|
|
||||||
protected open fun getStatusList() = arrayOf(
|
|
||||||
Pair(intl["status_filter_ongoing"], 1),
|
|
||||||
Pair(intl["status_filter_hiatus"], 2),
|
|
||||||
Pair(intl["status_filter_dropped"], 3),
|
|
||||||
Pair(intl["status_filter_completed"], 4),
|
|
||||||
)
|
|
||||||
|
|
||||||
private open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, Int>>) :
|
|
||||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
|
||||||
fun toUriPart() = vals[state].second
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
|
|
||||||
filterIsInstance<R>().firstOrNull()
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
private fun Element.imgAttr(): String = when {
|
|
||||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
|
||||||
hasAttr("data-src") -> attr("abs:data-src")
|
|
||||||
hasAttr("data-cfsrc") -> attr("abs:data-cfsrc")
|
|
||||||
else -> attr("abs:src")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.unescape(): String {
|
|
||||||
return UNESCAPE_REGEX.replace(this, "$1")
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val UNESCAPE_REGEX = """\\(.)""".toRegex()
|
|
||||||
private val MANGA_LIST_REGEX = """self\.__next_f\.push\(.*data\\":(\[.*trending.*])\}""".toRegex()
|
|
||||||
private val MANGA_DETAILS_REGEX = """self\.__next_f\.push\(.*data\\":(\{.*lastChapters.*\}).*\\"numFollow""".toRegex()
|
|
||||||
private const val MANGAS_PER_PAGE = 15
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,8 +2,8 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 4
|
baseVersionCode = 3
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly("com.github.tachiyomiorg:image-decoder:e08e9be535")
|
compileOnly("com.github.tachiyomiorg:image-decoder:398d3c074f")
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ 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.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
@ -182,7 +183,7 @@ abstract class PeachScan(
|
|||||||
val zis = ZipInputStream(response.body.byteStream())
|
val zis = ZipInputStream(response.body.byteStream())
|
||||||
|
|
||||||
val images = generateSequence { zis.nextEntry }
|
val images = generateSequence { zis.nextEntry }
|
||||||
.mapNotNull {
|
.map {
|
||||||
val entryName = it.name
|
val entryName = it.name
|
||||||
val splitEntryName = entryName.split('.')
|
val splitEntryName = entryName.split('.')
|
||||||
val entryIndex = splitEntryName.first().toInt()
|
val entryIndex = splitEntryName.first().toInt()
|
||||||
@ -194,7 +195,7 @@ abstract class PeachScan(
|
|||||||
val svgBytes = zis.readBytes()
|
val svgBytes = zis.readBytes()
|
||||||
val svgContent = svgBytes.toString(Charsets.UTF_8)
|
val svgContent = svgBytes.toString(Charsets.UTF_8)
|
||||||
val b64 = dataUriRegex.find(svgContent)?.groupValues?.get(1)
|
val b64 = dataUriRegex.find(svgContent)?.groupValues?.get(1)
|
||||||
?: return@mapNotNull null
|
?: throw IOException("Não foi possível corresponder a imagem no conteúdo SVG")
|
||||||
|
|
||||||
Base64.decode(b64, Base64.DEFAULT)
|
Base64.decode(b64, Base64.DEFAULT)
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import android.graphics.Rect
|
|||||||
import tachiyomi.decoder.ImageDecoder
|
import tachiyomi.decoder.ImageDecoder
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,98 +18,40 @@ import java.lang.reflect.Method
|
|||||||
*/
|
*/
|
||||||
object PeachScanUtils {
|
object PeachScanUtils {
|
||||||
private var decodeMethod: Method
|
private var decodeMethod: Method
|
||||||
private var newInstanceMethod: Method
|
|
||||||
|
|
||||||
private var classSignature = ClassSignature.Newest
|
private var isNewDecodeMethod = false
|
||||||
|
|
||||||
private enum class ClassSignature {
|
|
||||||
Old, New, Newest
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val rectClass = Rect::class.java
|
val rectClass = Rect::class.java
|
||||||
val booleanClass = Boolean::class.java
|
val booleanClass = Boolean::class.java
|
||||||
val intClass = Int::class.java
|
val intClass = Int::class.java
|
||||||
val byteArrayClass = ByteArray::class.java
|
val byteArrayClass = ByteArray::class.java
|
||||||
val inputStreamClass = InputStream::class.java
|
|
||||||
|
|
||||||
try {
|
decodeMethod = try {
|
||||||
// Mihon Preview r6595+
|
isNewDecodeMethod = true
|
||||||
classSignature = ClassSignature.Newest
|
|
||||||
|
|
||||||
// decode(region, sampleSize)
|
|
||||||
decodeMethod = ImageDecoder::class.java.getMethod(
|
|
||||||
"decode",
|
|
||||||
rectClass,
|
|
||||||
intClass,
|
|
||||||
)
|
|
||||||
|
|
||||||
// newInstance(stream, cropBorders, displayProfile)
|
|
||||||
newInstanceMethod = ImageDecoder.Companion::class.java.getMethod(
|
|
||||||
"newInstance",
|
|
||||||
inputStreamClass,
|
|
||||||
booleanClass,
|
|
||||||
byteArrayClass,
|
|
||||||
)
|
|
||||||
} catch (_: NoSuchMethodException) {
|
|
||||||
try {
|
|
||||||
// Mihon Stable & forks
|
|
||||||
classSignature = ClassSignature.New
|
|
||||||
|
|
||||||
// decode(region, rgb565, sampleSize, applyColorManagement, displayProfile)
|
// decode(region, rgb565, sampleSize, applyColorManagement, displayProfile)
|
||||||
decodeMethod = ImageDecoder::class.java.getMethod(
|
ImageDecoder::class.java.getMethod("decode", rectClass, booleanClass, intClass, booleanClass, byteArrayClass)
|
||||||
"decode",
|
} catch (e: NoSuchMethodException) {
|
||||||
rectClass,
|
isNewDecodeMethod = false
|
||||||
booleanClass,
|
|
||||||
intClass,
|
|
||||||
booleanClass,
|
|
||||||
byteArrayClass,
|
|
||||||
)
|
|
||||||
|
|
||||||
// newInstance(stream, cropBorders)
|
|
||||||
newInstanceMethod = ImageDecoder.Companion::class.java.getMethod(
|
|
||||||
"newInstance",
|
|
||||||
inputStreamClass,
|
|
||||||
booleanClass,
|
|
||||||
)
|
|
||||||
} catch (_: NoSuchMethodException) {
|
|
||||||
// Tachiyomi J2k
|
|
||||||
classSignature = ClassSignature.Old
|
|
||||||
|
|
||||||
// decode(region, rgb565, sampleSize)
|
// decode(region, rgb565, sampleSize)
|
||||||
decodeMethod =
|
ImageDecoder::class.java.getMethod("decode", rectClass, booleanClass, intClass)
|
||||||
ImageDecoder::class.java.getMethod(
|
|
||||||
"decode",
|
|
||||||
rectClass,
|
|
||||||
booleanClass,
|
|
||||||
intClass,
|
|
||||||
)
|
|
||||||
|
|
||||||
// newInstance(stream, cropBorders)
|
|
||||||
newInstanceMethod = ImageDecoder.Companion::class.java.getMethod(
|
|
||||||
"newInstance",
|
|
||||||
inputStreamClass,
|
|
||||||
booleanClass,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decodeImage(data: ByteArray, rgb565: Boolean, filename: String, entryName: String): Bitmap {
|
fun decodeImage(data: ByteArray, rgb565: Boolean, filename: String, entryName: String): Bitmap {
|
||||||
val decoder = when (classSignature) {
|
val decoder = ImageDecoder.newInstance(ByteArrayInputStream(data))
|
||||||
ClassSignature.Newest -> newInstanceMethod.invoke(ImageDecoder.Companion, ByteArrayInputStream(data), false, null)
|
|
||||||
else -> newInstanceMethod.invoke(ImageDecoder.Companion, ByteArrayInputStream(data), false)
|
|
||||||
} as ImageDecoder?
|
|
||||||
|
|
||||||
if (decoder == null || decoder.width <= 0 || decoder.height <= 0) {
|
if (decoder == null || decoder.width <= 0 || decoder.height <= 0) {
|
||||||
throw IOException("Falha ao inicializar o decodificador de imagem")
|
throw IOException("Falha ao inicializar o decodificador de imagem")
|
||||||
}
|
}
|
||||||
|
|
||||||
val rect = Rect(0, 0, decoder.width, decoder.height)
|
val rect = Rect(0, 0, decoder.width, decoder.height)
|
||||||
val bitmap = when (classSignature) {
|
val bitmap = if (isNewDecodeMethod) {
|
||||||
ClassSignature.Newest -> decodeMethod.invoke(decoder, rect, 1)
|
decodeMethod.invoke(decoder, rect, rgb565, 1, false, null)
|
||||||
ClassSignature.New -> decodeMethod.invoke(decoder, rect, rgb565, 1, false, null)
|
} else {
|
||||||
else -> decodeMethod.invoke(decoder, rect, rgb565, 1)
|
decodeMethod.invoke(decoder, rect, rgb565, 1)
|
||||||
} as Bitmap?
|
} as Bitmap?
|
||||||
|
|
||||||
decoder.recycle()
|
decoder.recycle()
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name=".all.unionmangas.UnionMangasUrlActivity"
|
|
||||||
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="https://unionmangas.xyz" />
|
|
||||||
|
|
||||||
<data android:scheme="https"/>
|
|
||||||
<data android:pathPattern="/manga-br/..*"/>
|
|
||||||
|
|
||||||
<data android:scheme="https"/>
|
|
||||||
<data android:pathPattern="/italy/..*"/>
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
@ -1,12 +0,0 @@
|
|||||||
ext {
|
|
||||||
extName = 'Union Mangas'
|
|
||||||
extClass = '.UnionMangasFactory'
|
|
||||||
extVersionCode = 1
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(':lib:cryptoaes'))
|
|
||||||
}
|
|
Before Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 41 KiB |
@ -1,238 +0,0 @@
|
|||||||
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.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.TimeZone
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
|
|
||||||
override val lang = langOption.lang
|
|
||||||
|
|
||||||
override val name: String = "Union Mangas"
|
|
||||||
|
|
||||||
override val baseUrl: String = "https://unionmangas.xyz"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
val langApiInfix = when (lang) {
|
|
||||||
"it" -> langOption.infix
|
|
||||||
else -> "v3/po"
|
|
||||||
}
|
|
||||||
|
|
||||||
override val client = network.client.newBuilder()
|
|
||||||
.rateLimit(5, 2, TimeUnit.SECONDS)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private fun apiHeaders(url: String): Headers {
|
|
||||||
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 fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
|
||||||
val chapters = mutableListOf<SChapter>()
|
|
||||||
var currentPage = 0
|
|
||||||
do {
|
|
||||||
val chaptersDto = fetchChapterListPageable(manga, currentPage)
|
|
||||||
chapters += chaptersDto.toSChapter(langOption)
|
|
||||||
currentPage++
|
|
||||||
} while (chaptersDto.hasNextPage())
|
|
||||||
return Observable.just(chapters.reversed())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchChapterListPageable(manga: SManga, page: Int): ChapterPageDto {
|
|
||||||
val maxResult = 16
|
|
||||||
val url = "$apiUrl/api/$langApiInfix/GetChapterListFilter/${manga.slug()}/$maxResult/$page/all/ASC"
|
|
||||||
return client.newCall(GET(url, apiHeaders(url))).execute()
|
|
||||||
.parseAs<ChapterPageDto>()
|
|
||||||
}
|
|
||||||
|
|
||||||
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) }
|
|
||||||
|
|
||||||
return MangasPage(
|
|
||||||
mangas = mangas,
|
|
||||||
hasNextPage = dto.hasNextPage(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
val url = "$baseUrl/${langOption.infix}/latest-releases".toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("page", "$page")
|
|
||||||
.build()
|
|
||||||
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> {
|
|
||||||
if (query.startsWith(slugPrefix)) {
|
|
||||||
val mangaUrl = query.substringAfter(slugPrefix)
|
|
||||||
return client.newCall(GET("$baseUrl/${langOption.infix}/$mangaUrl", headers))
|
|
||||||
.asObservableSuccess().map { response ->
|
|
||||||
val manga = mangaDetailsParse(response).apply {
|
|
||||||
url = mangaUrl
|
|
||||||
}
|
|
||||||
MangasPage(listOf(manga), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.fetchSearchManga(page, query, filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String = ""
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
val mangasDto = response.parseAs<MangaListDto>().apply {
|
|
||||||
currentPage = response.request.url.pathSegments.last()
|
|
||||||
}
|
|
||||||
|
|
||||||
return MangasPage(
|
|
||||||
mangas = mangasDto.toSManga(langOption.infix),
|
|
||||||
hasNextPage = mangasDto.hasNextPage(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseNextData() = asJsoup().parseNextData<T>()
|
|
||||||
|
|
||||||
private inline fun <reified T> Document.parseNextData(): NextData<T> {
|
|
||||||
val jsonContent = selectFirst("script#__NEXT_DATA__")!!.html()
|
|
||||||
return json.decodeFromString<NextData<T>>(jsonContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T {
|
|
||||||
return json.decodeFromString(body.string())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.removeProtocol() = trim().replace("https://", "")
|
|
||||||
|
|
||||||
private fun SManga.slug() = this.url.split("/").last()
|
|
||||||
|
|
||||||
private fun String.toUrlWithoutDomain() = trim().replace(apiUrl, "")
|
|
||||||
|
|
||||||
private fun mangaParse(dto: MangaDto, query: QueryDto): SManga {
|
|
||||||
return SManga.create().apply {
|
|
||||||
title = dto.title
|
|
||||||
thumbnail_url = dto.thumbnailUrl
|
|
||||||
status = dto.status
|
|
||||||
url = mangaUrlParse(dto.slug, query.type)
|
|
||||||
genre = dto.genres
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mangaUrlParse(slug: String, pathSegment: String) = "/$pathSegment/$slug"
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val apiUrl = "https://api.unionmanga.xyz"
|
|
||||||
val apiSeed = "8e0550790c94d6abc71d738959a88d209690dc86"
|
|
||||||
val domain = "yaoi-chan.xyz"
|
|
||||||
val slugPrefix = "slug:"
|
|
||||||
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,149 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.unionmangas
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class NextData<T>(val props: Props<T>, val query: QueryDto) {
|
|
||||||
val data get() = props.pageProps
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Props<T>(val pageProps: T)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class PopularMangaProps(@SerialName("data_popular") val mangas: List<PopularMangaDto>)
|
|
||||||
|
|
||||||
@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
|
|
||||||
class ChapterDto(
|
|
||||||
val date: String,
|
|
||||||
val slug: String,
|
|
||||||
@SerialName("idDoc") val slugManga: String,
|
|
||||||
@SerialName("idDetail") val id: String,
|
|
||||||
@SerialName("nameChapter") val name: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@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
|
|
||||||
class MangaDto(
|
|
||||||
@SerialName("name") val title: String,
|
|
||||||
@SerialName("image") private val _thumbnailUrl: String,
|
|
||||||
@SerialName("idDoc") val slug: String,
|
|
||||||
@SerialName("genres") private val _genres: String,
|
|
||||||
@SerialName("status") val _status: String,
|
|
||||||
) {
|
|
||||||
val thumbnailUrl get() = "${UnionMangas.apiUrl}$_thumbnailUrl"
|
|
||||||
val genres get() = _genres.split(",").joinToString { it.trim() }
|
|
||||||
val status get() = toSMangaStatus(_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.first().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
|
|
||||||
"completed" -> SManga.COMPLETED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.unionmangas
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
|
|
||||||
class UnionMangasFactory : SourceFactory {
|
|
||||||
override fun createSources(): List<Source> = languages.map { UnionMangas(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
class LanguageOption(val lang: String, val infix: String = lang, val chpPrefix: String, val pageDelimiter: String)
|
|
||||||
|
|
||||||
val languages = listOf(
|
|
||||||
LanguageOption("it", "italy", "leer", ","),
|
|
||||||
LanguageOption("pt-BR", "manga-br", "cap", "#"),
|
|
||||||
)
|
|
@ -1,36 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.unionmangas
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
class UnionMangasUrlActivity : Activity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
val host = intent?.data?.host
|
|
||||||
val pathSegments = intent?.data?.pathSegments
|
|
||||||
|
|
||||||
if (host != null && pathSegments != null) {
|
|
||||||
val intent = Intent().apply {
|
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
|
||||||
putExtra("query", slug(pathSegments))
|
|
||||||
putExtra("filter", packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
startActivity(intent)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
Log.e("UnionMangasUrlActivity", e.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finish()
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun slug(pathSegments: List<String>) = "${UnionMangas.slugPrefix}${pathSegments.last()}"
|
|
||||||
}
|
|
8
src/ar/gmanga/build.gradle
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
ext {
|
||||||
|
extName = 'GMANGA'
|
||||||
|
extClass = '.Gmanga'
|
||||||
|
themePkg = 'gmanga'
|
||||||
|
overrideVersionCode = 13
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/ar/gmanga/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
src/ar/gmanga/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
src/ar/gmanga/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
src/ar/gmanga/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/ar/gmanga/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 18 KiB |
@ -0,0 +1,33 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.ar.gmanga
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterListResponse(
|
||||||
|
val releases: List<ChapterRelease>,
|
||||||
|
val chapterizations: List<Chapterization>,
|
||||||
|
val teams: List<Team>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterRelease(
|
||||||
|
val id: Int,
|
||||||
|
@SerialName("chapterization_id") val chapId: Int,
|
||||||
|
@SerialName("team_id") val teamId: Int,
|
||||||
|
val chapter: JsonPrimitive,
|
||||||
|
@SerialName("time_stamp") val timestamp: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Chapterization(
|
||||||
|
val id: Int,
|
||||||
|
val title: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Team(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
)
|
@ -0,0 +1,90 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.ar.gmanga
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import eu.kanade.tachiyomi.multisrc.gmanga.BrowseManga
|
||||||
|
import eu.kanade.tachiyomi.multisrc.gmanga.Gmanga
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.asObservable
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
import kotlinx.serialization.json.float
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class Gmanga : Gmanga(
|
||||||
|
"GMANGA",
|
||||||
|
"https://gmanga.org",
|
||||||
|
"ar",
|
||||||
|
"https://media.gmanga.me",
|
||||||
|
) {
|
||||||
|
override val client = super.client.newBuilder()
|
||||||
|
.rateLimit(4)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
init {
|
||||||
|
// remove obsolete preferences
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000).run {
|
||||||
|
if (contains("gmanga_chapter_listing")) {
|
||||||
|
edit().remove("gmanga_chapter_listing").apply()
|
||||||
|
}
|
||||||
|
if (contains("gmanga_last_listing")) {
|
||||||
|
edit().remove("gmanga_last_listing").apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
val decMga = response.decryptAs<JsonObject>()
|
||||||
|
val selectedManga = decMga["rows"]!!.jsonArray[0].jsonObject["rows"]!!.jsonArray
|
||||||
|
val manags = selectedManga.map {
|
||||||
|
json.decodeFromJsonElement<BrowseManga>(it.jsonArray[17])
|
||||||
|
}
|
||||||
|
|
||||||
|
val entries = manags.map { it.toSManga(::createThumbnail) }
|
||||||
|
.distinctBy { it.url }
|
||||||
|
|
||||||
|
return MangasPage(
|
||||||
|
entries,
|
||||||
|
hasNextPage = (manags.size >= 30),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
|
return client.newCall(chapterListRequest(manga))
|
||||||
|
.asObservable() // sites returns false 302 code
|
||||||
|
.map(::chapterListParse)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chaptersRequest(manga: SManga): Request {
|
||||||
|
val mangaId = manga.url.substringAfterLast("/")
|
||||||
|
return GET("https://api2.gmanga.me/api/mangas/$mangaId/releases", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chaptersParse(response: Response): List<SChapter> {
|
||||||
|
val chapterList = response.parseAs<ChapterListResponse>()
|
||||||
|
|
||||||
|
return chapterList.releases.map {
|
||||||
|
SChapter.create().apply {
|
||||||
|
val chapter = chapterList.chapterizations.first { chap -> chap.id == it.chapId }
|
||||||
|
val team = chapterList.teams.firstOrNull { team -> team.id == it.teamId }
|
||||||
|
|
||||||
|
url = "/r/${it.id}"
|
||||||
|
chapter_number = it.chapter.float
|
||||||
|
date_upload = it.timestamp * 1000
|
||||||
|
scanlator = team?.name
|
||||||
|
|
||||||
|
val chapterName = chapter.title.let { if (it.trim() != "") " - $it" else "" }
|
||||||
|
name = "${chapter_number.let { if (it % 1 > 0) it else it.toInt() }}$chapterName"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
src/en/coloredmanga/build.gradle
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
ext {
|
||||||
|
extName = 'Colored Manga'
|
||||||
|
extClass = '.ColoredManga'
|
||||||
|
themePkg = 'madara'
|
||||||
|
baseUrl = 'https://coloredmanga.com'
|
||||||
|
overrideVersionCode = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/en/coloredmanga/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
src/en/coloredmanga/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/en/coloredmanga/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/en/coloredmanga/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.8 KiB |
BIN
src/en/coloredmanga/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
@ -0,0 +1,14 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.coloredmanga
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class ColoredManga : Madara(
|
||||||
|
"Colored Manga",
|
||||||
|
"https://coloredmanga.com",
|
||||||
|
"en",
|
||||||
|
dateFormat = SimpleDateFormat("dd-MMM", Locale.ENGLISH),
|
||||||
|
) {
|
||||||
|
override val useNewChapterEndpoint = true
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'ComicExtra'
|
extName = 'ComicExtra'
|
||||||
extClass = '.ComicExtra'
|
extClass = '.ComicExtra'
|
||||||
extVersionCode = 15
|
extVersionCode = 14
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@ -23,7 +22,7 @@ class ComicExtra : ParsedHttpSource() {
|
|||||||
|
|
||||||
override val name = "ComicExtra"
|
override val name = "ComicExtra"
|
||||||
|
|
||||||
override val baseUrl = "https://comicextra.org"
|
override val baseUrl = "https://comicextra.me"
|
||||||
|
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
@ -41,11 +40,7 @@ class ComicExtra : ParsedHttpSource() {
|
|||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
return if (query.isNotBlank()) {
|
return if (query.isNotBlank()) {
|
||||||
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply {
|
GET("$baseUrl/comic-search?key=$query", headers)
|
||||||
addQueryParameter("keyword", query)
|
|
||||||
if (page > 1) addQueryParameter("page", page.toString())
|
|
||||||
}.build()
|
|
||||||
GET(url, headers)
|
|
||||||
} else {
|
} else {
|
||||||
var url = baseUrl
|
var url = baseUrl
|
||||||
filters.forEach { filter ->
|
filters.forEach { filter ->
|
||||||
@ -158,7 +153,7 @@ class ComicExtra : ParsedHttpSource() {
|
|||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
val pages = mutableListOf<Page>()
|
val pages = mutableListOf<Page>()
|
||||||
|
|
||||||
document.select("div.chapter-container img").forEachIndexed { i, img ->
|
document.select("img.chapter_img").forEachIndexed { i, img ->
|
||||||
pages.add(Page(i, "", img.attr("abs:src")))
|
pages.add(Page(i, "", img.attr("abs:src")))
|
||||||
}
|
}
|
||||||
return pages
|
return pages
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'EZmanga'
|
extName = 'EZmanga'
|
||||||
extClass = '.EZmanga'
|
extClass = '.EZmanga'
|
||||||
themePkg = 'keyoapp'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://ezmanga.net'
|
baseUrl = 'https://ezmanga.net'
|
||||||
overrideVersionCode = 34
|
overrideVersionCode = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
package eu.kanade.tachiyomi.extension.en.ezmanga
|
package eu.kanade.tachiyomi.extension.en.ezmanga
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.keyoapp.Keyoapp
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class EZmanga : Keyoapp(
|
class EZmanga : Madara(
|
||||||
"EZmanga",
|
"EZmanga",
|
||||||
"https://ezmanga.net",
|
"https://ezmanga.net",
|
||||||
"en",
|
"en",
|
||||||
|
dateFormat = SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH),
|
||||||
) {
|
) {
|
||||||
// Migrated from Madara to Keyoapp
|
override val useNewChapterEndpoint = true
|
||||||
override val versionId = 2
|
|
||||||
|
override val client = super.client.newBuilder()
|
||||||
|
.rateLimit(1)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Hentai2Read'
|
extName = 'Hentai2Read'
|
||||||
extClass = '.Hentai2Read'
|
extClass = '.Hentai2Read'
|
||||||
extVersionCode = 16
|
extVersionCode = 14
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ class Hentai2Read : ParsedHttpSource() {
|
|||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
return SManga.create().apply {
|
return SManga.create().apply {
|
||||||
thumbnail_url = element.select("img").attr("abs:src")
|
thumbnail_url = element.select("img").attr("abs:data-src")
|
||||||
element.select("div.overlay-title a").let {
|
element.select("div.overlay-title a").let {
|
||||||
title = it.text()
|
title = it.text()
|
||||||
setUrlWithoutDomain(it.attr("href"))
|
setUrlWithoutDomain(it.attr("href"))
|
||||||
@ -162,7 +162,7 @@ class Hentai2Read : ParsedHttpSource() {
|
|||||||
manga.genre = infoElement.select("li:contains(Category) > a, li:contains(Content) > a").joinToString(", ") { it.text() }
|
manga.genre = infoElement.select("li:contains(Category) > a, li:contains(Content) > a").joinToString(", ") { it.text() }
|
||||||
manga.description = buildDescription(infoElement)
|
manga.description = buildDescription(infoElement)
|
||||||
manga.status = infoElement.select("li:contains(Status) > a").text().orEmpty().let { parseStatus(it) }
|
manga.status = infoElement.select("li:contains(Status) > a").text().orEmpty().let { parseStatus(it) }
|
||||||
manga.thumbnail_url = document.select("a#js-linkNext img").attr("src")
|
manga.thumbnail_url = document.select("a#js-linkNext > img").attr("src")
|
||||||
manga.title = document.select("h3.block-title > a").first()!!.ownText().trim()
|
manga.title = document.select("h3.block-title > a").first()!!.ownText().trim()
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
ext {
|
|
||||||
extName = 'Kewn Scans'
|
|
||||||
extClass = '.KewnScans'
|
|
||||||
themePkg = 'keyoapp'
|
|
||||||
baseUrl = 'https://kewnscans.org'
|
|
||||||
overrideVersionCode = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 16 KiB |
@ -1,5 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.en.kewnscans
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.keyoapp.Keyoapp
|
|
||||||
|
|
||||||
class KewnScans : Keyoapp("Kewn Scans", "https://kewnscans.org", "en")
|
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Mangago'
|
extName = 'Mangago'
|
||||||
extClass = '.Mangago'
|
extClass = '.Mangago'
|
||||||
extVersionCode = 14
|
extVersionCode = 13
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,8 +175,7 @@ class Mangago : ParsedHttpSource() {
|
|||||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||||
val link = element.select("a.chico")
|
val link = element.select("a.chico")
|
||||||
|
|
||||||
val urlOriginal = link.attr("href")
|
setUrlWithoutDomain(link.attr("href"))
|
||||||
if (urlOriginal.startsWith("http")) url = urlOriginal else setUrlWithoutDomain(urlOriginal)
|
|
||||||
name = link.text().trim()
|
name = link.text().trim()
|
||||||
date_upload = runCatching {
|
date_upload = runCatching {
|
||||||
dateFormat.parse(element.select("td:last-child").text().trim())?.time
|
dateFormat.parse(element.select("td:last-child").text().trim())?.time
|
||||||
@ -243,13 +242,6 @@ class Mangago : ParsedHttpSource() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
if (chapter.url.startsWith("http")) {
|
|
||||||
return GET(chapter.url, headers)
|
|
||||||
}
|
|
||||||
return super.pageListRequest(chapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String =
|
override fun imageUrlParse(document: Document): String =
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
8
src/en/mangajar/build.gradle
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
ext {
|
||||||
|
extName = 'MangaJar'
|
||||||
|
extClass = '.MangaJar'
|
||||||
|
extVersionCode = 9
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/en/mangajar/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
src/en/mangajar/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src/en/mangajar/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
src/en/mangajar/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/en/mangajar/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,275 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.mangajar
|
||||||
|
|
||||||
|
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.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 okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Single
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class MangaJar : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val name = "MangaJar"
|
||||||
|
|
||||||
|
override val baseUrl = "https://mangajar.com"
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client: OkHttpClient = network.cloudflareClient
|
||||||
|
|
||||||
|
// Popular
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/manga?sortBy=popular&page=$page")
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "article[class*=flex-item]"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(element.select("a").attr("href"))
|
||||||
|
title = element.select("img").attr("title")
|
||||||
|
thumbnail_url = element.select("img").let {
|
||||||
|
if (it.hasAttr("data-src")) {
|
||||||
|
it.attr("data-src")
|
||||||
|
} else {
|
||||||
|
it.attr("src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "a.page-link[rel=next]"
|
||||||
|
|
||||||
|
// Latest
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/manga?sortBy=-last_chapter_at&page=$page")
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
|
// Search
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||||
|
val genreFilter = filterList.findInstance<GenreList>()
|
||||||
|
val genre = genreFilter?.let { f -> f.values[f.state] }
|
||||||
|
|
||||||
|
val url = (if (genre!!.isEmpty()) "$baseUrl/search" else "$baseUrl/genre/$genre").toHttpUrl().newBuilder()
|
||||||
|
|
||||||
|
url.addQueryParameter("q", query)
|
||||||
|
url.addQueryParameter("page", page.toString())
|
||||||
|
|
||||||
|
for (filter in filterList) {
|
||||||
|
when (filter) {
|
||||||
|
is OrderBy -> {
|
||||||
|
url.addQueryParameter("sortBy", filter.toUriPart())
|
||||||
|
}
|
||||||
|
is SortBy -> {
|
||||||
|
url.addQueryParameter("sortAscending", filter.toUriPart())
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GET(url.build(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
|
// Details
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
|
description = document.select("div.manga-description.entry").text()
|
||||||
|
thumbnail_url = document.select("div.row > div > img").attr("src")
|
||||||
|
genre = document.select("div.post-info > span > a[href*=genre]").joinToString { it.text() }
|
||||||
|
status = parseStatus(document.select("span:has(b)")[1].text())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(status: String) = when {
|
||||||
|
status.contains("Ongoing") -> SManga.ONGOING
|
||||||
|
status.contains("Ended") -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chapters
|
||||||
|
|
||||||
|
/** For the first page. Pagination is done in [findChapters] */
|
||||||
|
override fun chapterListRequest(manga: SManga) = GET(baseUrl + manga.url + "/chaptersList")
|
||||||
|
|
||||||
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
|
return findChapters(chapterListRequest(manga)).toObservable()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findChapters(request: Request): Single<List<SChapter>> {
|
||||||
|
return client.newCall(request).asObservableSuccess().toSingle().flatMap { response ->
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val thisPage = document.select(chapterListSelector()).map { chapter ->
|
||||||
|
SChapter.create().apply {
|
||||||
|
val link = chapter.select("a")
|
||||||
|
url = link.attr("href")
|
||||||
|
name = link.text()
|
||||||
|
date_upload = parseChapterDate(chapter.select("span.chapter-date").text().trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val nextPageLink = document.select("a.page-link[rel=\"next\"]").firstOrNull()
|
||||||
|
if (nextPageLink == null) {
|
||||||
|
Single.just(thisPage)
|
||||||
|
} else {
|
||||||
|
findChapters(GET("$baseUrl${nextPageLink.attr("href")}")).map { remainingChapters ->
|
||||||
|
thisPage + remainingChapters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "li.list-group-item.chapter-item"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
private fun parseChapterDate(string: String): Long {
|
||||||
|
return if ("ago" in string) {
|
||||||
|
parseRelativeDate(string)
|
||||||
|
} else {
|
||||||
|
dateFormat.parse(string)?.time ?: 0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseRelativeDate(date: String): Long {
|
||||||
|
val trimmedDate = date.substringBefore(" ago").removeSuffix("s").split(" ")
|
||||||
|
|
||||||
|
val calendar = Calendar.getInstance()
|
||||||
|
when (trimmedDate[1]) {
|
||||||
|
"month" -> calendar.apply { add(Calendar.MONTH, -trimmedDate[0].toInt()) }
|
||||||
|
"week" -> calendar.apply { add(Calendar.WEEK_OF_MONTH, -trimmedDate[0].toInt()) }
|
||||||
|
"day" -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) }
|
||||||
|
"hour" -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) }
|
||||||
|
"minute" -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) }
|
||||||
|
"second" -> calendar.apply { add(Calendar.SECOND, 0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return calendar.timeInMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page List
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
return document.select("img[data-page]").mapIndexed { i, element ->
|
||||||
|
Page(i, "", if (element.hasAttr("data-src")) element.attr("abs:data-src") else element.attr("abs:src"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
OrderBy(),
|
||||||
|
SortBy(),
|
||||||
|
GenreList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private class SortBy : UriPartFilter(
|
||||||
|
"Sort By",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Descending", "0"),
|
||||||
|
Pair("Ascending", "1"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private class OrderBy : UriPartFilter(
|
||||||
|
"Order By",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Popularity", "popular"),
|
||||||
|
Pair("Year", "year"),
|
||||||
|
Pair("Alphabet", "name"),
|
||||||
|
Pair("Date added", "published_at"),
|
||||||
|
Pair("Date updated", "last_chapter_at"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private class GenreList : Filter.Select<String>(
|
||||||
|
"Select Genre",
|
||||||
|
arrayOf(
|
||||||
|
"",
|
||||||
|
"Fantasy",
|
||||||
|
"Adventure",
|
||||||
|
"Martial Arts",
|
||||||
|
"Action",
|
||||||
|
"Demons",
|
||||||
|
"Shounen",
|
||||||
|
"Drama",
|
||||||
|
"Isekai",
|
||||||
|
"School Life",
|
||||||
|
"Harem",
|
||||||
|
"Horror",
|
||||||
|
"Supernatural",
|
||||||
|
"Mystery",
|
||||||
|
"Sci-Fi",
|
||||||
|
"Webtoons",
|
||||||
|
"Romance",
|
||||||
|
"Magic",
|
||||||
|
"Slice of Life",
|
||||||
|
"Seinen",
|
||||||
|
"Historical",
|
||||||
|
"Ecchi",
|
||||||
|
"Comedy",
|
||||||
|
"Sports",
|
||||||
|
"Tragedy",
|
||||||
|
"Shounen Ai",
|
||||||
|
"Yaoi",
|
||||||
|
"Shoujo",
|
||||||
|
"Super Power",
|
||||||
|
"Food",
|
||||||
|
"Psychological",
|
||||||
|
"Gender Bender",
|
||||||
|
"Smut",
|
||||||
|
"Shoujo Ai",
|
||||||
|
"Yuri",
|
||||||
|
"4-koma",
|
||||||
|
"Mecha",
|
||||||
|
"Adult",
|
||||||
|
"Mature",
|
||||||
|
"Military",
|
||||||
|
"Vampire",
|
||||||
|
"Kids",
|
||||||
|
"Space",
|
||||||
|
"Police",
|
||||||
|
"Music",
|
||||||
|
"One Shot",
|
||||||
|
"Parody",
|
||||||
|
"Josei",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
||||||
|
|
||||||
|
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||||
|
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||||
|
fun toUriPart() = vals[state].second
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following date related code is taken directly from Genkan.kt
|
||||||
|
companion object {
|
||||||
|
val dateFormat by lazy {
|
||||||
|
SimpleDateFormat("dd MMM yyyy", Locale.ENGLISH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
src/en/manganelobiz/build.gradle
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
ext {
|
||||||
|
extName = 'Manganelo.biz'
|
||||||
|
extClass = '.ManganeloBiz'
|
||||||
|
themePkg = 'madara'
|
||||||
|
baseUrl = 'https://manganelo.biz'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/en/manganelobiz/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
src/en/manganelobiz/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/en/manganelobiz/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
src/en/manganelobiz/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
BIN
src/en/manganelobiz/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
@ -0,0 +1,7 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.manganelobiz
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
|
||||||
|
class ManganeloBiz : Madara("Manganelo.biz", "https://manganelo.biz", "en") {
|
||||||
|
override val useNewChapterEndpoint = false
|
||||||
|
}
|
9
src/en/manganeloscom/build.gradle
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
ext {
|
||||||
|
extName = 'MangaNelos.com'
|
||||||
|
extClass = '.MangaNelosCom'
|
||||||
|
themePkg = 'paprika'
|
||||||
|
baseUrl = 'http://manganelos.com'
|
||||||
|
overrideVersionCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/en/manganeloscom/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
src/en/manganeloscom/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src/en/manganeloscom/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
src/en/manganeloscom/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
src/en/manganeloscom/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 31 KiB |
@ -0,0 +1,5 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.manganeloscom
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.paprika.Paprika
|
||||||
|
|
||||||
|
class MangaNelosCom : Paprika("MangaNelos.com", "http://manganelos.com", "en")
|
10
src/en/manganelowebsite/build.gradle
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
ext {
|
||||||
|
extName = 'Manganelo.website (unoriginal)'
|
||||||
|
extClass = '.ManganeloWebsite'
|
||||||
|
themePkg = 'madara'
|
||||||
|
baseUrl = 'https://manganelo.website'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/en/manganelowebsite/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/en/manganelowebsite/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
src/en/manganelowebsite/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
src/en/manganelowebsite/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/en/manganelowebsite/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 20 KiB |
@ -0,0 +1,7 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.manganelowebsite
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
|
||||||
|
class ManganeloWebsite : Madara("Manganelo.website (unoriginal)", "https://manganelo.website", "en") {
|
||||||
|
override val useNewChapterEndpoint = false
|
||||||
|
}
|
10
src/en/mangarosie/build.gradle
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
ext {
|
||||||
|
extName = 'MangaRosie'
|
||||||
|
extClass = '.MangaRosie'
|
||||||
|
themePkg = 'madara'
|
||||||
|
baseUrl = 'https://mangarosie.in'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/en/mangarosie/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
src/en/mangarosie/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
src/en/mangarosie/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
BIN
src/en/mangarosie/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/en/mangarosie/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 25 KiB |
@ -0,0 +1,8 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.mangarosie
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
|
||||||
|
class MangaRosie : Madara("MangaRosie", "https://mangarosie.in", "en") {
|
||||||
|
override val useNewChapterEndpoint = false
|
||||||
|
override val mangaDetailsSelectorStatus = "div.summary-heading:contains(Status) + div.summary-content"
|
||||||
|
}
|
10
src/en/muctau/build.gradle
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
ext {
|
||||||
|
extName = 'Muctau'
|
||||||
|
extClass = '.Muctau'
|
||||||
|
themePkg = 'madara'
|
||||||
|
baseUrl = 'https://bibimanga.com'
|
||||||
|
overrideVersionCode = 4
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/en/muctau/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
src/en/muctau/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
src/en/muctau/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
src/en/muctau/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/en/muctau/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 16 KiB |
@ -0,0 +1,5 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.muctau
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
|
||||||
|
class Muctau : Madara("Muctau", "https://bibimanga.com", "en")
|
@ -15,7 +15,6 @@ class OmegaScans : HeanCms("Omega Scans", "https://omegascans.org", "en") {
|
|||||||
override val versionId = 2
|
override val versionId = 2
|
||||||
|
|
||||||
override val useNewChapterEndpoint = true
|
override val useNewChapterEndpoint = true
|
||||||
override val enableLogin = true
|
|
||||||
|
|
||||||
override fun getGenreList() = listOf(
|
override fun getGenreList() = listOf(
|
||||||
Genre("Romance", 1),
|
Genre("Romance", 1),
|
||||||
|
@ -3,8 +3,7 @@ ext {
|
|||||||
extClass = '.StoneScape'
|
extClass = '.StoneScape'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://stonescape.xyz'
|
baseUrl = 'https://stonescape.xyz'
|
||||||
overrideVersionCode = 1
|
overrideVersionCode = 0
|
||||||
isNsfw = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -8,13 +8,8 @@ class StoneScape : Madara(
|
|||||||
"StoneScape",
|
"StoneScape",
|
||||||
"https://stonescape.xyz",
|
"https://stonescape.xyz",
|
||||||
"en",
|
"en",
|
||||||
SimpleDateFormat("MMMM dd, yyyy", Locale.ENGLISH),
|
SimpleDateFormat("MMMM dd, yyyy", Locale("en")),
|
||||||
) {
|
) {
|
||||||
override val mangaSubString = "series"
|
override val mangaSubString = "series"
|
||||||
|
override val chapterUrlSelector = "div + a"
|
||||||
override val chapterUrlSelector = "li > a"
|
|
||||||
|
|
||||||
override val mangaDetailsSelectorAuthor = ".post-content .manga-authors a"
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "li.wp-manga-chapter:not(.premium-block)"
|
|
||||||
}
|
}
|
||||||
|