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 802485e18..232719811 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 @@ -27,8 +27,9 @@ enum class MangaSource( MANGATOWN("MangaTown", "en", MangaTownRepository::class.java), MANGALIB("MangaLib", "ru", MangaLibRepository::class.java), NUDEMOON("Nude-Moon", "ru", NudeMoonRepository::class.java), - MANGAREAD("MangaRead", "en", MangareadRepository::class.java); - // HENTAILIB("HentaiLib", "ru", HentaiLibRepository::class.java) + MANGAREAD("MangaRead", "en", MangareadRepository::class.java), + REMANGA("Remanga", "ru", RemangaRepository::class.java), + HENTAILIB("HentaiLib", "ru", HentaiLibRepository::class.java); @get:Throws(NoBeanDefFoundException::class) @Deprecated("") diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt index 3886bc8b7..7dcd82813 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt @@ -19,7 +19,9 @@ val parserModule factory(named(MangaSource.HENCHAN)) { HenChanRepository(get()) } factory(named(MangaSource.YAOICHAN)) { YaoiChanRepository(get()) } factory(named(MangaSource.MANGATOWN)) { MangaTownRepository(get()) } - single(named(MangaSource.MANGALIB)) { MangaLibRepository(get()) } + factory(named(MangaSource.MANGALIB)) { MangaLibRepository(get()) } factory(named(MangaSource.NUDEMOON)) { NudeMoonRepository(get()) } factory(named(MangaSource.MANGAREAD)) { MangareadRepository(get()) } + factory(named(MangaSource.REMANGA)) { RemangaRepository(get()) } + factory(named(MangaSource.HENTAILIB)) { HentaiLibRepository(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index e3d2237c7..3bab2ce54 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.core.parser +import android.net.Uri import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaSource @@ -23,4 +24,17 @@ abstract class RemoteMangaRepository( override suspend fun getTags(): Set = emptySet() abstract fun onCreatePreferences(): Set + + 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) + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HentaiLibRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HentaiLibRepository.kt index b53fc098b..2a089cb6a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HentaiLibRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HentaiLibRepository.kt @@ -1,10 +1,12 @@ 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) { - protected override val defaultDomain = "hentailib.me" + override val defaultDomain = "hentailib.me" override val source = MangaSource.HENTAILIB -}*/ +} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt index 347b5c063..726b8deb5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt @@ -216,6 +216,7 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) : .parseJsonArray() return json.map { jo -> val url = "https://$domain/${jo.getString("slug")}" + val covers = jo.getJSONObject("covers") Manga( id = url.longHashCode(), url = url, @@ -227,7 +228,8 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) : .toFloatOrNull()?.div(5f) ?: Manga.NO_RATING, state = null, source = source, - coverUrl = "https://$domain${jo.getJSONObject("covers").getString("thumbnail")}" + coverUrl = "https://$domain${covers.getString("thumbnail")}", + largeCoverUrl = "https://$domain${covers.getString("default")}" ) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt new file mode 100644 index 000000000..563697de3 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt @@ -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 = 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 { + 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 { + val referer = "https://${conf.getDomain(DEFAULT_DOMAIN)}/" + val content = loaderContext.httpGet(chapter.url).parseJson() + .getJSONObject("content").getJSONArray("pages") + val pages = ArrayList(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 { + 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 + } +} \ 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 5d5770933..44d64c222 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 @@ -111,4 +111,17 @@ fun String.md5(): String { return BigInteger(1, md.digest(toByteArray())) .toString(16) .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) + } } \ No newline at end of file