diff --git a/src/all/unionmangas/build.gradle b/src/all/unionmangas/build.gradle index 7d42dde73..308db3159 100644 --- a/src/all/unionmangas/build.gradle +++ b/src/all/unionmangas/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Union Mangas' extClass = '.UnionMangasFactory' - extVersionCode = 3 + extVersionCode = 4 isNsfw = true } diff --git a/src/all/unionmangas/res/mipmap-hdpi/ic_launcher.png b/src/all/unionmangas/res/mipmap-hdpi/ic_launcher.png index 869d060df..192b1691f 100644 Binary files a/src/all/unionmangas/res/mipmap-hdpi/ic_launcher.png and b/src/all/unionmangas/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/unionmangas/res/mipmap-mdpi/ic_launcher.png b/src/all/unionmangas/res/mipmap-mdpi/ic_launcher.png index 03dacb35d..7a1314df7 100644 Binary files a/src/all/unionmangas/res/mipmap-mdpi/ic_launcher.png and b/src/all/unionmangas/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/unionmangas/res/mipmap-xhdpi/ic_launcher.png b/src/all/unionmangas/res/mipmap-xhdpi/ic_launcher.png index f87d63d82..37a770ead 100644 Binary files a/src/all/unionmangas/res/mipmap-xhdpi/ic_launcher.png and b/src/all/unionmangas/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/unionmangas/res/mipmap-xxhdpi/ic_launcher.png b/src/all/unionmangas/res/mipmap-xxhdpi/ic_launcher.png index 10b4edef9..3422a06e2 100644 Binary files a/src/all/unionmangas/res/mipmap-xxhdpi/ic_launcher.png and b/src/all/unionmangas/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/unionmangas/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/unionmangas/res/mipmap-xxxhdpi/ic_launcher.png index aa1de0bf5..8929bdad1 100644 Binary files a/src/all/unionmangas/res/mipmap-xxxhdpi/ic_launcher.png and b/src/all/unionmangas/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/unionmangas/src/eu/kanade/tachiyomi/extension/all/unionmangas/UnionMangas.kt b/src/all/unionmangas/src/eu/kanade/tachiyomi/extension/all/unionmangas/UnionMangas.kt index 1aeba3de5..b52dcf439 100644 --- a/src/all/unionmangas/src/eu/kanade/tachiyomi/extension/all/unionmangas/UnionMangas.kt +++ b/src/all/unionmangas/src/eu/kanade/tachiyomi/extension/all/unionmangas/UnionMangas.kt @@ -1,6 +1,5 @@ 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 @@ -10,22 +9,16 @@ 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 @@ -38,39 +31,12 @@ class UnionMangas(private val langOption: LanguageOption) : HttpSource() { 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) + .rateLimit(2) .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 headersBuilder(): Headers.Builder = super.headersBuilder() + .set("Referer", "$baseUrl/") override fun chapterListParse(response: Response) = throw UnsupportedOperationException() @@ -79,95 +45,101 @@ class UnionMangas(private val langOption: LanguageOption) : HttpSource() { var currentPage = 0 do { val chaptersDto = fetchChapterListPageable(manga, currentPage) - chapters += chaptersDto.toSChapter(langOption) + chapters += chaptersDto.data.map { chapter -> + SChapter.create().apply { + name = chapter.name + date_upload = chapter.date.toDate() + url = chapter.toChapterUrl(langOption.infix) + } + } currentPage++ } while (chaptersDto.hasNextPage()) - return Observable.just(chapters.reversed()) + return Observable.just(chapters) } - private fun fetchChapterListPageable(manga: SManga, page: Int): ChapterPageDto { + private fun fetchChapterListPageable(manga: SManga, page: Int): Pageable { + manga.apply { + url = getURLCompatibility(url) + } + val maxResult = 16 - val url = "$apiUrl/api/$langApiInfix/GetChapterListFilter/${manga.slug()}/$maxResult/$page/all/ASC" - return client.newCall(GET(url, apiHeaders(url))).execute() - .parseAs() + val url = "$apiUrl/${langOption.infix}/GetChapterListFilter/${manga.slug()}/$maxResult/$page/all/ASC" + return client.newCall(GET(url, headers)).execute() + .parseAs>() } - override fun latestUpdatesParse(response: Response): MangasPage { - val nextData = response.parseNextData() - val dto = nextData.data.latestUpdateDto - val mangas = dto.mangas.map { mangaParse(it, nextData.query) } + override fun latestUpdatesParse(response: Response) = popularMangaParse(response) + override fun latestUpdatesRequest(page: Int): Request { + val maxResult = 24 + val url = "$apiUrl/${langOption.infix}/HomeLastUpdate".toHttpUrl().newBuilder() + .addPathSegment("$maxResult") + .addPathSegment("${page - 1}") + .build() + return GET(url, headers) + } + + override fun getMangaUrl(manga: SManga): String { + manga.apply { + url = getURLCompatibility(url) + } + + return baseUrl + manga.url.replace(langOption.infix, langOption.mangaSubstring) + } + + override fun mangaDetailsRequest(manga: SManga): Request { + manga.apply { + url = getURLCompatibility(url) + } + + val url = "$apiUrl/${langOption.infix}/getInfoManga".toHttpUrl().newBuilder() + .addPathSegment(manga.slug()) + .build() + return GET(url, headers) + } + + override fun mangaDetailsParse(response: Response): SManga { + val dto = response.parseAs() + return mangaParse(dto.details) + } + + override fun pageListRequest(chapter: SChapter): Request { + val chapterSlug = getURLCompatibility(chapter.url) + .substringAfter(langOption.infix) + + val url = "$apiUrl/${langOption.infix}/GetImageChapter$chapterSlug" + return GET(url, headers) + } + + override fun pageListParse(response: Response): List { + val location = response.request.url.toString() + val dto = response.parseAs() + return dto.pages.mapIndexed { index, url -> + Page(index, location, imageUrl = url) + } + } + + override fun popularMangaParse(response: Response): MangasPage { + val dto = response.parseAs>() + val mangas = dto.data.map(::mangaParse) 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 popularMangaRequest(page: Int): Request { + val maxResult = 24 + return GET("$apiUrl/${langOption.infix}/HomeTopFllow/$maxResult/${page - 1}") } - override fun mangaDetailsParse(response: Response): SManga { - val nextData = response.parseNextData() - 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 { - 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().data.pageListData - val decodedData = CryptoAES.decrypt(pageListData, password) - return ChaptersDto( - data = json.decodeFromString(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+,"(?[^"]+)"\)""".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() - 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() + val maxResult = 20 + val url = "$apiUrl/${langOption.infix}/QuickSearch/".toHttpUrl().newBuilder() .addPathSegment(query) - .addPathSegment("${page - 1}") + .addPathSegment("$maxResult") .build() - return GET(url, apiHeaders(url.toString())) + return GET(url, headers) } override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { @@ -185,52 +157,54 @@ class UnionMangas(private val langOption: LanguageOption) : HttpSource() { override fun imageUrlParse(response: Response): String = "" override fun searchMangaParse(response: Response): MangasPage { - val mangasDto = response.parseAs().apply { - currentPage = response.request.url.pathSegments.last() - } - + val dto = response.parseAs() return MangasPage( - mangas = mangasDto.toSManga(langOption.infix), - hasNextPage = mangasDto.hasNextPage(), + dto.mangas.map(::mangaParse), + false, ) } - private inline fun Response.parseNextData() = asJsoup().parseNextData() + /* + * Keeps compatibility with pt-BR previous version + * */ + private fun getURLCompatibility(url: String): String { + val slugSuffix = "-br" + val mangaSubString = "manga-br" - private inline fun Document.parseNextData(): NextData { - val jsonContent = selectFirst("script#__NEXT_DATA__")!!.html() - return json.decodeFromString>(jsonContent) + val oldSlug = url.substringAfter(mangaSubString) + .substring(1) + .split("/") + .first() + + val newSlug = oldSlug.substringBeforeLast(slugSuffix) + + return url.replace(oldSlug, newSlug) } private inline fun 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 { + private fun mangaParse(dto: MangaDto): SManga { return SManga.create().apply { title = dto.title thumbnail_url = dto.thumbnailUrl status = dto.status - url = mangaUrlParse(dto.slug, query.type) + url = "/${langOption.infix}/${dto.slug}" genre = dto.genres + initialized = true } } - private fun mangaUrlParse(slug: String, pathSegment: String) = "/$pathSegment/$slug" + private fun String.toDate(): Long = + try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L } companion object { const val SEARCH_PREFIX = "slug:" - val apiUrl = "https://api.unionmanga.xyz" - val apiSeed = "8e0550790c94d6abc71d738959a88d209690dc86" - val domain = "yaoi-chan.xyz" - val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) - val apiDateFormat = SimpleDateFormat("EE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.ENGLISH) - .apply { timeZone = TimeZone.getTimeZone("GMT") } + val apiUrl = "https://app.unionmanga.xyz/api" + val oldApiUrl = "https://api.unionmanga.xyz" + val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH) } } diff --git a/src/all/unionmangas/src/eu/kanade/tachiyomi/extension/all/unionmangas/UnionMangasDto.kt b/src/all/unionmangas/src/eu/kanade/tachiyomi/extension/all/unionmangas/UnionMangasDto.kt index e06326654..9a0f65e10 100644 --- a/src/all/unionmangas/src/eu/kanade/tachiyomi/extension/all/unionmangas/UnionMangasDto.kt +++ b/src/all/unionmangas/src/eu/kanade/tachiyomi/extension/all/unionmangas/UnionMangasDto.kt @@ -1,149 +1,68 @@ 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(val props: Props, val query: QueryDto) { - val data get() = props.pageProps +class MangaDetailsDto(private val data: Props) { + val details: MangaDto get() = data.details + + @Serializable + class Props( + @SerialName("infoDoc") val details: MangaDto, + ) } @Serializable -class Props(val pageProps: T) - -@Serializable -class PopularMangaProps(@SerialName("data_popular") val mangas: List) - -@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 = emptyList(), -) : Pageable() { - fun toSChapter(langOption: LanguageOption): List = - 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}" +open class Pageable( + var currentPage: Int, + var totalPage: Int, + val data: List, +) { + fun hasNextPage() = (currentPage + 1) <= totalPage } @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, -) : 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 - } - } +) { + fun toChapterUrl(lang: String) = "/$lang/${this.slugManga}/$id" } -@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("genresName") 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) -} + val thumbnailUrl get() = "${UnionMangas.oldApiUrl}$_thumbnailUrl" -@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, - @SerialName("lsstatus") private val _status: List, -) { - - val thumbnailUrl get() = "${UnionMangas.apiUrl}$_thumbnailUrl" - val genres get() = _genres.joinToString { it.name } - val status get() = toSMangaStatus(_status.firstOrNull()?.name ?: "") - - @Serializable - class Prop( - val name: String, - ) -} - -@Serializable -class ChaptersDto( - @SerialName("dataManga") val data: PageDto, - private var delimiter: String = "", -) { - val images get() = data.getImages(delimiter) -} - -@Serializable -class PageDto( - @SerialName("source") private val imgData: String, -) { - fun getImages(delimiter: String): List = imgData.split(delimiter) -} - -private fun mangaUrlParse(slug: String, pathSegment: String) = "/$pathSegment/$slug" - -private fun toSMangaStatus(status: String) = - when (status.lowercase()) { + val status get() = when (_status) { "ongoing" -> SManga.ONGOING "completed" -> SManga.COMPLETED else -> SManga.UNKNOWN } +} + +@Serializable +class SearchDto( + @SerialName("data") + val mangas: List, +) + +@Serializable +class PageDto(val `data`: Data) { + val pages: List get() = `data`.lsDetail.source.split("#") + + @Serializable + class Data(val lsDetail: LsDetail) + + @Serializable + class LsDetail(val source: String) +} diff --git a/src/all/unionmangas/src/eu/kanade/tachiyomi/extension/all/unionmangas/UnionMangasFactory.kt b/src/all/unionmangas/src/eu/kanade/tachiyomi/extension/all/unionmangas/UnionMangasFactory.kt index 0356f440f..cd2e01ba4 100644 --- a/src/all/unionmangas/src/eu/kanade/tachiyomi/extension/all/unionmangas/UnionMangasFactory.kt +++ b/src/all/unionmangas/src/eu/kanade/tachiyomi/extension/all/unionmangas/UnionMangasFactory.kt @@ -7,9 +7,9 @@ class UnionMangasFactory : SourceFactory { override fun createSources(): List = languages.map { UnionMangas(it) } } -class LanguageOption(val lang: String, val infix: String = lang, val chpPrefix: String, val pageDelimiter: String) +class LanguageOption(val lang: String, val infix: String = lang, val mangaSubstring: String = infix) val languages = listOf( - LanguageOption("it", "italy", "leer", ","), - LanguageOption("pt-BR", "manga-br", "cap", "#"), + LanguageOption("pt-BR", "manga-br"), + LanguageOption("ru", "manga-ru", "mangas"), )