Compare commits
36 Commits
b3f274dab5
...
480cb9d780
Author | SHA1 | Date |
---|---|---|
Vetle Ledaal | 480cb9d780 | |
Vetle Ledaal | 96a3e3c8e4 | |
Vetle Ledaal | 2a2d4ed46d | |
Vetle Ledaal | 0bbc41ee11 | |
Vetle Ledaal | 79bd3dbcca | |
Vetle Ledaal | 6a8ab8c12e | |
Vetle Ledaal | 908373a885 | |
Vetle Ledaal | 57e3fdfe6a | |
Secozzi | ec8c080c75 | |
Secozzi | 8f16b6c06d | |
Secozzi | 2455baa236 | |
Secozzi | 6b63da2979 | |
Chopper | 06b5579243 | |
renovate[bot] | ddd978f27e | |
bapeey | 3dc97aaff8 | |
Cuong M. Tran | 9637963a6c | |
Vetle Ledaal | 02f70b000d | |
Vetle Ledaal | 7172b0567e | |
Vetle Ledaal | 613a2c5f50 | |
Vetle Ledaal | 243a6e8d3f | |
Coin | 32a79b4a89 | |
Vetle Ledaal | 937843c751 | |
Barrell Titor | 01de950ce9 | |
KirinRaikage | 471e4d3190 | |
bapeey | 142b22b440 | |
Vetle Ledaal | 3bb82737b7 | |
Vetle Ledaal | da0b929bf3 | |
Vetle Ledaal | 2d316661f4 | |
Vetle Ledaal | d586621f6e | |
Vetle Ledaal | 31ac8a4156 | |
AwkwardPeak7 | 8fd440d838 | |
Guuza | 78f2c9c650 | |
Vetle Ledaal | 23815a1ee1 | |
Vetle Ledaal | bc12176199 | |
bapeey | a176c34f73 | |
Cuong M. Tran | 756f3c63ea |
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -13,5 +13,9 @@ pref_show_paid_chapter_title=Display paid chapters
|
|||
pref_show_paid_chapter_summary_on=Paid chapters will appear.
|
||||
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
|
||||
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.
|
||||
id_not_found_error=Failed to get the ID for slug: %s
|
||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 21
|
||||
baseVersionCode = 22
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
|
|
@ -2,10 +2,12 @@ package eu.kanade.tachiyomi.multisrc.heancms
|
|||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
|
@ -14,7 +16,9 @@ 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.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
|
@ -43,6 +47,8 @@ abstract class HeanCms(
|
|||
|
||||
protected open val useNewChapterEndpoint = false
|
||||
|
||||
protected open val enableLogin = false
|
||||
|
||||
/**
|
||||
* Custom Json instance to make usage of `encodeDefaults`,
|
||||
* which is not enabled on the injected instance of the app.
|
||||
|
@ -70,6 +76,44 @@ abstract class HeanCms(
|
|||
.add("Origin", 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 {
|
||||
val url = "$apiUrl/query".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("query_string", "")
|
||||
|
@ -277,24 +321,30 @@ abstract class HeanCms(
|
|||
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast("#")
|
||||
|
||||
override fun pageListRequest(chapter: SChapter) =
|
||||
GET(apiUrl + chapter.url.replace("/$mangaSubDirectory/", "/chapter/"), headers)
|
||||
GET(apiUrl + chapter.url.replace("/$mangaSubDirectory/", "/chapter/"), authHeaders())
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val result = response.parseAs<HeanCmsPagePayloadDto>()
|
||||
|
||||
if (result.isPaywalled()) throw Exception(intl["paid_chapter_error"])
|
||||
if (result.isPaywalled() && result.chapter.chapterData == null) {
|
||||
throw Exception(intl["paid_chapter_error"])
|
||||
}
|
||||
|
||||
return if (useNewChapterEndpoint) {
|
||||
result.chapter.chapterData?.images.orEmpty().mapIndexed { i, img ->
|
||||
Page(i, imageUrl = img)
|
||||
Page(i, imageUrl = img.toAbsoluteUrl())
|
||||
}
|
||||
} else {
|
||||
result.data.orEmpty().mapIndexed { i, img ->
|
||||
Page(i, imageUrl = img)
|
||||
Page(i, imageUrl = img.toAbsoluteUrl())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 imageUrlParse(response: Response): String = ""
|
||||
|
@ -343,6 +393,32 @@ abstract class HeanCms(
|
|||
summaryOff = intl["pref_show_paid_chapter_summary_off"]
|
||||
setDefaultValue(SHOW_PAID_CHAPTERS_DEFAULT)
|
||||
}.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 {
|
||||
|
@ -357,6 +433,21 @@ abstract class HeanCms(
|
|||
private val SharedPreferences.showPaidChapters: Boolean
|
||||
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 {
|
||||
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, */*"
|
||||
|
@ -367,5 +458,12 @@ abstract class HeanCms(
|
|||
|
||||
private const val SHOW_PAID_CHAPTERS_PREF = "pref_show_paid_chap"
|
||||
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,6 +7,33 @@ import kotlinx.serialization.Serializable
|
|||
import org.jsoup.Jsoup
|
||||
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
|
||||
class HeanCmsQuerySearchDto(
|
||||
val data: List<HeanCmsSeriesDto> = emptyList(),
|
||||
|
@ -129,7 +156,7 @@ class HeanCmsPageDataDto(
|
|||
)
|
||||
|
||||
private fun String.toAbsoluteThumbnailUrl(apiUrl: String, coverPath: String): String {
|
||||
return if (startsWith("https://")) this else "$apiUrl/$coverPath$this"
|
||||
return if (startsWith("https://") || startsWith("http://")) this else "$apiUrl/$coverPath$this"
|
||||
}
|
||||
|
||||
fun String.toStatus(): Int = when (this) {
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
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
|
|
@ -0,0 +1,13 @@
|
|||
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
|
|
@ -0,0 +1,9 @@
|
|||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
}
|
|
@ -0,0 +1,248 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
package eu.kanade.tachiyomi.extension.es.mangaesp
|
||||
package eu.kanade.tachiyomi.multisrc.mangaesp
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
@Serializable
|
||||
class TopSeriesDto(
|
||||
|
@ -101,18 +102,22 @@ class ChapterDto(
|
|||
private val slug: String,
|
||||
@SerialName("created_at") private val date: String,
|
||||
) {
|
||||
fun toSChapter(seriesSlug: String, dateFormat: SimpleDateFormat): SChapter {
|
||||
fun toSChapter(seriesSlug: String): SChapter {
|
||||
return SChapter.create().apply {
|
||||
name = "Capítulo ${number.toString().removeSuffix(".0")}"
|
||||
if (!this@ChapterDto.name.isNullOrBlank()) {
|
||||
name += " - ${this@ChapterDto.name}"
|
||||
}
|
||||
date_upload = try {
|
||||
dateFormat.parse(date)?.time ?: 0L
|
||||
DATE_FORMATTER.parse(date)?.time ?: 0L
|
||||
} catch (e: Exception) {
|
||||
0L
|
||||
}
|
||||
url = "/ver/$seriesSlug/$slug"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMATTER by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) }
|
||||
}
|
||||
}
|
|
@ -2,8 +2,8 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
||||
baseVersionCode = 4
|
||||
|
||||
dependencies {
|
||||
compileOnly("com.github.tachiyomiorg:image-decoder:398d3c074f")
|
||||
compileOnly("com.github.tachiyomiorg:image-decoder:e08e9be535")
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ import uy.kohesive.injekt.Injekt
|
|||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
@ -183,7 +182,7 @@ abstract class PeachScan(
|
|||
val zis = ZipInputStream(response.body.byteStream())
|
||||
|
||||
val images = generateSequence { zis.nextEntry }
|
||||
.map {
|
||||
.mapNotNull {
|
||||
val entryName = it.name
|
||||
val splitEntryName = entryName.split('.')
|
||||
val entryIndex = splitEntryName.first().toInt()
|
||||
|
@ -195,7 +194,7 @@ abstract class PeachScan(
|
|||
val svgBytes = zis.readBytes()
|
||||
val svgContent = svgBytes.toString(Charsets.UTF_8)
|
||||
val b64 = dataUriRegex.find(svgContent)?.groupValues?.get(1)
|
||||
?: throw IOException("Não foi possível corresponder a imagem no conteúdo SVG")
|
||||
?: return@mapNotNull null
|
||||
|
||||
Base64.decode(b64, Base64.DEFAULT)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.graphics.Rect
|
|||
import tachiyomi.decoder.ImageDecoder
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.lang.reflect.Method
|
||||
|
||||
/**
|
||||
|
@ -18,40 +19,98 @@ import java.lang.reflect.Method
|
|||
*/
|
||||
object PeachScanUtils {
|
||||
private var decodeMethod: Method
|
||||
private var newInstanceMethod: Method
|
||||
|
||||
private var isNewDecodeMethod = false
|
||||
private var classSignature = ClassSignature.Newest
|
||||
|
||||
private enum class ClassSignature {
|
||||
Old, New, Newest
|
||||
}
|
||||
|
||||
init {
|
||||
val rectClass = Rect::class.java
|
||||
val booleanClass = Boolean::class.java
|
||||
val intClass = Int::class.java
|
||||
val byteArrayClass = ByteArray::class.java
|
||||
val inputStreamClass = InputStream::class.java
|
||||
|
||||
decodeMethod = try {
|
||||
isNewDecodeMethod = true
|
||||
try {
|
||||
// Mihon Preview r6595+
|
||||
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)
|
||||
ImageDecoder::class.java.getMethod("decode", rectClass, booleanClass, intClass, booleanClass, byteArrayClass)
|
||||
} catch (e: NoSuchMethodException) {
|
||||
isNewDecodeMethod = false
|
||||
decodeMethod = ImageDecoder::class.java.getMethod(
|
||||
"decode",
|
||||
rectClass,
|
||||
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)
|
||||
ImageDecoder::class.java.getMethod("decode", rectClass, booleanClass, intClass)
|
||||
decodeMethod =
|
||||
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 {
|
||||
val decoder = ImageDecoder.newInstance(ByteArrayInputStream(data))
|
||||
val decoder = when (classSignature) {
|
||||
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) {
|
||||
throw IOException("Falha ao inicializar o decodificador de imagem")
|
||||
}
|
||||
|
||||
val rect = Rect(0, 0, decoder.width, decoder.height)
|
||||
val bitmap = if (isNewDecodeMethod) {
|
||||
decodeMethod.invoke(decoder, rect, rgb565, 1, false, null)
|
||||
} else {
|
||||
decodeMethod.invoke(decoder, rect, rgb565, 1)
|
||||
val bitmap = when (classSignature) {
|
||||
ClassSignature.Newest -> decodeMethod.invoke(decoder, rect, 1)
|
||||
ClassSignature.New -> decodeMethod.invoke(decoder, rect, rgb565, 1, false, null)
|
||||
else -> decodeMethod.invoke(decoder, rect, rgb565, 1)
|
||||
} as Bitmap?
|
||||
|
||||
decoder.recycle()
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<?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>
|
|
@ -0,0 +1,12 @@
|
|||
ext {
|
||||
extName = 'Union Mangas'
|
||||
extClass = '.UnionMangasFactory'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:cryptoaes'))
|
||||
}
|
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 41 KiB |
|
@ -0,0 +1,238 @@
|
|||
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") }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
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", "#"),
|
||||
)
|
|
@ -0,0 +1,36 @@
|
|||
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()}"
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
ext {
|
||||
extName = 'GMANGA'
|
||||
extClass = '.Gmanga'
|
||||
themePkg = 'gmanga'
|
||||
overrideVersionCode = 13
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 18 KiB |
|
@ -1,33 +0,0 @@
|
|||
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,
|
||||
)
|
|
@ -1,90 +0,0 @@
|
|||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
ext {
|
||||
extName = 'Colored Manga'
|
||||
extClass = '.ColoredManga'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://coloredmanga.com'
|
||||
overrideVersionCode = 2
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 12 KiB |
|
@ -1,14 +0,0 @@
|
|||
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 {
|
||||
extName = 'ComicExtra'
|
||||
extClass = '.ComicExtra'
|
||||
extVersionCode = 14
|
||||
extVersionCode = 15
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -8,6 +8,7 @@ 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 okhttp3.Response
|
||||
|
@ -22,7 +23,7 @@ class ComicExtra : ParsedHttpSource() {
|
|||
|
||||
override val name = "ComicExtra"
|
||||
|
||||
override val baseUrl = "https://comicextra.me"
|
||||
override val baseUrl = "https://comicextra.org"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
|
@ -40,7 +41,11 @@ class ComicExtra : ParsedHttpSource() {
|
|||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return if (query.isNotBlank()) {
|
||||
GET("$baseUrl/comic-search?key=$query", headers)
|
||||
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("keyword", query)
|
||||
if (page > 1) addQueryParameter("page", page.toString())
|
||||
}.build()
|
||||
GET(url, headers)
|
||||
} else {
|
||||
var url = baseUrl
|
||||
filters.forEach { filter ->
|
||||
|
@ -153,7 +158,7 @@ class ComicExtra : ParsedHttpSource() {
|
|||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
document.select("img.chapter_img").forEachIndexed { i, img ->
|
||||
document.select("div.chapter-container img").forEachIndexed { i, img ->
|
||||
pages.add(Page(i, "", img.attr("abs:src")))
|
||||
}
|
||||
return pages
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
ext {
|
||||
extName = 'EZmanga'
|
||||
extClass = '.EZmanga'
|
||||
themePkg = 'madara'
|
||||
themePkg = 'keyoapp'
|
||||
baseUrl = 'https://ezmanga.net'
|
||||
overrideVersionCode = 0
|
||||
overrideVersionCode = 34
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,19 +1,12 @@
|
|||
package eu.kanade.tachiyomi.extension.en.ezmanga
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import eu.kanade.tachiyomi.multisrc.keyoapp.Keyoapp
|
||||
|
||||
class EZmanga : Madara(
|
||||
class EZmanga : Keyoapp(
|
||||
"EZmanga",
|
||||
"https://ezmanga.net",
|
||||
"en",
|
||||
dateFormat = SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH),
|
||||
) {
|
||||
override val useNewChapterEndpoint = true
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
.rateLimit(1)
|
||||
.build()
|
||||
// Migrated from Madara to Keyoapp
|
||||
override val versionId = 2
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Hentai2Read'
|
||||
extClass = '.Hentai2Read'
|
||||
extVersionCode = 14
|
||||
extVersionCode = 16
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ class Hentai2Read : ParsedHttpSource() {
|
|||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
thumbnail_url = element.select("img").attr("abs:data-src")
|
||||
thumbnail_url = element.select("img").attr("abs:src")
|
||||
element.select("div.overlay-title a").let {
|
||||
title = it.text()
|
||||
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.description = buildDescription(infoElement)
|
||||
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()
|
||||
return manga
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
ext {
|
||||
extName = 'Kewn Scans'
|
||||
extClass = '.KewnScans'
|
||||
themePkg = 'keyoapp'
|
||||
baseUrl = 'https://kewnscans.org'
|
||||
overrideVersionCode = 0
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 16 KiB |
|
@ -0,0 +1,5 @@
|
|||
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 {
|
||||
extName = 'Mangago'
|
||||
extClass = '.Mangago'
|
||||
extVersionCode = 13
|
||||
extVersionCode = 14
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -175,7 +175,8 @@ class Mangago : ParsedHttpSource() {
|
|||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
val link = element.select("a.chico")
|
||||
|
||||
setUrlWithoutDomain(link.attr("href"))
|
||||
val urlOriginal = link.attr("href")
|
||||
if (urlOriginal.startsWith("http")) url = urlOriginal else setUrlWithoutDomain(urlOriginal)
|
||||
name = link.text().trim()
|
||||
date_upload = runCatching {
|
||||
dateFormat.parse(element.select("td:last-child").text().trim())?.time
|
||||
|
@ -242,6 +243,13 @@ 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 =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
ext {
|
||||
extName = 'MangaJar'
|
||||
extClass = '.MangaJar'
|
||||
extVersionCode = 9
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 22 KiB |
|
@ -1,275 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
ext {
|
||||
extName = 'Manganelo.biz'
|
||||
extClass = '.ManganeloBiz'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://manganelo.biz'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 12 KiB |
|
@ -1,7 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
ext {
|
||||
extName = 'MangaNelos.com'
|
||||
extClass = '.MangaNelosCom'
|
||||
themePkg = 'paprika'
|
||||
baseUrl = 'http://manganelos.com'
|
||||
overrideVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 31 KiB |
|
@ -1,5 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.en.manganeloscom
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.paprika.Paprika
|
||||
|
||||
class MangaNelosCom : Paprika("MangaNelos.com", "http://manganelos.com", "en")
|
|
@ -1,10 +0,0 @@
|
|||
ext {
|
||||
extName = 'Manganelo.website (unoriginal)'
|
||||
extClass = '.ManganeloWebsite'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://manganelo.website'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 20 KiB |
|
@ -1,7 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
ext {
|
||||
extName = 'MangaRosie'
|
||||
extClass = '.MangaRosie'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://mangarosie.in'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 25 KiB |
|
@ -1,8 +0,0 @@
|
|||
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"
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
ext {
|
||||
extName = 'Muctau'
|
||||
extClass = '.Muctau'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://bibimanga.com'
|
||||
overrideVersionCode = 4
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 16 KiB |
|
@ -1,5 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.en.muctau
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
|
||||
class Muctau : Madara("Muctau", "https://bibimanga.com", "en")
|
|
@ -15,6 +15,7 @@ class OmegaScans : HeanCms("Omega Scans", "https://omegascans.org", "en") {
|
|||
override val versionId = 2
|
||||
|
||||
override val useNewChapterEndpoint = true
|
||||
override val enableLogin = true
|
||||
|
||||
override fun getGenreList() = listOf(
|
||||
Genre("Romance", 1),
|
||||
|
|
|
@ -3,7 +3,8 @@ ext {
|
|||
extClass = '.StoneScape'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://stonescape.xyz'
|
||||
overrideVersionCode = 0
|
||||
overrideVersionCode = 1
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|