diff --git a/src/en/catmanga/build.gradle b/src/en/catmanga/build.gradle index ba22b1548..926a7d204 100644 --- a/src/en/catmanga/build.gradle +++ b/src/en/catmanga/build.gradle @@ -6,7 +6,7 @@ ext { extName = 'CatManga' pkgNameSuffix = "en.catmanga" extClass = '.CatManga' - extVersionCode = 4 + extVersionCode = 5 } apply from: "$rootDir/common.gradle" diff --git a/src/en/catmanga/src/eu/kanade/tachiyomi/extension/en/catmanga/CatData.kt b/src/en/catmanga/src/eu/kanade/tachiyomi/extension/en/catmanga/CatData.kt new file mode 100644 index 000000000..f4e4e8d86 --- /dev/null +++ b/src/en/catmanga/src/eu/kanade/tachiyomi/extension/en/catmanga/CatData.kt @@ -0,0 +1,60 @@ +package eu.kanade.tachiyomi.extension.en.catmanga + +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.Serializable + +@Serializable +data class CatSeries( + val alt_titles: List, + val authors: List, + val genres: List, + val chapters: List, + val title: String, + val series_id: String, + val description: String, + val status: String, + val cover_art: CatSeriesCover, + val all_covers: List +) { + fun toSManga() = this.let { series -> + SManga.create().apply { + url = "/series/${series.series_id}" + title = series.title + thumbnail_url = series.cover_art.source + author = series.authors.joinToString(", ") + description = series.description + genre = series.genres.joinToString(", ") + status = when (series.status) { + "ongoing" -> SManga.ONGOING + "completed" -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + if (chapters.isEmpty()) { + description = "[COMING SOON] $description" + } + if (alt_titles.isNotEmpty()) { + description += "\n\nAlternative titles:\n" + alt_titles.forEach { + description += "• $it\n" + } + } + } + } +} + +@Serializable +data class CatSeriesChapter( + val title: String? = null, + val groups: List, + val number: Float, + val display_number: String? = null, + val volume: Int? = null +) + +@Serializable +data class CatSeriesCover( + val source: String, + val width: Int, + val height: Int +) diff --git a/src/en/catmanga/src/eu/kanade/tachiyomi/extension/en/catmanga/CatManga.kt b/src/en/catmanga/src/eu/kanade/tachiyomi/extension/en/catmanga/CatManga.kt index 9735e852f..9308f4057 100644 --- a/src/en/catmanga/src/eu/kanade/tachiyomi/extension/en/catmanga/CatManga.kt +++ b/src/en/catmanga/src/eu/kanade/tachiyomi/extension/en/catmanga/CatManga.kt @@ -10,128 +10,120 @@ 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.Serializable +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.jsonObject -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.Protocol +import okhttp3.Request import okhttp3.Response -import okhttp3.ResponseBody.Companion.toResponseBody import org.jsoup.nodes.Document import rx.Observable import uy.kohesive.injekt.injectLazy -import java.net.HttpURLConnection +@ExperimentalSerializationApi class CatManga : HttpSource() { private val application: Application by injectLazy() override val name = "CatManga" override val baseUrl = "https://catmanga.org" - override val supportsLatest = true + override val supportsLatest = false override val lang = "en" private val json: Json by injectLazy() - private lateinit var seriesCache: LinkedHashMap // LinkedHashMap to preserve insertion order - private lateinit var latestSeries: List - override val client = super.client.newBuilder().addInterceptor { chain -> - // An interceptor which facilitates caching the data retrieved from the homepage - when (chain.request().url) { - doNothingRequest.url -> Response.Builder().body( - "".toResponseBody("text/plain; charset=utf-8".toMediaType()) - ).code(HttpURLConnection.HTTP_NO_CONTENT).message("").protocol(Protocol.HTTP_1_0).request(chain.request()).build() - homepageRequest.url -> { - /* Homepage embeds a Json Object with information about every single series in the service */ - val response = chain.proceed(chain.request()) - val responseBody = response.peekBody(Long.MAX_VALUE).string() - val seriesList = response.asJsoup(responseBody).getDataJsonObject()["props"]!!.jsonObject["pageProps"]!!.jsonObject["series"]!! - val latests = response.asJsoup(responseBody).getDataJsonObject()["props"]!!.jsonObject["pageProps"]!!.jsonObject["latests"]!! - seriesCache = linkedMapOf( - *json.decodeFromJsonElement>(seriesList).map { it.series_id to it }.toTypedArray() - ) - latestSeries = json.decodeFromJsonElement>>(latests).map { json.decodeFromJsonElement(it[0]).series_id } - response - } - else -> chain.proceed(chain.request()) - } - }.build() + private val allSeriesRequest = GET("$baseUrl/api/series/allSeries") - private val homepageRequest = GET(baseUrl) - private val doNothingRequest = GET("https://dev.null") + override fun popularMangaRequest(page: Int) = allSeriesRequest - override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = if (this::seriesCache.isInitialized) doNothingRequest else homepageRequest - override fun popularMangaRequest(page: Int) = if (this::seriesCache.isInitialized) doNothingRequest else homepageRequest - override fun latestUpdatesRequest(page: Int) = if (this::seriesCache.isInitialized) doNothingRequest else homepageRequest - override fun chapterListRequest(manga: SManga) = homepageRequest + override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = allSeriesRequest - private fun idOf(manga: SManga) = manga.url.substringAfterLast("/") override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { return client.newCall(searchMangaRequest(page, query, filters)) .asObservableSuccess() - .map { - val manga = seriesCache.asSequence().map { it.value }.filter { - if (query.startsWith(SERIES_ID_SEARCH_PREFIX)) { - return@filter it.series_id.contains(query.removePrefix(SERIES_ID_SEARCH_PREFIX), true) + .map { response -> + val manga = json.decodeFromString>(response.body!!.string()) + .filter { + if (query.startsWith(SERIES_ID_SEARCH_PREFIX)) { + return@filter it.series_id.contains(query.removePrefix(SERIES_ID_SEARCH_PREFIX), true) + } + sequence { yieldAll(it.alt_titles); yield(it.title) } + .any { title -> title.contains(query, true) } } - sequence { yieldAll(it.alt_titles); yield(it.title) } - .any { title -> title.contains(query, true) } - }.map { it.toSManga() }.toList() - + .map { it.toSManga() } + .toList() MangasPage(manga, false) } } - override fun fetchMangaDetails(manga: SManga): Observable = client.newCall(homepageRequest) - .asObservableSuccess() - .map { seriesCache[idOf(manga)]?.toSManga() ?: manga } - - override fun fetchChapterList(manga: SManga): Observable> { + override fun fetchMangaDetails(manga: SManga): Observable { val seriesId = manga.url.substringAfter("/series/") - return client.newCall(chapterListRequest(manga)) + return client.newCall(allSeriesRequest) .asObservableSuccess() - .map { - val seriesPrefs = application.getSharedPreferences("source_${id}_time_found:$seriesId", 0) - val seriesPrefsEditor = seriesPrefs.edit() - val cl = seriesCache[idOf(manga)]!!.chapters.asReversed().map { - val title = it.title ?: "" - val groups = it.groups.joinToString(", ") - val number = it.number.content - val displayNumber = it.display_number ?: number - SChapter.create().apply { - url = "${manga.url}/$number" - chapter_number = number.toFloat() - name = "Chapter $displayNumber" + if (title.isNotBlank()) " - $title" else "" - scanlator = groups - - // Save current time when a chapter is found for the first time, and reuse it on future checks to - // prevent manga entry without any new chapter bumped to the top of "Latest chapter" list - // when the library is updated. - val currentTimeMillis = System.currentTimeMillis() - if (!seriesPrefs.contains(number)) { - seriesPrefsEditor.putLong(number, currentTimeMillis) - } - date_upload = seriesPrefs.getLong(number, currentTimeMillis) - } - } - seriesPrefsEditor.apply() - cl + .map { response -> + json.decodeFromString>(response.body!!.string()) + .find { it.series_id == seriesId } + ?.toSManga() ?: manga } } - override fun popularMangaParse(response: Response) = MangasPage(seriesCache.map { it.value.toSManga() }, false) + override fun fetchChapterList(manga: SManga): Observable> { + val seriesId = manga.url.substringAfter("/series/") + return client.newCall(allSeriesRequest) + .asObservableSuccess() + .map { response -> + val seriesPrefs = application.getSharedPreferences("source_${id}_time_found:$seriesId", 0) + val seriesPrefsEditor = seriesPrefs.edit() + val chapters = json.decodeFromString>(response.body!!.string()) + .find { it.series_id == seriesId }!! + .chapters + .asReversed() + .map { chapter -> + val title = chapter.title ?: "" + val groups = chapter.groups.joinToString(", ") + val numberUrl = chapter.number.chapterNumberToUrlPath() + val displayNumber = chapter.display_number ?: numberUrl + SChapter.create().apply { + url = "${manga.url}/$numberUrl" + chapter_number = chapter.number + scanlator = groups - override fun latestUpdatesParse(response: Response) = MangasPage( - latestSeries.map { seriesCache[it]!!.toSManga() }, - false - ) + name = if (chapter.volume != null) { + "Vol.${chapter.volume} " + } else { + "" + } + name += "Ch.$displayNumber" + if (title.isNotBlank()) { + name += " - $title" + } + + // Save current time when a chapter is found for the first time, and reuse it on future + // checks to prevent manga entry without any new chapter bumped to the top of + // "Latest chapter" list when the library is updated. + val currentTimeMillis = System.currentTimeMillis() + if (!seriesPrefs.contains(numberUrl)) { + seriesPrefsEditor.putLong(numberUrl, currentTimeMillis) + } + date_upload = seriesPrefs.getLong(numberUrl, currentTimeMillis) + } + } + seriesPrefsEditor.apply() + chapters + } + } + + override fun popularMangaParse(response: Response): MangasPage { + val mangas = json.decodeFromString>(response.body!!.string()).map { it.toSManga() } + return MangasPage(mangas, false) + } override fun pageListParse(response: Response): List { - return json.decodeFromJsonElement>(response.asJsoup().getDataJsonObject()["props"]!!.jsonObject["pageProps"]!!.jsonObject["pages"]!!).mapIndexed { index, s -> - Page(index, "", s) - } + val jsonElement = response.asJsoup() + .getDataJsonObject()["props"]!! + .jsonObject["pageProps"]!! + .jsonObject["pages"]!! + return json.decodeFromJsonElement>(jsonElement).mapIndexed { index, s -> Page(index, "", s) } } /** @@ -139,6 +131,21 @@ class CatManga : HttpSource() { */ private fun Document.getDataJsonObject() = json.parseToJsonElement(getElementById("__NEXT_DATA__").html()).jsonObject + /** + * Returns string without decimal when it is not relevant + */ + private fun Float.chapterNumberToUrlPath(): String { + return if (toInt().toFloat() == this) toInt().toString() else toString() + } + + override fun latestUpdatesRequest(page: Int): Request { + throw UnsupportedOperationException("Not used.") + } + + override fun latestUpdatesParse(response: Response): MangasPage { + throw UnsupportedOperationException("Not used.") + } + override fun mangaDetailsParse(response: Response): SManga { throw UnsupportedOperationException("Not used.") } @@ -159,28 +166,3 @@ class CatManga : HttpSource() { const val SERIES_ID_SEARCH_PREFIX = "series_id:" } } - -@Serializable -private data class JsonImage(val source: String, val width: Int, val height: Int) - -@Serializable -private data class JsonChapter(val title: String? = null, val groups: List, val number: JsonPrimitive, val display_number: String? = null, val volume: Int? = null) - -@Serializable -private data class JsonSeries(val alt_titles: List, val authors: List, val genres: List, val chapters: List, val title: String, val series_id: String, val description: String, val status: String, val cover_art: JsonImage, val all_covers: List) { - fun toSManga() = this.let { jsonSeries -> - SManga.create().apply { - url = "/series/${jsonSeries.series_id}" - title = jsonSeries.title - thumbnail_url = jsonSeries.cover_art.source - author = jsonSeries.authors.joinToString(", ") - description = jsonSeries.description - genre = jsonSeries.genres.joinToString(", ") - status = when (jsonSeries.status) { - "ongoing" -> SManga.ONGOING - "completed" -> SManga.COMPLETED - else -> SManga.UNKNOWN - } - } - } -}