diff --git a/.run/HeanCmsGenerator.run.xml b/.run/HeanCmsGenerator.run.xml new file mode 100644 index 000000000..f3b736bdd --- /dev/null +++ b/.run/HeanCmsGenerator.run.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/multisrc/overrides/heancms/default/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/heancms/default/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..906d6af2b Binary files /dev/null and b/multisrc/overrides/heancms/default/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/heancms/default/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/heancms/default/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..702ad7b64 Binary files /dev/null and b/multisrc/overrides/heancms/default/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/heancms/default/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/heancms/default/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..7c8f5d756 Binary files /dev/null and b/multisrc/overrides/heancms/default/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/heancms/default/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/heancms/default/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..cc83692bb Binary files /dev/null and b/multisrc/overrides/heancms/default/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/heancms/default/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/heancms/default/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..57c4616cc Binary files /dev/null and b/multisrc/overrides/heancms/default/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/heancms/default/res/web_hi_res_512.png b/multisrc/overrides/heancms/default/res/web_hi_res_512.png new file mode 100644 index 000000000..42d475ae1 Binary files /dev/null and b/multisrc/overrides/heancms/default/res/web_hi_res_512.png differ diff --git a/multisrc/overrides/heancms/reaperscans/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/heancms/reaperscans/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..f6c5fc5a7 Binary files /dev/null and b/multisrc/overrides/heancms/reaperscans/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/heancms/reaperscans/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/heancms/reaperscans/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..e7ffd65f3 Binary files /dev/null and b/multisrc/overrides/heancms/reaperscans/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/heancms/reaperscans/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/heancms/reaperscans/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..d79fd54e9 Binary files /dev/null and b/multisrc/overrides/heancms/reaperscans/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/heancms/reaperscans/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/heancms/reaperscans/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..779d9aea3 Binary files /dev/null and b/multisrc/overrides/heancms/reaperscans/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/heancms/reaperscans/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/heancms/reaperscans/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..08a0864d9 Binary files /dev/null and b/multisrc/overrides/heancms/reaperscans/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/heancms/reaperscans/res/web_hi_res_512.png b/multisrc/overrides/heancms/reaperscans/res/web_hi_res_512.png new file mode 100644 index 000000000..1c6f8c99c Binary files /dev/null and b/multisrc/overrides/heancms/reaperscans/res/web_hi_res_512.png differ diff --git a/multisrc/overrides/heancms/reaperscans/src/ReaperScans.kt b/multisrc/overrides/heancms/reaperscans/src/ReaperScans.kt new file mode 100644 index 000000000..4c324546e --- /dev/null +++ b/multisrc/overrides/heancms/reaperscans/src/ReaperScans.kt @@ -0,0 +1,48 @@ +package eu.kanade.tachiyomi.extension.pt.reaperscans + +import eu.kanade.tachiyomi.multisrc.heancms.Genre +import eu.kanade.tachiyomi.multisrc.heancms.HeanCms +import eu.kanade.tachiyomi.network.interceptor.rateLimitHost +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient + +class ReaperScans : HeanCms( + "Reaper Scans", + "https://reaperscans.com.br", + "pt-BR" +) { + + override val client: OkHttpClient = super.client.newBuilder() + .rateLimitHost(apiUrl.toHttpUrl(), 1, 2) + .build() + + // Site changed from Madara to HeanCms. + override val versionId = 2 + + override fun getGenreList(): List = listOf( + Genre("Artes Marciais", 2), + Genre("Aventura", 10), + Genre("Ação", 9), + Genre("Comédia", 14), + Genre("Drama", 15), + Genre("Escolar", 7), + Genre("Fantasia", 11), + Genre("Ficção científica", 16), + Genre("Guerra", 17), + Genre("Isekai", 18), + Genre("Jogo", 12), + Genre("Mangá", 24), + Genre("Manhua", 23), + Genre("Manhwa", 22), + Genre("Mecha", 19), + Genre("Mistério", 20), + Genre("Nacional", 8), + Genre("Realidade Virtual", 21), + Genre("Retorno", 3), + Genre("Romance", 5), + Genre("Segunda vida", 4), + Genre("Seinen", 1), + Genre("Shounen", 13), + Genre("Terror", 6) + ) +} diff --git a/multisrc/overrides/heancms/yugenmangas/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/heancms/yugenmangas/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..4ffac3c37 Binary files /dev/null and b/multisrc/overrides/heancms/yugenmangas/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/heancms/yugenmangas/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/heancms/yugenmangas/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..951c8a5b3 Binary files /dev/null and b/multisrc/overrides/heancms/yugenmangas/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/heancms/yugenmangas/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/heancms/yugenmangas/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..e0c512945 Binary files /dev/null and b/multisrc/overrides/heancms/yugenmangas/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/heancms/yugenmangas/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/heancms/yugenmangas/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..e7c0b0c7d Binary files /dev/null and b/multisrc/overrides/heancms/yugenmangas/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/heancms/yugenmangas/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/heancms/yugenmangas/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..110565c56 Binary files /dev/null and b/multisrc/overrides/heancms/yugenmangas/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/heancms/yugenmangas/res/web_hi_res_512.png b/multisrc/overrides/heancms/yugenmangas/res/web_hi_res_512.png new file mode 100644 index 000000000..6e3163991 Binary files /dev/null and b/multisrc/overrides/heancms/yugenmangas/res/web_hi_res_512.png differ diff --git a/multisrc/overrides/heancms/yugenmangas/src/YugenMangas.kt b/multisrc/overrides/heancms/yugenmangas/src/YugenMangas.kt new file mode 100644 index 000000000..cf01a7677 --- /dev/null +++ b/multisrc/overrides/heancms/yugenmangas/src/YugenMangas.kt @@ -0,0 +1,61 @@ +package eu.kanade.tachiyomi.extension.es.yugenmangas + +import eu.kanade.tachiyomi.multisrc.heancms.Genre +import eu.kanade.tachiyomi.multisrc.heancms.HeanCms + +class YugenMangas : HeanCms("YugenMangas", "https://yugenmangas.com", "es") { + + // Site changed from Madara to HeanCms. + override val versionId = 2 + + override fun getGenreList(): List = listOf( + Genre("+18", 1), + Genre("Acción", 36), + Genre("Adulto", 38), + Genre("Apocalíptico", 3), + Genre("Artes marciales (1)", 16), + Genre("Artes marciales (2)", 37), + Genre("Aventura", 2), + Genre("Boys Love", 4), + Genre("Ciencia ficción", 39), + Genre("Comedia", 5), + Genre("Demonios", 6), + Genre("Deporte", 26), + Genre("Drama", 7), + Genre("Ecchi", 8), + Genre("Familia", 9), + Genre("Fantasía", 10), + Genre("Girls Love", 11), + Genre("Gore", 12), + Genre("Harem", 13), + Genre("Harem inverso", 14), + Genre("Histórico", 48), + Genre("Horror", 41), + Genre("Isekai", 40), + Genre("Josei", 15), + Genre("Maduro", 42), + Genre("Magia", 17), + Genre("MangoScan", 35), + Genre("Mecha", 18), + Genre("Militar", 19), + Genre("Misterio", 20), + Genre("Psicológico", 21), + Genre("Realidad virtual", 46), + Genre("Recuentos de la vida", 25), + Genre("Reencarnación", 22), + Genre("Regresion", 23), + Genre("Romance", 24), + Genre("Seinen", 27), + Genre("Shonen", 28), + Genre("Shoujo", 29), + Genre("Sistema", 45), + Genre("Smut", 30), + Genre("Supernatural", 31), + Genre("Supervivencia", 32), + Genre("Tragedia", 33), + Genre("Transmigración", 34), + Genre("Vida Escolar", 47), + Genre("Yaoi", 43), + Genre("Yuri", 44) + ) +} diff --git a/multisrc/overrides/madara/yugenmangas/src/YugenMangasFactory.kt b/multisrc/overrides/madara/yugenmangas/src/YugenMangas.kt similarity index 67% rename from multisrc/overrides/madara/yugenmangas/src/YugenMangasFactory.kt rename to multisrc/overrides/madara/yugenmangas/src/YugenMangas.kt index f5c1202de..2a0c26b1b 100644 --- a/multisrc/overrides/madara/yugenmangas/src/YugenMangasFactory.kt +++ b/multisrc/overrides/madara/yugenmangas/src/YugenMangas.kt @@ -1,11 +1,10 @@ -package eu.kanade.tachiyomi.extension.all.yugenmangas +package eu.kanade.tachiyomi.extension.pt.yugenmangas import eu.kanade.tachiyomi.multisrc.madara.Madara import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.interceptor.rateLimit -import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.decodeFromString import okhttp3.Headers import okhttp3.Interceptor import okhttp3.OkHttpClient @@ -15,34 +14,8 @@ import java.text.SimpleDateFormat import java.util.Locale import java.util.concurrent.TimeUnit -class YugenMangasFactory : SourceFactory { - override fun createSources() = listOf( - YugenMangasEs(), - YugenMangasBr() - ) -} - -abstract class YugenMangas( - override val baseUrl: String, - lang: String, - dateFormat: SimpleDateFormat = SimpleDateFormat("MMMMM dd, yyyy", Locale.US) -) : Madara("YugenMangas", baseUrl, lang, dateFormat) { - - override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { - name = element.selectFirst("p.chapter-manhwa-title")!!.text() - date_upload = parseChapterDate(element.selectFirst("span.chapter-release-date i")?.text()) - - val chapterUrl = element.selectFirst("a")!!.attr("abs:href") - setUrlWithoutDomain( - chapterUrl.substringBefore("?style=paged") + - if (!chapterUrl.endsWith(chapterUrlSuffix)) chapterUrlSuffix else "" - ) - } -} - -class YugenMangasEs : YugenMangas("https://yugenmangas.com", "es") - -class YugenMangasBr : YugenMangas( +class YugenMangas : Madara( + "YugenMangas", "https://yugenmangas.com.br", "pt-BR", SimpleDateFormat("MMMMM dd, yyyy", Locale("pt", "BR")) @@ -64,19 +37,26 @@ class YugenMangasBr : YugenMangas( override val useNewChapterEndpoint: Boolean = true + override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { + name = element.selectFirst("p.chapter-manhwa-title")!!.text() + date_upload = parseChapterDate(element.selectFirst("span.chapter-release-date i")?.text()) + + val chapterUrl = element.selectFirst("a")!!.attr("abs:href") + setUrlWithoutDomain( + chapterUrl.substringBefore("?style=paged") + + if (!chapterUrl.endsWith(chapterUrlSuffix)) chapterUrlSuffix else "" + ) + } + private var userAgent: String? = null private var checkedUa = false private fun uaIntercept(chain: Interceptor.Chain): Response { if (userAgent == null && !checkedUa) { - val browser = BROWSERS.random() - val uaResponse = chain.proceed(GET("$UA_DB_URL/$browser")) + val uaResponse = chain.proceed(GET(UA_DB_URL)) if (uaResponse.isSuccessful) { - userAgent = uaResponse.asJsoup() - .select(".listing-of-useragents span.code") - .firstOrNull() - ?.text() + userAgent = json.decodeFromString>(uaResponse.body!!.string()).random() checkedUa = true } @@ -95,7 +75,6 @@ class YugenMangasBr : YugenMangas( } companion object { - private val BROWSERS = arrayOf("chrome", "firefox", "edge", "opera", "vivaldi") - private const val UA_DB_URL = "https://whatismybrowser.com/guides/the-latest-user-agent" + private const val UA_DB_URL = "https://tachiyomiorg.github.io/user-agents/user-agents.json" } } diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/heancms/HeanCms.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/heancms/HeanCms.kt new file mode 100644 index 000000000..59c3a5d9c --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/heancms/HeanCms.kt @@ -0,0 +1,289 @@ +package eu.kanade.tachiyomi.multisrc.heancms + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +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.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import rx.Observable +import uy.kohesive.injekt.injectLazy + +abstract class HeanCms( + override val name: String, + override val baseUrl: String, + override val lang: String, + protected val apiUrl: String = baseUrl.replace("://", "://api.") +) : HttpSource() { + + override val supportsLatest = true + + override val client: OkHttpClient = network.cloudflareClient + + protected val json: Json by injectLazy() + + protected val intl by lazy { HeanCmsIntl(lang) } + + private var seriesSlugMap: Map? = null + + override fun headersBuilder(): Headers.Builder = Headers.Builder() + .add("Origin", baseUrl) + .add("Referer", "$baseUrl/") + + override fun popularMangaRequest(page: Int): Request { + val payloadObj = HeanCmsSearchDto( + order = "desc", + orderBy = "total_views", + status = "Ongoing", + type = "Comic" + ) + + val payload = json.encodeToString(payloadObj).toRequestBody(JSON_MEDIA_TYPE) + + val apiHeaders = headersBuilder() + .add("Accept", ACCEPT_JSON) + .add("Content-Type", payload.contentType().toString()) + .build() + + return POST("$apiUrl/series/querysearch", apiHeaders, payload) + } + + override fun popularMangaParse(response: Response): MangasPage { + val mangaList = response.parseAs>() + .map { it.toSManga(apiUrl) } + + fetchAllTitles() + + return MangasPage(mangaList, hasNextPage = false) + } + + override fun latestUpdatesRequest(page: Int): Request { + val payloadObj = HeanCmsSearchDto( + order = "desc", + orderBy = "latest", + status = "Ongoing", + type = "Comic" + ) + + val payload = json.encodeToString(payloadObj).toRequestBody(JSON_MEDIA_TYPE) + + val apiHeaders = headersBuilder() + .add("Accept", ACCEPT_JSON) + .add("Content-Type", payload.contentType().toString()) + .build() + + return POST("$apiUrl/series/querysearch", apiHeaders, payload) + } + + override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response) + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val sortByFilter = filters.firstInstanceOrNull() + + val payloadObj = HeanCmsSearchDto( + order = if (sortByFilter?.state?.ascending == true) "asc" else "desc", + orderBy = sortByFilter?.selected ?: "total_views", + status = filters.firstInstanceOrNull()?.selected?.value ?: "Ongoing", + type = "Comic", + tagIds = filters.firstInstanceOrNull()?.state + ?.filter(Genre::state) + ?.map(Genre::id) + .orEmpty() + ) + + val payload = json.encodeToString(payloadObj).toRequestBody(JSON_MEDIA_TYPE) + + val apiHeaders = headersBuilder() + .add("Accept", ACCEPT_JSON) + .add("Content-Type", payload.contentType().toString()) + .build() + + val apiUrl = "$apiUrl/series/querysearch".toHttpUrl().newBuilder() + .addQueryParameter("q", query) + .toString() + + return POST(apiUrl, apiHeaders, payload) + } + + override fun searchMangaParse(response: Response): MangasPage { + val query = response.request.url.queryParameter("q").orEmpty() + + var mangaList = response.parseAs>() + .map { it.toSManga(apiUrl) } + + if (query.isNotBlank()) { + mangaList = mangaList.filter { it.title.contains(query, ignoreCase = true) } + } + + fetchAllTitles() + + return MangasPage(mangaList, hasNextPage = false) + } + + // Workaround to allow "Open in browser" use the real URL. + override fun fetchMangaDetails(manga: SManga): Observable { + return client.newCall(seriesDetailsRequest(manga)) + .asObservableSuccess() + .map { response -> + mangaDetailsParse(response).apply { initialized = true } + } + } + + private fun seriesDetailsRequest(manga: SManga): Request { + val seriesSlug = manga.url + .substringAfterLast("/") + .replace(TIMESTAMP_REGEX, "") + + val currentSlug = seriesSlugMap?.get(seriesSlug) ?: seriesSlug + + val apiHeaders = headersBuilder() + .add("Accept", ACCEPT_JSON) + .build() + + return GET("$apiUrl/series/$currentSlug#${manga.status}", apiHeaders) + } + + override fun mangaDetailsParse(response: Response): SManga { + val result = runCatching { response.parseAs() } + val seriesDetails = result.getOrNull()?.toSManga(apiUrl) + ?: throw Exception(intl.urlChangedError(name)) + + return seriesDetails.apply { + status = response.request.url.fragment?.toIntOrNull() ?: SManga.UNKNOWN + } + } + + override fun chapterListRequest(manga: SManga): Request = seriesDetailsRequest(manga) + + override fun chapterListParse(response: Response): List { + val result = response.parseAs() + val seriesSlug = response.request.url.pathSegments.last() + + return result.chapters.orEmpty() + .map { it.toSChapter(seriesSlug) } + .reversed() + } + + override fun pageListRequest(chapter: SChapter): Request { + val chapterId = chapter.url.substringAfterLast("#") + + val apiHeaders = headersBuilder() + .add("Accept", ACCEPT_JSON) + .build() + + return GET("$apiUrl/series/chapter/$chapterId", apiHeaders) + } + + override fun pageListParse(response: Response): List { + return response.parseAs().content?.images.orEmpty() + .mapIndexed { i, url -> Page(i, "", "$apiUrl/$url") } + } + + override fun fetchImageUrl(page: Page): Observable = Observable.just(page.imageUrl!!) + + override fun imageUrlParse(response: Response): String = "" + + override fun imageRequest(page: Page): Request { + val imageHeaders = headersBuilder() + .add("Accept", ACCEPT_IMAGE) + .build() + + return GET(page.imageUrl!!, imageHeaders) + } + + protected open fun getStatusList(): List = listOf( + Status(intl.statusOngoing, "Ongoing"), + Status(intl.statusOnHiatus, "Hiatus"), + Status(intl.statusDropped, "Dropped"), + ) + + protected open fun getSortProperties(): List = listOf( + SortProperty(intl.sortByTitle, "title"), + SortProperty(intl.sortByViews, "total_views"), + SortProperty(intl.sortByLatest, "latest"), + SortProperty(intl.sortByRecentlyAdded, "recently_added"), + ) + + protected open fun getGenreList(): List = emptyList() + + protected open fun fetchAllTitles() { + if (!seriesSlugMap.isNullOrEmpty()) { + return + } + + val result = runCatching { + client.newCall(allTitlesRequest()).execute() + .let { parseAllTitles(it) } + } + + seriesSlugMap = result.getOrNull() + } + + protected open fun allTitlesRequest(): Request { + val payloadObj = HeanCmsSearchDto( + order = "desc", + orderBy = "total_views", + status = "", + type = "Comic" + ) + + val payload = json.encodeToString(payloadObj).toRequestBody(JSON_MEDIA_TYPE) + + val apiHeaders = headersBuilder() + .add("Accept", ACCEPT_JSON) + .add("Content-Type", payload.contentType().toString()) + .build() + + return POST("$apiUrl/series/querysearch", apiHeaders, payload) + } + + protected open fun parseAllTitles(response: Response): Map { + return response.parseAs>() + .filter { it.type == "Comic" } + .associateBy( + keySelector = { it.slug.replace(TIMESTAMP_REGEX, "") }, + valueTransform = HeanCmsSeriesDto::slug + ) + } + + override fun getFilterList(): FilterList { + val genres = getGenreList() + + val filters = listOfNotNull( + StatusFilter(intl.statusFilterTitle, getStatusList()), + SortByFilter(intl.sortByFilterTitle, getSortProperties()), + GenreFilter(intl.genreFilterTitle, genres).takeIf { genres.isNotEmpty() } + ) + + return FilterList(filters) + } + + private inline fun Response.parseAs(): T = use { + json.decodeFromString(it.body?.string().orEmpty()) + } + + private inline fun List<*>.firstInstanceOrNull(): R? = + filterIsInstance().firstOrNull() + + 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, */*" + + private val JSON_MEDIA_TYPE = "application/json".toMediaType() + + val TIMESTAMP_REGEX = "-\\d+$".toRegex() + } +} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsDto.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsDto.kt new file mode 100644 index 000000000..1c6f6d48b --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsDto.kt @@ -0,0 +1,93 @@ +package eu.kanade.tachiyomi.multisrc.heancms + +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.jsoup.Jsoup +import java.text.SimpleDateFormat +import java.util.Locale + +@Serializable +data class HeanCmsSeriesDto( + val id: Int, + @SerialName("series_slug") val slug: String, + @SerialName("series_type") val type: String = "Comic", + val author: String? = null, + val description: String? = null, + val studio: String? = null, + val status: String? = null, + val thumbnail: String, + val title: String, + val tags: List? = emptyList(), + val chapters: List? = emptyList() +) { + + fun toSManga(apiUrl: String): SManga = SManga.create().apply { + val descriptionBody = this@HeanCmsSeriesDto.description?.let(Jsoup::parseBodyFragment) + + title = this@HeanCmsSeriesDto.title + author = this@HeanCmsSeriesDto.author?.trim() + artist = this@HeanCmsSeriesDto.studio?.trim() + description = descriptionBody?.select("p") + ?.joinToString("\n\n") { it.text() } + ?.ifEmpty { descriptionBody.text().replace("\n", "\n\n") } + genre = tags.orEmpty() + .sortedBy(HeanCmsTagDto::name) + .joinToString { it.name } + thumbnail_url = "$apiUrl/cover/$thumbnail" + status = when (this@HeanCmsSeriesDto.status) { + "Ongoing" -> SManga.ONGOING + "Hiatus" -> SManga.ON_HIATUS + "Dropped" -> SManga.CANCELLED + "Completed", "Finished" -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + url = "/series/${slug.replace(HeanCms.TIMESTAMP_REGEX, "")}" + } +} + +@Serializable +data class HeanCmsTagDto(val name: String) + +@Serializable +data class HeanCmsChapterDto( + val id: Int, + @SerialName("chapter_name") val name: String, + @SerialName("chapter_slug") val slug: String, + val index: String, + @SerialName("created_at") val createdAt: String, +) { + + fun toSChapter(seriesSlug: String): SChapter = SChapter.create().apply { + name = this@HeanCmsChapterDto.name.trim() + date_upload = runCatching { DATE_FORMAT.parse(createdAt.substringBefore("."))?.time } + .getOrNull() ?: 0L + url = "/series/$seriesSlug/$slug#$id" + } + + companion object { + private val DATE_FORMAT by lazy { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US) + } + } +} + +@Serializable +data class HeanCmsReaderDto( + val content: HeanCmsReaderContentDto? = null +) + +@Serializable +data class HeanCmsReaderContentDto( + val images: List? = emptyList() +) + +@Serializable +data class HeanCmsSearchDto( + val order: String, + @SerialName("order_by") val orderBy: String, + @SerialName("series_status") val status: String, + @SerialName("series_type") val type: String, + @SerialName("tags_ids") val tagIds: List = emptyList() +) diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsFilters.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsFilters.kt new file mode 100644 index 000000000..14c3c919b --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsFilters.kt @@ -0,0 +1,32 @@ +package eu.kanade.tachiyomi.multisrc.heancms + +import eu.kanade.tachiyomi.source.model.Filter + +class Genre(title: String, val id: Int) : Filter.CheckBox(title) + +class GenreFilter(title: String, genres: List) : Filter.Group(title, genres) + +open class EnhancedSelect(name: String, values: Array) : Filter.Select(name, values) { + val selected: T + get() = values[state] +} + +data class Status(val name: String, val value: String) { + override fun toString(): String = name +} + +class StatusFilter(title: String, statuses: List) : + EnhancedSelect(title, statuses.toTypedArray()) + +data class SortProperty(val name: String, val value: String) { + override fun toString(): String = name +} + +class SortByFilter(title: String, private val sortProperties: List) : Filter.Sort( + title, + sortProperties.map { it.name }.toTypedArray(), + Selection(1, ascending = false) +) { + val selected: String + get() = sortProperties[state!!.index].value +} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsGenerator.kt new file mode 100644 index 000000000..20acaeb5f --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsGenerator.kt @@ -0,0 +1,25 @@ +package eu.kanade.tachiyomi.multisrc.heancms + +import generator.ThemeSourceData.SingleLang +import generator.ThemeSourceGenerator + +class HeanCmsGenerator : ThemeSourceGenerator { + + override val themePkg = "heancms" + + override val themeClass = "HeanCms" + + override val baseVersionCode: Int = 1 + + override val sources = listOf( + SingleLang("Reaper Scans", "https://reaperscans.com.br", "pt-BR", overrideVersionCode = 33), + SingleLang("YugenMangas", "https://yugenmangas.com", "es", isNsfw = true), + ) + + companion object { + @JvmStatic + fun main(args: Array) { + HeanCmsGenerator().createAll() + } + } +} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsIntl.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsIntl.kt new file mode 100644 index 000000000..a63beaf92 --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsIntl.kt @@ -0,0 +1,86 @@ +package eu.kanade.tachiyomi.multisrc.heancms + +class HeanCmsIntl(lang: String) { + + val availableLang: String = if (lang in AVAILABLE_LANGS) lang else ENGLISH + + val genreFilterTitle: String = when (availableLang) { + BRAZILIAN_PORTUGUESE -> "Gêneros" + SPANISH -> "Géneros" + else -> "Genres" + } + + val statusFilterTitle: String = when (availableLang) { + BRAZILIAN_PORTUGUESE -> "Estado" + SPANISH -> "Estado" + else -> "Status" + } + + val statusOngoing: String = when (availableLang) { + BRAZILIAN_PORTUGUESE -> "Em andamento" + SPANISH -> "En curso" + else -> "Ongoing" + } + + val statusOnHiatus: String = when (availableLang) { + BRAZILIAN_PORTUGUESE -> "Em hiato" + SPANISH -> "En hiatus" + else -> "Ongoing" + } + + val statusDropped: String = when (availableLang) { + BRAZILIAN_PORTUGUESE -> "Cancelada" + SPANISH -> "Abandonada" + else -> "Dropped" + } + + val sortByFilterTitle: String = when (availableLang) { + BRAZILIAN_PORTUGUESE -> "Ordenar por" + SPANISH -> "Ordenar por" + else -> "Sort by" + } + + val sortByTitle: String = when (availableLang) { + BRAZILIAN_PORTUGUESE -> "Título" + SPANISH -> "Titulo" + else -> "Title" + } + + val sortByViews: String = when (availableLang) { + BRAZILIAN_PORTUGUESE -> "Visualizações" + SPANISH -> "Número de vistas" + else -> "Views" + } + + val sortByLatest: String = when (availableLang) { + BRAZILIAN_PORTUGUESE -> "Recentes" + SPANISH -> "Recientes" + else -> "Latest" + } + + val sortByRecentlyAdded: String = when (availableLang) { + BRAZILIAN_PORTUGUESE -> "Data de criação" + SPANISH -> "Añadido recientemente" + else -> "Recently added" + } + + fun urlChangedError(sourceName: String): String = when (availableLang) { + BRAZILIAN_PORTUGUESE -> + "A URL da série mudou. Migre de $sourceName " + + "para $sourceName para atualizar a URL." + SPANISH -> + "La URL de la serie ha cambiado. Migre de $sourceName a " + + "$sourceName para actualizar la URL." + else -> + "The URL of the series has changed. Migrate from $sourceName " + + "to $sourceName to update the URL." + } + + companion object { + const val BRAZILIAN_PORTUGUESE = "pt-BR" + const val ENGLISH = "en" + const val SPANISH = "es" + + val AVAILABLE_LANGS = arrayOf(BRAZILIAN_PORTUGUESE, ENGLISH, SPANISH) + } +} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt index 5cd9e6faf..dfeedb0b0 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt @@ -19,7 +19,6 @@ class MadaraGenerator : ThemeSourceGenerator { MultiLang("Olympus Scanlation", "https://olympusscanlation.com", listOf("es", "pt-BR")), MultiLang("Reaper Scans", "https://reaperscans.com", listOf("en", "fr", "id", "tr"), className = "ReaperScansFactory", pkgName = "reaperscans", overrideVersionCode = 7), MultiLang("Seven King Scanlation", "https://sksubs.net", listOf("es", "en"), isNsfw = true), - MultiLang("YugenMangas", "https://yugenmangas.com", listOf("es", "pt-BR"), overrideVersionCode = 3), SingleLang("1st Kiss Manga.love", "https://1stkissmanga.love", "en", className = "FirstKissMangaLove", overrideVersionCode = 1), SingleLang("1st Kiss Manhua", "https://1stkissmanhua.com", "en", className = "FirstKissManhua", overrideVersionCode = 3), SingleLang("1st Kiss", "https://1stkissmanga.io", "en", className = "FirstKissManga", pkgName = "firstkissmanga", overrideVersionCode = 7), @@ -486,6 +485,7 @@ class MadaraGenerator : ThemeSourceGenerator { SingleLang("YaoiToon", "https://yaoitoon.com", "en", isNsfw = true), SingleLang("Yetişkin Rüya Manga", "https://yetiskin.ruyamanga.com", "tr", isNsfw = true, className = "YetiskinRuyaManga"), SingleLang("YonaBar", "https://yonabar.com", "ar", isNsfw = true, overrideVersionCode = 2), + SingleLang("YugenMangas", "https://yugenmangas.com.br", "pt-BR"), SingleLang("Yuri Verso", "https://yuri.live", "pt-BR", overrideVersionCode = 3), SingleLang("Zinmanga", "https://zinmanga.com", "en", overrideVersionCode = 1), SingleLang("Zinmanhwa", "https://zinmanhwa.com", "en"),