Add Remanga source
This commit is contained in:
@@ -27,8 +27,9 @@ enum class MangaSource(
|
|||||||
MANGATOWN("MangaTown", "en", MangaTownRepository::class.java),
|
MANGATOWN("MangaTown", "en", MangaTownRepository::class.java),
|
||||||
MANGALIB("MangaLib", "ru", MangaLibRepository::class.java),
|
MANGALIB("MangaLib", "ru", MangaLibRepository::class.java),
|
||||||
NUDEMOON("Nude-Moon", "ru", NudeMoonRepository::class.java),
|
NUDEMOON("Nude-Moon", "ru", NudeMoonRepository::class.java),
|
||||||
MANGAREAD("MangaRead", "en", MangareadRepository::class.java);
|
MANGAREAD("MangaRead", "en", MangareadRepository::class.java),
|
||||||
// HENTAILIB("HentaiLib", "ru", HentaiLibRepository::class.java)
|
REMANGA("Remanga", "ru", RemangaRepository::class.java),
|
||||||
|
HENTAILIB("HentaiLib", "ru", HentaiLibRepository::class.java);
|
||||||
|
|
||||||
@get:Throws(NoBeanDefFoundException::class)
|
@get:Throws(NoBeanDefFoundException::class)
|
||||||
@Deprecated("")
|
@Deprecated("")
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ val parserModule
|
|||||||
factory<MangaRepository>(named(MangaSource.HENCHAN)) { HenChanRepository(get()) }
|
factory<MangaRepository>(named(MangaSource.HENCHAN)) { HenChanRepository(get()) }
|
||||||
factory<MangaRepository>(named(MangaSource.YAOICHAN)) { YaoiChanRepository(get()) }
|
factory<MangaRepository>(named(MangaSource.YAOICHAN)) { YaoiChanRepository(get()) }
|
||||||
factory<MangaRepository>(named(MangaSource.MANGATOWN)) { MangaTownRepository(get()) }
|
factory<MangaRepository>(named(MangaSource.MANGATOWN)) { MangaTownRepository(get()) }
|
||||||
single<MangaRepository>(named(MangaSource.MANGALIB)) { MangaLibRepository(get()) }
|
factory<MangaRepository>(named(MangaSource.MANGALIB)) { MangaLibRepository(get()) }
|
||||||
factory<MangaRepository>(named(MangaSource.NUDEMOON)) { NudeMoonRepository(get()) }
|
factory<MangaRepository>(named(MangaSource.NUDEMOON)) { NudeMoonRepository(get()) }
|
||||||
factory<MangaRepository>(named(MangaSource.MANGAREAD)) { MangareadRepository(get()) }
|
factory<MangaRepository>(named(MangaSource.MANGAREAD)) { MangareadRepository(get()) }
|
||||||
|
factory<MangaRepository>(named(MangaSource.REMANGA)) { RemangaRepository(get()) }
|
||||||
|
factory<MangaRepository>(named(MangaSource.HENTAILIB)) { HentaiLibRepository(get()) }
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
package org.koitharu.kotatsu.core.parser
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.core.model.MangaPage
|
import org.koitharu.kotatsu.core.model.MangaPage
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
@@ -23,4 +24,17 @@ abstract class RemoteMangaRepository(
|
|||||||
override suspend fun getTags(): Set<MangaTag> = emptySet()
|
override suspend fun getTags(): Set<MangaTag> = emptySet()
|
||||||
|
|
||||||
abstract fun onCreatePreferences(): Set<String>
|
abstract fun onCreatePreferences(): Set<String>
|
||||||
|
|
||||||
|
protected fun generateUid(url: String): Long {
|
||||||
|
val uri = Uri.parse(url)
|
||||||
|
val path = uri.path ?: error("Cannot generate uid: bad uri \"$url\"")
|
||||||
|
val x = source.name.hashCode()
|
||||||
|
val y = path.hashCode()
|
||||||
|
return (x.toLong() shl 32) or (y.toLong() and 0xffffffffL)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun generateUid(id: Int): Long {
|
||||||
|
val x = source.name.hashCode()
|
||||||
|
return (x.toLong() shl 32) or (id.toLong() and 0xffffffffL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.core.parser.site
|
package org.koitharu.kotatsu.core.parser.site
|
||||||
|
|
||||||
/*
|
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
|
||||||
class HentaiLibRepository(loaderContext: MangaLoaderContext) : MangaLibRepository(loaderContext) {
|
class HentaiLibRepository(loaderContext: MangaLoaderContext) : MangaLibRepository(loaderContext) {
|
||||||
|
|
||||||
protected override val defaultDomain = "hentailib.me"
|
override val defaultDomain = "hentailib.me"
|
||||||
|
|
||||||
override val source = MangaSource.HENTAILIB
|
override val source = MangaSource.HENTAILIB
|
||||||
|
|
||||||
}*/
|
}
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
|
|||||||
.parseJsonArray()
|
.parseJsonArray()
|
||||||
return json.map { jo ->
|
return json.map { jo ->
|
||||||
val url = "https://$domain/${jo.getString("slug")}"
|
val url = "https://$domain/${jo.getString("slug")}"
|
||||||
|
val covers = jo.getJSONObject("covers")
|
||||||
Manga(
|
Manga(
|
||||||
id = url.longHashCode(),
|
id = url.longHashCode(),
|
||||||
url = url,
|
url = url,
|
||||||
@@ -227,7 +228,8 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
|
|||||||
.toFloatOrNull()?.div(5f) ?: Manga.NO_RATING,
|
.toFloatOrNull()?.div(5f) ?: Manga.NO_RATING,
|
||||||
state = null,
|
state = null,
|
||||||
source = source,
|
source = source,
|
||||||
coverUrl = "https://$domain${jo.getJSONObject("covers").getString("thumbnail")}"
|
coverUrl = "https://$domain${covers.getString("thumbnail")}",
|
||||||
|
largeCoverUrl = "https://$domain${covers.getString("default")}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
package org.koitharu.kotatsu.core.parser.site
|
||||||
|
|
||||||
|
import androidx.collection.arraySetOf
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.core.exceptions.ParseException
|
||||||
|
import org.koitharu.kotatsu.core.model.*
|
||||||
|
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||||
|
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||||
|
import org.koitharu.kotatsu.utils.ext.*
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) {
|
||||||
|
|
||||||
|
override val source = MangaSource.REMANGA
|
||||||
|
|
||||||
|
override val sortOrders: Set<SortOrder> = EnumSet.of(
|
||||||
|
SortOrder.POPULARITY,
|
||||||
|
SortOrder.RATING,
|
||||||
|
SortOrder.ALPHABETICAL,
|
||||||
|
SortOrder.UPDATED,
|
||||||
|
SortOrder.NEWEST
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getList(
|
||||||
|
offset: Int,
|
||||||
|
query: String?,
|
||||||
|
sortOrder: SortOrder?,
|
||||||
|
tag: MangaTag?
|
||||||
|
): List<Manga> {
|
||||||
|
val domain = conf.getDomain(DEFAULT_DOMAIN)
|
||||||
|
val urlBuilder = StringBuilder()
|
||||||
|
.append("https://api.")
|
||||||
|
.append(domain)
|
||||||
|
if (query != null) {
|
||||||
|
urlBuilder.append("/api/search/?query=")
|
||||||
|
.append(query.urlEncoded())
|
||||||
|
} else {
|
||||||
|
urlBuilder.append("/api/search/catalog/?page=")
|
||||||
|
.append("&ordering=")
|
||||||
|
.append(getSortKey(sortOrder))
|
||||||
|
if (tag != null) {
|
||||||
|
urlBuilder.append("&genres=" + tag.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
urlBuilder.append((offset / PAGE_SIZE) + 1)
|
||||||
|
.append("&count=")
|
||||||
|
.append(PAGE_SIZE)
|
||||||
|
val content = loaderContext.httpGet(urlBuilder.toString()).parseJson()
|
||||||
|
.getJSONArray("content")
|
||||||
|
return content.map { jo ->
|
||||||
|
val url = "https://$domain/manga/${jo.getString("dir")}"
|
||||||
|
val img = jo.getJSONObject("img")
|
||||||
|
Manga(
|
||||||
|
id = generateUid(url),
|
||||||
|
url = url,
|
||||||
|
title = jo.getString("rus_name"),
|
||||||
|
altTitle = jo.getString("en_name"),
|
||||||
|
rating = jo.getString("avg_rating").toFloatOrNull()?.div(10f) ?: Manga.NO_RATING,
|
||||||
|
coverUrl = "https://api.$domain${img.getString("mid")}",
|
||||||
|
largeCoverUrl = "https://api.$domain${img.getString("high")}",
|
||||||
|
author = null,
|
||||||
|
tags = jo.optJSONArray("genres")?.mapToSet { g ->
|
||||||
|
MangaTag(
|
||||||
|
title = g.getString("name"),
|
||||||
|
key = g.getInt("id").toString(),
|
||||||
|
source = MangaSource.REMANGA
|
||||||
|
)
|
||||||
|
}.orEmpty(),
|
||||||
|
source = MangaSource.REMANGA
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
|
val domain = conf.getDomain(DEFAULT_DOMAIN)
|
||||||
|
val slug = manga.url.toHttpUrl().pathSegments.last()
|
||||||
|
val data = loaderContext.httpGet(
|
||||||
|
url = "https://api.$domain/api/titles/$slug/"
|
||||||
|
).parseJson()
|
||||||
|
val content = try {
|
||||||
|
data.getJSONObject("content")
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
throw ParseException(data.optString("msg"), e)
|
||||||
|
}
|
||||||
|
val branchId = content.getJSONArray("branches").optJSONObject(0)
|
||||||
|
?.getLong("id") ?: throw ParseException("No branches found")
|
||||||
|
val chapters = loaderContext.httpGet(
|
||||||
|
url = "https://api.$domain/api/titles/chapters/?branch_id=$branchId"
|
||||||
|
).parseJson().getJSONArray("content")
|
||||||
|
return manga.copy(
|
||||||
|
description = content.getString("description"),
|
||||||
|
state = when (content.optJSONObject("status")?.getInt("id")) {
|
||||||
|
STATUS_ONGOING -> MangaState.ONGOING
|
||||||
|
STATUS_FINISHED -> MangaState.FINISHED
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
|
tags = content.getJSONArray("genres").mapToSet { g ->
|
||||||
|
MangaTag(
|
||||||
|
title = g.getString("name"),
|
||||||
|
key = g.getInt("id").toString(),
|
||||||
|
source = MangaSource.REMANGA
|
||||||
|
)
|
||||||
|
},
|
||||||
|
chapters = chapters.mapIndexed { i, jo ->
|
||||||
|
val id = jo.getLong("id")
|
||||||
|
val name = jo.getString("name")
|
||||||
|
MangaChapter(
|
||||||
|
id = generateUid(id.toInt()),
|
||||||
|
url = "https://api.$domain/api/titles/chapters/$id/",
|
||||||
|
number = chapters.length() - i,
|
||||||
|
name = buildString {
|
||||||
|
append("Глава ")
|
||||||
|
append(jo.getString("chapter"))
|
||||||
|
if (name.isNotEmpty()) {
|
||||||
|
append(" - ")
|
||||||
|
append(name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
source = MangaSource.REMANGA
|
||||||
|
)
|
||||||
|
}.asReversed()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val referer = "https://${conf.getDomain(DEFAULT_DOMAIN)}/"
|
||||||
|
val content = loaderContext.httpGet(chapter.url).parseJson()
|
||||||
|
.getJSONObject("content").getJSONArray("pages")
|
||||||
|
val pages = ArrayList<MangaPage>(content.length())
|
||||||
|
for (i in 0 until content.length()) {
|
||||||
|
when (val item = content.get(i)) {
|
||||||
|
is JSONObject -> pages += parsePage(item, referer)
|
||||||
|
is JSONArray -> item.mapTo(pages) { parsePage(it, referer) }
|
||||||
|
else -> throw ParseException("Unknown json item $item")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getTags(): Set<MangaTag> {
|
||||||
|
val domain = conf.getDomain(DEFAULT_DOMAIN)
|
||||||
|
val content = loaderContext.httpGet("https://api.$domain/api/forms/titles/?get=genres")
|
||||||
|
.parseJson().getJSONObject("content").getJSONArray("genres")
|
||||||
|
return content.mapToSet { jo ->
|
||||||
|
MangaTag(
|
||||||
|
title = jo.getString("name"),
|
||||||
|
key = jo.getInt("id").toString(),
|
||||||
|
source = source
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreatePreferences() = arraySetOf(SourceSettings.KEY_DOMAIN)
|
||||||
|
|
||||||
|
private fun getSortKey(order: SortOrder?) = when (order) {
|
||||||
|
SortOrder.UPDATED -> "-chapter_date"
|
||||||
|
SortOrder.POPULARITY -> "-rating"
|
||||||
|
SortOrder.RATING -> "-votes"
|
||||||
|
SortOrder.NEWEST -> "-id"
|
||||||
|
else -> "-rating"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parsePage(jo: JSONObject, referer: String) = MangaPage(
|
||||||
|
id = generateUid(jo.getLong("id").toInt()),
|
||||||
|
url = jo.getString("link"),
|
||||||
|
referer = referer,
|
||||||
|
source = source
|
||||||
|
)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
const val PAGE_SIZE = 30
|
||||||
|
const val DEFAULT_DOMAIN = "remanga.org"
|
||||||
|
|
||||||
|
const val STATUS_ONGOING = 1
|
||||||
|
const val STATUS_FINISHED = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -111,4 +111,17 @@ fun String.md5(): String {
|
|||||||
return BigInteger(1, md.digest(toByteArray()))
|
return BigInteger(1, md.digest(toByteArray()))
|
||||||
.toString(16)
|
.toString(16)
|
||||||
.padStart(32, '0')
|
.padStart(32, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.substringBetween(from: String, to: String, fallbackValue: String): String {
|
||||||
|
val fromIndex = indexOf(from)
|
||||||
|
if (fromIndex == -1) {
|
||||||
|
return fallbackValue
|
||||||
|
}
|
||||||
|
val toIndex = lastIndexOf(to)
|
||||||
|
return if (toIndex == -1) {
|
||||||
|
fallbackValue
|
||||||
|
} else {
|
||||||
|
substring(fromIndex + from.length, toIndex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user