Add MangaDex source

This commit is contained in:
Koitharu
2020-04-26 20:22:49 +03:00
parent ef7108f6c9
commit 5af32898f8
14 changed files with 228 additions and 9 deletions

View File

@@ -1,6 +1,7 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="BooleanLiteralArgument" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="TrailingComma" enabled="true" level="INFORMATION" enabled_by_default="true" />
</profile>
</component>

View File

@@ -15,7 +15,7 @@ android {
minSdkVersion 21
targetSdkVersion 29
versionCode gitCommits
versionName '0.3'
versionName '0.3.1'
buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\""

View File

@@ -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)
}

View File

@@ -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<Manga> {
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<MangaPage> {
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<MangaTag> {
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"
}
}

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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
}
}
} ?: context.getString(R.string.other_storage)

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -130,4 +130,5 @@
<string name="not_available">Недоступно</string>
<string name="cannot_find_available_storage">Не удалось найти ни одного доступного хранилища</string>
<string name="other_storage">Другое хранилище</string>
<string name="use_ssl">Защищённое соединение (HTTPS)</string>
</resources>

View File

@@ -22,6 +22,7 @@
<string name="key_reader_animation">reader_animation</string>
<string name="key_parser_domain">domain</string>
<string name="key_parser_ssl">ssl</string>
<string-array name="values_theme">
<item>-1</item>
<item>1</item>

View File

@@ -131,4 +131,5 @@
<string name="not_available">Not available</string>
<string name="cannot_find_available_storage">Cannot find any available storage</string>
<string name="other_storage">Other storage</string>
<string name="use_ssl">Use secure connection (HTTPS)</string>
</resources>

View File

@@ -7,4 +7,9 @@
android:title="@string/domain"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:key="@string/key_parser_ssl"
android:title="@string/use_ssl"
app:iconSpaceReserved="false" />
</PreferenceScreen>