From 5af32898f8234c6dc75460501e0db554943cf9e2 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 26 Apr 2020 20:22:49 +0300 Subject: [PATCH] Add MangaDex source --- .idea/inspectionProfiles/Project_Default.xml | 1 + app/build.gradle | 2 +- .../kotatsu/core/model/MangaSource.kt | 3 +- .../core/parser/site/MangaTownRepository.kt | 172 ++++++++++++++++++ .../kotatsu/core/prefs/SourceConfig.kt | 4 + .../kotatsu/ui/reader/standard/PageHolder.kt | 4 +- .../kotatsu/ui/reader/wetoon/WebtoonHolder.kt | 4 +- .../org/koitharu/kotatsu/utils/ext/FileExt.kt | 10 +- .../koitharu/kotatsu/utils/ext/ParseExt.kt | 21 +++ .../koitharu/kotatsu/utils/ext/StringExt.kt | 8 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/constants.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/pref_source.xml | 5 + 14 files changed, 228 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index e520b8638..0947cf71a 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,6 +1,7 @@ \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index ad07dec78..e8a72de1d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,7 +15,7 @@ android { minSdkVersion 21 targetSdkVersion 29 versionCode gitCommits - versionName '0.3' + versionName '0.3.1' buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\"" diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt index 78dd571ce..daca081c5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt @@ -20,5 +20,6 @@ enum class MangaSource( MANGACHAN("Манга-тян", "ru", MangaChanRepository::class.java), DESUME("Desu.me", "ru", DesuMeRepository::class.java), HENCHAN("Хентай-тян", "ru", HenChanRepository::class.java), - YAOICHAN("Яой-тян", "ru", YaoiChanRepository::class.java) + YAOICHAN("Яой-тян", "ru", YaoiChanRepository::class.java), + MANGATOWN("MangaTown", "en", MangaTownRepository::class.java) } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt new file mode 100644 index 000000000..cee43c3d6 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt @@ -0,0 +1,172 @@ +package org.koitharu.kotatsu.core.parser.site + +import org.intellij.lang.annotations.Language +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.exceptions.ParseException +import org.koitharu.kotatsu.core.model.* +import org.koitharu.kotatsu.core.parser.RemoteMangaRepository +import org.koitharu.kotatsu.utils.ext.* +import java.util.* + +class MangaTownRepository : RemoteMangaRepository() { + + override val source = MangaSource.MANGATOWN + + override val sortOrders = setOf( + SortOrder.ALPHABETICAL, + SortOrder.RATING, + SortOrder.POPULARITY, + SortOrder.UPDATED + ) + + override suspend fun getList( + offset: Int, + query: String?, + sortOrder: SortOrder?, + tag: MangaTag? + ): List { + val domain = conf.getDomain(DOMAIN) + val ssl = conf.isUseSsl(false) + val scheme = if (ssl) "https" else "http" + val sortKey = when (sortOrder) { + SortOrder.ALPHABETICAL -> "?name.az" + SortOrder.RATING -> "?rating.za" + SortOrder.UPDATED -> "?last_chapter_time.za" + else -> "" + } + val page = (offset / 30) + 1 + val url = when { + !query.isNullOrEmpty() -> "$scheme://$domain/search?name=${query.urlEncoded()}" + tag != null -> "$scheme://$domain/directory/${tag.key}/$page.htm$sortKey" + else -> "$scheme://$domain/directory/$page.htm$sortKey" + } + val doc = loaderContext.httpGet(url).parseHtml() + val root = doc.body().selectFirst("ul.manga_pic_list") + ?: throw ParseException("Root not found") + return root.select("li").mapNotNull { li -> + val a = li.selectFirst("a.manga_cover") + val href = a.attr("href").withDomain(domain, ssl) + val views = li.select("p.view") + val status = views.findOwnText { x -> x.startsWith("Status:") } + ?.substringAfter(':')?.trim()?.toLowerCase(Locale.ROOT) + Manga( + id = href.longHashCode(), + title = a.attr("title"), + coverUrl = a.selectFirst("img").attr("src"), + source = MangaSource.MANGATOWN, + altTitle = null, + rating = li.selectFirst("p.score")?.selectFirst("b") + ?.ownText()?.toFloatOrNull()?.div(5f) ?: Manga.NO_RATING, + largeCoverUrl = null, + author = views.findText { x -> x.startsWith("Author:") }?.substringAfter(':') + ?.trim(), + state = when (status) { + "ongoing" -> MangaState.ONGOING + "completed" -> MangaState.FINISHED + else -> null + }, + tags = li.selectFirst("p.keyWord")?.select("a")?.mapNotNull tags@{ x -> + MangaTag( + title = x.attr("title"), + key = x.attr("href").parseTagKey() ?: return@tags null, + source = MangaSource.MANGATOWN + ) + }?.toSet().orEmpty(), + url = href + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val domain = conf.getDomain(DOMAIN) + val ssl = conf.isUseSsl(false) + val doc = loaderContext.httpGet(manga.url).parseHtml() + val root = doc.body().selectFirst("section.main") + ?.selectFirst("div.article_content") ?: throw ParseException("Cannot find root") + val info = root.selectFirst("div.detail_info").selectFirst("ul") + val chaptersList = root.selectFirst("div.chapter_content") + ?.selectFirst("ul.chapter_list")?.select("li")?.asReversed() + return manga.copy( + tags = manga.tags + info.select("li").find { x -> + x.selectFirst("b")?.ownText() == "Genre(s):" + }?.select("a")?.mapNotNull { a -> + MangaTag( + title = a.attr("title"), + key = a.attr("href").parseTagKey() ?: return@mapNotNull null, + source = MangaSource.MANGATOWN + ) + }.orEmpty(), + description = info.getElementById("show")?.ownText(), + chapters = chaptersList?.mapIndexedNotNull { i, li -> + val href = li.selectFirst("a").attr("href").withDomain(domain, ssl) + val name = li.select("span").filter { it.className().isEmpty() }.joinToString(" - ") { it.text() }.trim() + MangaChapter( + id = href.longHashCode(), + url = href, + source = MangaSource.MANGATOWN, + number = i + 1, + name = if (name.isEmpty()) "${manga.title} - ${i + 1}" else name + ) + } + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val domain = conf.getDomain(DOMAIN) + val ssl = conf.isUseSsl(false) + val doc = loaderContext.httpGet(chapter.url).parseHtml() + val root = doc.body().selectFirst("div.page_select") + ?: throw ParseException("Cannot find root") + return root.selectFirst("select").select("option").mapNotNull { + val href = it.attr("value").withDomain(domain, ssl) + if (href.endsWith("featured.html")) { + return@mapNotNull null + } + MangaPage( + id = href.longHashCode(), + url = href, + source = MangaSource.MANGATOWN + ) + } + } + + override suspend fun getPageFullUrl(page: MangaPage): String { + val domain = conf.getDomain(DOMAIN) + val ssl = conf.isUseSsl(false) + val doc = loaderContext.httpGet(page.url).parseHtml() + return doc.getElementById("image").attr("src").withDomain(domain, ssl) + } + + override suspend fun getTags(): Set { + val domain = conf.getDomain(DOMAIN) + val doc = loaderContext.httpGet("http://$domain/directory/").parseHtml() + val root = doc.body().selectFirst("aside.right") + .getElementsContainingOwnText("Genres") + .first() + .nextElementSibling() + return root.select("li").mapNotNull { li -> + val a = li.selectFirst("a") ?: return@mapNotNull null + val key = a.attr("href").parseTagKey() + if (key.isNullOrEmpty()) { + return@mapNotNull null + } + MangaTag( + source = MangaSource.MANGATOWN, + key = key, + title = a.text() + ) + }.toSet() + } + + + override fun onCreatePreferences() = setOf(R.string.key_parser_domain, R.string.key_parser_ssl) + + private fun String.parseTagKey() = split('/').findLast { TAG_REGEX matches it } + + private companion object { + + @Language("RegExp") + val TAG_REGEX = Regex("[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+") + const val DOMAIN = "www.mangatown.com" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceConfig.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceConfig.kt index 49995eae4..30194765c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceConfig.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceConfig.kt @@ -8,16 +8,20 @@ interface SourceConfig { fun getDomain(defaultValue: String): String + fun isUseSsl(defaultValue: Boolean): Boolean + private class PrefSourceConfig(context: Context, source: MangaSource) : SourceConfig { private val prefs = context.getSharedPreferences(source.name, Context.MODE_PRIVATE) private val keyDomain = context.getString(R.string.key_parser_domain) + private val keySsl = context.getString(R.string.key_parser_ssl) override fun getDomain(defaultValue: String) = prefs.getString(keyDomain, defaultValue) ?.takeUnless(String::isBlank) ?: defaultValue + override fun isUseSsl(defaultValue: Boolean) = prefs.getBoolean(keySsl, defaultValue) } companion object { diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/PageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/PageHolder.kt index 2de37a7b3..124bfe607 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/PageHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/PageHolder.kt @@ -9,6 +9,7 @@ import kotlinx.android.synthetic.main.item_page.* import kotlinx.coroutines.* import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.MangaPage +import org.koitharu.kotatsu.domain.MangaProviderFactory import org.koitharu.kotatsu.ui.common.list.BaseViewHolder import org.koitharu.kotatsu.ui.reader.PageLoader import org.koitharu.kotatsu.utils.ext.getDisplayMessage @@ -43,7 +44,8 @@ class PageHolder(parent: ViewGroup, private val loader: PageLoader) : ssiv.recycle() try { val uri = withContext(Dispatchers.IO) { - loader.loadFile(data.url, force) + val pageUrl = MangaProviderFactory.create(data.source).getPageFullUrl(data) + loader.loadFile(pageUrl, force) }.toUri() ssiv.setImage(ImageSource.uri(uri)) } catch (e: CancellationException) { diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/WebtoonHolder.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/WebtoonHolder.kt index 4181a9797..481940d17 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/WebtoonHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/WebtoonHolder.kt @@ -10,6 +10,7 @@ import kotlinx.android.synthetic.main.item_page_webtoon.* import kotlinx.coroutines.* import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.MangaPage +import org.koitharu.kotatsu.domain.MangaProviderFactory import org.koitharu.kotatsu.ui.common.list.BaseViewHolder import org.koitharu.kotatsu.ui.reader.PageLoader import org.koitharu.kotatsu.utils.ext.getDisplayMessage @@ -42,7 +43,8 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) : ssiv.recycle() try { val uri = withContext(Dispatchers.IO) { - loader.loadFile(data.url, force) + val pageUrl = MangaProviderFactory.create(data.source).getPageFullUrl(data) + loader.loadFile(pageUrl, force) }.toUri() ssiv.setImage(ImageSource.uri(uri)) } catch (e: CancellationException) { diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt index 8c30fcca1..6b2e38276 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt @@ -33,16 +33,16 @@ inline fun File.findParent(predicate: (File) -> Boolean): File? { return current } -fun File.getStorageName(context: Context): String { +fun File.getStorageName(context: Context): String = safe { val manager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { manager.getStorageVolume(this)?.getDescription(context)?.let { - return it + return@safe it } } - return when { + when { Environment.isExternalStorageEmulated(this) -> context.getString(R.string.internal_storage) Environment.isExternalStorageRemovable(this) -> context.getString(R.string.external_storage) - else -> context.getString(R.string.other_storage) + else -> null } -} \ No newline at end of file +} ?: context.getString(R.string.other_storage) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt index ea3cfd91e..048525a0e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt @@ -5,6 +5,7 @@ import okhttp3.internal.closeQuietly import org.json.JSONObject import org.jsoup.Jsoup import org.jsoup.nodes.Document +import org.jsoup.select.Elements fun Response.parseHtml(): Document { try { @@ -27,4 +28,24 @@ fun Response.parseJson(): JSONObject { } finally { closeQuietly() } +} + +inline fun Elements.findOwnText(predicate: (String) -> Boolean): String? { + for (x in this) { + val ownText = x.ownText() + if (predicate(ownText)) { + return ownText + } + } + return null +} + +inline fun Elements.findText(predicate: (String) -> Boolean): String? { + for (x in this) { + val text = x.text() + if (predicate(text)) { + return text + } + } + return null } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt index 13fe030a9..871f3ca38 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt @@ -14,6 +14,14 @@ fun String.longHashCode(): Long { } fun String.withDomain(domain: String, ssl: Boolean = true) = when { + this.startsWith("//") -> buildString { + append("http") + if (ssl) { + append('s') + } + append(":") + append(this@withDomain) + } this.startsWith("/") -> buildString { append("http") if (ssl) { diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 1bcc315a3..51f62d15e 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -130,4 +130,5 @@ Недоступно Не удалось найти ни одного доступного хранилища Другое хранилище + Защищённое соединение (HTTPS) \ No newline at end of file diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index cb332612c..bc69568ca 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -22,6 +22,7 @@ reader_animation domain + ssl -1 1 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4913a198c..0dfb3988b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -131,4 +131,5 @@ Not available Cannot find any available storage Other storage + Use secure connection (HTTPS) \ No newline at end of file diff --git a/app/src/main/res/xml/pref_source.xml b/app/src/main/res/xml/pref_source.xml index aebbbf090..195ad5a21 100644 --- a/app/src/main/res/xml/pref_source.xml +++ b/app/src/main/res/xml/pref_source.xml @@ -7,4 +7,9 @@ android:title="@string/domain" app:iconSpaceReserved="false" /> + + \ No newline at end of file