Add MangaDex source
This commit is contained in:
1
.idea/inspectionProfiles/Project_Default.xml
generated
1
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -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>
|
||||
@@ -15,7 +15,7 @@ android {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode gitCommits
|
||||
versionName '0.3'
|
||||
versionName '0.3.1'
|
||||
|
||||
buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\""
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user