diff --git a/src/en/catmanga/AndroidManifest.xml b/src/en/catmanga/AndroidManifest.xml new file mode 100644 index 000000000..308dd35c2 --- /dev/null +++ b/src/en/catmanga/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/en/catmanga/build.gradle b/src/en/catmanga/build.gradle new file mode 100644 index 000000000..8d75789f5 --- /dev/null +++ b/src/en/catmanga/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'CatManga' + pkgNameSuffix = "en.catmanga" + extClass = '.CatManga' + extVersionCode = 1 + libVersion = '1.2' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/catmanga/res/mipmap-hdpi/ic_launcher.png b/src/en/catmanga/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..4db917a88 Binary files /dev/null and b/src/en/catmanga/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/catmanga/res/mipmap-mdpi/ic_launcher.png b/src/en/catmanga/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..8637c4583 Binary files /dev/null and b/src/en/catmanga/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/catmanga/res/mipmap-xhdpi/ic_launcher.png b/src/en/catmanga/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..423723cf7 Binary files /dev/null and b/src/en/catmanga/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/catmanga/res/mipmap-xxhdpi/ic_launcher.png b/src/en/catmanga/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..f5cf760a0 Binary files /dev/null and b/src/en/catmanga/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/catmanga/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/catmanga/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..1e7256b0d Binary files /dev/null and b/src/en/catmanga/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/catmanga/res/web_hi_res_512.png b/src/en/catmanga/res/web_hi_res_512.png new file mode 100644 index 000000000..6e46ab8b8 Binary files /dev/null and b/src/en/catmanga/res/web_hi_res_512.png differ 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 new file mode 100644 index 000000000..63470a8c0 --- /dev/null +++ b/src/en/catmanga/src/eu/kanade/tachiyomi/extension/en/catmanga/CatManga.kt @@ -0,0 +1,201 @@ +package eu.kanade.tachiyomi.extension.en.catmanga + +import eu.kanade.tachiyomi.network.GET +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 eu.kanade.tachiyomi.util.asJsoup +import okhttp3.Response +import org.json.JSONArray +import org.json.JSONObject +import org.jsoup.nodes.Document +import rx.Observable + +class CatManga : HttpSource() { + + override val name = "CatManga" + override val baseUrl = "https://catmanga.org" + override val supportsLatest = true + override val lang = "en" + + override fun popularMangaRequest(page: Int) = GET(baseUrl) + + override fun latestUpdatesRequest(page: Int) = popularMangaRequest(page) + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = popularMangaRequest(page) + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return client.newCall(popularMangaRequest(page)) + .asObservableSuccess() + .map { response -> + val mangas = if (query.startsWith(SERIES_ID_SEARCH_PREFIX)) { + getFilteredSeriesList( + response.asJsoup().getDataJsonObject(), + idFilter = query.removePrefix(SERIES_ID_SEARCH_PREFIX) + ) + } else { + getFilteredSeriesList( + response.asJsoup().getDataJsonObject(), + titleFilter = query + ) + } + MangasPage(mangas, false) + } + } + + override fun popularMangaParse(response: Response): MangasPage { + val mangas = getFilteredSeriesList(response.asJsoup().getDataJsonObject()) + return MangasPage(mangas, false) + } + + override fun latestUpdatesParse(response: Response): MangasPage { + val latests = response.asJsoup().getDataJsonObject() + .getJSONObject("props") + .getJSONObject("pageProps") + .getJSONArray("latests") + val mangas = (0 until latests.length()).map { i -> + val manga = latests.getJSONArray(i).getJSONObject(0) + SManga.create().apply { + url = "/series/${manga.getString("series_id")}" + title = manga.getString("title") + thumbnail_url = manga.getJSONObject("cover_art").getString("source") + } + } + return MangasPage(mangas, false) + } + + override fun mangaDetailsParse(response: Response): SManga { + return SManga.create().apply { + val series = response.asJsoup().getDataJsonObject() + .getJSONObject("props") + .getJSONObject("pageProps") + .getJSONObject("series") + title = series.getString("title") + author = series.getJSONArray("authors").joinToString(", ") + description = series.getString("description") + genre = series.getJSONArray("genres").joinToString(", ") + status = when (series.getString("status")) { + "ongoing" -> SManga.ONGOING + "completed" -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + thumbnail_url = series.getJSONObject("cover_art").getString("source") + } + } + + override fun chapterListParse(response: Response): List { + val jsonObject = response.asJsoup().getDataJsonObject() + + val querySeries = jsonObject.getJSONObject("query").getString("series") + val seriesUrl = jsonObject.getString("page").replace("[series]", querySeries) + + val series = jsonObject.getJSONObject("props").getJSONObject("pageProps").getJSONObject("series") + val chapters = series.getJSONArray("chapters") + return (0 until chapters.length()).map { i -> + val chapter = chapters.getJSONObject(i) + val title = chapter.optString("title") + val groups = chapter.getJSONArray("groups").joinToString() + val number = chapter.getString("number") + val displayNumber = chapter.optString("display_number", number) + SChapter.create().apply { + url = "$seriesUrl/$number" + chapter_number = number.toFloat() + name = "Chapter $displayNumber" + if (title.isNotBlank()) " - $title" else "" + scanlator = groups + date_upload = System.currentTimeMillis() + } + }.reversed() + } + + override fun pageListParse(response: Response): List { + val pages = response.asJsoup().getDataJsonObject() + .getJSONObject("props") + .getJSONObject("pageProps") + .getJSONArray("pages") + return (0 until pages.length()).map { i -> Page(i, "", pages.getString(i)) } + } + + /** + * Returns json object of site data + */ + private fun Document.getDataJsonObject(): JSONObject { + return JSONObject(getElementById("__NEXT_DATA__").html()) + } + + /** + * @return filtered series from home page + * @param data json data from [getDataJsonObject] + * @param titleFilter will be used to check against title and alt_titles, null to disable filter + * @param idFilter will be used to check against id, null to disable filter, only used when [titleFilter] is unset + */ + private fun getFilteredSeriesList( + data: JSONObject, + titleFilter: String? = null, + idFilter: String? = null + ): List { + val series = data.getJSONObject("props").getJSONObject("pageProps").getJSONArray("series") + val mangas = mutableListOf() + for (i in 0 until series.length()) { + val manga = series.getJSONObject(i) + val mangaId = manga.getString("series_id") + val mangaTitle = manga.getString("title") + val mangaAltTitles = manga.getJSONArray("alt_titles") + + // Filtering + if (titleFilter != null) { + if (!(mangaTitle.contains(titleFilter, true) || mangaAltTitles.contains(titleFilter))) { + continue + } + } else if (idFilter != null) { + if (!mangaId.contains(idFilter, true)) { + continue + } + } + + mangas += SManga.create().apply { + url = "/series/$mangaId" + title = mangaTitle + thumbnail_url = manga.getJSONObject("cover_art").getString("source") + } + } + return mangas.toList() + } + + private fun JSONArray.joinToString(separator: String = ", "): String { + val stringBuilder = StringBuilder() + for (i in 0 until length()) { + if (i > 0) stringBuilder.append(separator) + val item = getString(i) + stringBuilder.append(item) + } + return stringBuilder.toString() + } + + /** + * For string objects + */ + private operator fun JSONArray.contains(other: CharSequence): Boolean { + for (i in 0 until length()) { + if (optString(i, "").contains(other, true)) { + return true + } + } + return false + } + + override fun searchMangaParse(response: Response): MangasPage { + throw UnsupportedOperationException("Not used.") + } + + override fun imageUrlParse(response: Response): String { + throw UnsupportedOperationException("Not used.") + } + + companion object { + const val SERIES_ID_SEARCH_PREFIX = "series_id:" + } +} diff --git a/src/en/catmanga/src/eu/kanade/tachiyomi/extension/en/catmanga/CatMangaUrlActivity.kt b/src/en/catmanga/src/eu/kanade/tachiyomi/extension/en/catmanga/CatMangaUrlActivity.kt new file mode 100644 index 000000000..fc2a09fe1 --- /dev/null +++ b/src/en/catmanga/src/eu/kanade/tachiyomi/extension/en/catmanga/CatMangaUrlActivity.kt @@ -0,0 +1,38 @@ +package eu.kanade.tachiyomi.extension.en.catmanga + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import kotlin.system.exitProcess + +/** + * Springboard that accepts https://catmanga.org/series/xxxxxx intents and redirects them to + * the main Tachiyomi process. + */ +class CatMangaUrlActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size > 1) { + val id = pathSegments[1] + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${CatManga.SERIES_ID_SEARCH_PREFIX}$id") + putExtra("filter", packageName) + } + + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e("CatMangaUrlActivity", e.toString()) + } + } else { + Log.e("CatMangaUrlActivity", "could not parse uri from intent $intent") + } + + finish() + exitProcess(0) + } +}