[RU]Remanga alt free img server (#15538)

* [RU]Remanga alt free img server

* right injecting

* minFix

* fix header load

* LimitHost

* authorization breaks exManga

* separation of paid and ex manga chapters + crutch for search rus chapters

* do not disrupt the operation of the main source

* alt domain exmanga

* fix bookmarks

* callback request for update outside the library

* no need fixLink

* exremanga icon

* notify - no hide chapters

* notify long

* fix mangaID

* low ping low stress

* getChapterUrl

* notify of non-availability
This commit is contained in:
Eshlender 2023-03-06 02:44:00 +05:00 committed by GitHub
parent af1c9610ed
commit 52d39a63be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 158 additions and 45 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'Remanga' extName = 'Remanga'
pkgNameSuffix = 'ru.remanga' pkgNameSuffix = 'ru.remanga'
extClass = '.Remanga' extClass = '.Remanga'
extVersionCode = 62 extVersionCode = 63
} }
dependencies { dependencies {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.extension.ru.remanga package eu.kanade.tachiyomi.extension.ru.remanga
import android.annotation.SuppressLint
import android.annotation.TargetApi import android.annotation.TargetApi
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
@ -10,11 +9,14 @@ import androidx.preference.ListPreference
import eu.kanade.tachiyomi.extension.ru.remanga.dto.BookDto import eu.kanade.tachiyomi.extension.ru.remanga.dto.BookDto
import eu.kanade.tachiyomi.extension.ru.remanga.dto.BranchesDto import eu.kanade.tachiyomi.extension.ru.remanga.dto.BranchesDto
import eu.kanade.tachiyomi.extension.ru.remanga.dto.ChunksPageDto import eu.kanade.tachiyomi.extension.ru.remanga.dto.ChunksPageDto
import eu.kanade.tachiyomi.extension.ru.remanga.dto.ExBookDto
import eu.kanade.tachiyomi.extension.ru.remanga.dto.LibraryDto import eu.kanade.tachiyomi.extension.ru.remanga.dto.LibraryDto
import eu.kanade.tachiyomi.extension.ru.remanga.dto.MangaDetDto import eu.kanade.tachiyomi.extension.ru.remanga.dto.MangaDetDto
import eu.kanade.tachiyomi.extension.ru.remanga.dto.MyLibraryDto import eu.kanade.tachiyomi.extension.ru.remanga.dto.MyLibraryDto
import eu.kanade.tachiyomi.extension.ru.remanga.dto.PageDto import eu.kanade.tachiyomi.extension.ru.remanga.dto.PageDto
import eu.kanade.tachiyomi.extension.ru.remanga.dto.PageWrapperDto import eu.kanade.tachiyomi.extension.ru.remanga.dto.PageWrapperDto
import eu.kanade.tachiyomi.extension.ru.remanga.dto.PagesDto
import eu.kanade.tachiyomi.extension.ru.remanga.dto.SeriesExWrapperDto
import eu.kanade.tachiyomi.extension.ru.remanga.dto.SeriesWrapperDto import eu.kanade.tachiyomi.extension.ru.remanga.dto.SeriesWrapperDto
import eu.kanade.tachiyomi.extension.ru.remanga.dto.TagsDto import eu.kanade.tachiyomi.extension.ru.remanga.dto.TagsDto
import eu.kanade.tachiyomi.extension.ru.remanga.dto.UserDto import eu.kanade.tachiyomi.extension.ru.remanga.dto.UserDto
@ -71,6 +73,10 @@ class Remanga : ConfigurableSource, HttpSource() {
private val baseMirr: String = "https://api.xn--80aaig9ahr.xn--c1avg" // https://реманга.орг private val baseMirr: String = "https://api.xn--80aaig9ahr.xn--c1avg" // https://реманга.орг
private val domain: String? = preferences.getString(DOMAIN_PREF, baseOrig) private val domain: String? = preferences.getString(DOMAIN_PREF, baseOrig)
private val baseRuss: String = "https://exmanga.ru"
private val baseUkr: String = "https://ex.euromc.com.ua"
private val exManga: String = preferences.getString(exDOMAIN_PREF, baseRuss) ?: baseRuss
override val baseUrl = domain.toString() override val baseUrl = domain.toString()
override val supportsLatest = true override val supportsLatest = true
@ -82,9 +88,19 @@ class Remanga : ConfigurableSource, HttpSource() {
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/jxl,image/webp,*/*;q=0.8") .add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/jxl,image/webp,*/*;q=0.8")
.add("Referer", baseUrl.replace("api.", "")) .add("Referer", baseUrl.replace("api.", ""))
private fun exHeaders() = Headers.Builder()
.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.57")
.set("Accept", "image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
.set("Referer", baseUrl.replace("api.", ""))
.build()
private fun authIntercept(chain: Interceptor.Chain): Response { private fun authIntercept(chain: Interceptor.Chain): Response {
val request = chain.request() val request = chain.request()
// authorization breaks exManga
if (request.url.toString().contains(exManga)) {
return chain.proceed(request)
}
val cookies = client.cookieJar.loadForRequest(baseUrl.replace("api.", "").toHttpUrl()) val cookies = client.cookieJar.loadForRequest(baseUrl.replace("api.", "").toHttpUrl())
val authCookie = cookies val authCookie = cookies
.firstOrNull { cookie -> cookie.name == "user" } .firstOrNull { cookie -> cookie.name == "user" }
@ -121,6 +137,7 @@ class Remanga : ConfigurableSource, HttpSource() {
network.cloudflareClient.newBuilder() network.cloudflareClient.newBuilder()
.rateLimitHost("https://img3.reimg.org".toHttpUrl(), 2) .rateLimitHost("https://img3.reimg.org".toHttpUrl(), 2)
.rateLimitHost("https://img5.reimg.org".toHttpUrl(), 2) .rateLimitHost("https://img5.reimg.org".toHttpUrl(), 2)
.rateLimitHost(exManga.toHttpUrl(), 2)
.addInterceptor { imageContentTypeIntercept(it) } .addInterceptor { imageContentTypeIntercept(it) }
.addInterceptor { authIntercept(it) } .addInterceptor { authIntercept(it) }
.build() .build()
@ -129,6 +146,8 @@ class Remanga : ConfigurableSource, HttpSource() {
private var branches = mutableMapOf<String, List<BranchesDto>>() private var branches = mutableMapOf<String, List<BranchesDto>>()
private var mangaIDs = mutableMapOf<String, Long>()
override fun popularMangaRequest(page: Int) = GET("$baseUrl/api/search/catalog/?ordering=-rating&count=$count&page=$page", headers) override fun popularMangaRequest(page: Int) = GET("$baseUrl/api/search/catalog/?ordering=-rating&count=$count&page=$page", headers)
override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response) override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response)
@ -331,17 +350,20 @@ class Remanga : ConfigurableSource, HttpSource() {
} }
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
val series = json.decodeFromString<SeriesWrapperDto<MangaDetDto>>(response.body.string()) val series = json.decodeFromString<SeriesWrapperDto<MangaDetDto>>(response.body.string())
branches[series.content.en_name] = series.content.branches branches[series.content.dir] = series.content.branches
mangaIDs[series.content.dir] = series.content.id
return series.content.toSManga() return series.content.toSManga()
} }
private fun mangaBranches(manga: SManga): List<BranchesDto> { private fun mangaBranches(manga: SManga): List<BranchesDto> {
val responseString = client.newCall(GET(baseUrl + manga.url)).execute().body.string() val responseString = client.newCall(GET(baseUrl + manga.url)).execute().body.string()
// manga requiring login return "content" as a JsonArray instead of the JsonObject we expect // manga requiring login return "content" as a JsonArray instead of the JsonObject we expect
// callback request for update outside the library
val content = json.decodeFromString<JsonObject>(responseString)["content"] val content = json.decodeFromString<JsonObject>(responseString)["content"]
return if (content is JsonObject) { return if (content is JsonObject) {
val series = json.decodeFromJsonElement<MangaDetDto>(content) val series = json.decodeFromJsonElement<MangaDetDto>(content)
branches[series.en_name] = series.branches branches[series.dir] = series.branches
mangaIDs[series.dir] = series.id
series.branches series.branches
} else { } else {
emptyList() emptyList()
@ -350,7 +372,8 @@ class Remanga : ConfigurableSource, HttpSource() {
private fun selector(b: BranchesDto): Int = b.count_chapters private fun selector(b: BranchesDto): Int = b.count_chapters
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val branch = branches.getOrElse(manga.title) { mangaBranches(manga) } val branch = branches.getOrElse(manga.url.substringAfter("/api/titles/").substringBefore("/")) { mangaBranches(manga) }
val mangaID = mangaIDs[manga.url.substringAfter("/api/titles/").substringBefore("/")]
return when { return when {
manga.status == SManga.LICENSED && branch.isEmpty() -> { manga.status == SManga.LICENSED && branch.isEmpty() -> {
Observable.error(Exception("Лицензировано - Нет глав")) Observable.error(Exception("Лицензировано - Нет глав"))
@ -362,7 +385,7 @@ class Remanga : ConfigurableSource, HttpSource() {
val selectedBranch = branch.maxByOrNull { selector(it) }!! val selectedBranch = branch.maxByOrNull { selector(it) }!!
return (1..(selectedBranch.count_chapters / 100 + 1)).map { return (1..(selectedBranch.count_chapters / 100 + 1)).map {
val response = chapterListRequest(selectedBranch.id, it) val response = chapterListRequest(selectedBranch.id, it)
chapterListParse(response, manga) chapterListParse(response, manga, mangaID)
}.let { Observable.just(it.flatten()) } }.let { Observable.just(it.flatten()) }
} }
} }
@ -382,29 +405,22 @@ class Remanga : ConfigurableSource, HttpSource() {
this this
} }
@SuppressLint("DefaultLocale")
private fun chapterName(book: BookDto): String {
var chapterName = "${book.tome}. Глава ${book.chapter}"
if (book.is_paid and (book.is_bought != true)) {
chapterName += " \uD83D\uDCB2 "
}
if (book.name.isNotBlank()) {
chapterName += " ${book.name.capitalize()}"
}
return chapterName
}
override fun chapterListParse(response: Response) = throw UnsupportedOperationException("chapterListParse(response: Response, manga: SManga)") override fun chapterListParse(response: Response) = throw UnsupportedOperationException("chapterListParse(response: Response, manga: SManga)")
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> { private fun chapterListParse(response: Response, manga: SManga, mangaID: Long?): List<SChapter> {
var chapters = json.decodeFromString<SeriesWrapperDto<List<BookDto>>>(response.body.string()).content val chapters = json.decodeFromString<SeriesWrapperDto<List<BookDto>>>(response.body.string()).content
if (!preferences.getBoolean(PAID_PREF, false)) { val exChapters = if (preferences.getBoolean(exPAID_PREF, true)) {
chapters = chapters.filter { !it.is_paid or (it.is_bought == true) } try {
json.decodeFromString<SeriesExWrapperDto<List<ExBookDto>>>(client.newCall(GET("$exManga/chapter/history/$mangaID", exHeaders())).execute().body.string()).data
} catch (_: Exception) {
throw Exception("Домен $exManga сервиса exmanga недоступен, выберите другой в настройках расширения")
}
} else {
emptyList()
} }
return chapters.map { chapter -> var chaptersList = chapters.map { chapter ->
SChapter.create().apply { SChapter.create().apply {
chapter_number = chapter.chapter.split(".").take(2).joinToString(".").toFloat() chapter_number = chapter.chapter.split(".").take(2).joinToString(".").toFloat()
name = chapterName(chapter)
url = "/manga/${manga.url.substringAfterLast("/api/titles/")}ch${chapter.id}" url = "/manga/${manga.url.substringAfterLast("/api/titles/")}ch${chapter.id}"
date_upload = parseDate(chapter.upload_date) date_upload = parseDate(chapter.upload_date)
scanlator = if (chapter.publishers.isNotEmpty()) { scanlator = if (chapter.publishers.isNotEmpty()) {
@ -412,8 +428,34 @@ class Remanga : ConfigurableSource, HttpSource() {
} else { } else {
null null
} }
var exChID = exChapters.find { (it.id == chapter.id) }
if (preferences.getBoolean(exPAID_PREF, true)) {
if (chapter.is_paid) {
if (exChID != null) {
url = "$exManga/chapter?id=${exChID.id}"
scanlator = "exmanga"
}
}
} else {
exChID = null
}
var chapterName = "${chapter.tome}. Глава ${chapter.chapter}"
if (chapter.is_paid and (chapter.is_bought != true) and (exChID == null)) {
chapterName += " \uD83D\uDCB2 "
}
if (chapter.name.isNotBlank()) {
chapterName += " ${chapter.name.capitalize()}"
}
name = chapterName
} }
} }
if (!preferences.getBoolean(PAID_PREF, false)) {
chaptersList = chaptersList.filter { !it.name.contains("\uD83D\uDCB2") }
}
return chaptersList
} }
private fun fixLink(link: String): String { private fun fixLink(link: String): String {
@ -427,25 +469,47 @@ class Remanga : ConfigurableSource, HttpSource() {
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val body = response.body.string() val body = response.body.string()
val heightEmptyChunks = 10 val heightEmptyChunks = 10
return try { try {
val page = json.decodeFromString<SeriesWrapperDto<PageDto>>(body) val exPage = json.decodeFromString<SeriesExWrapperDto<List<List<PagesDto>>>>(body)
page.content.pages.filter { it.height > heightEmptyChunks }.map {
Page(it.page, "", fixLink(it.link))
}
} catch (e: SerializationException) {
val page = json.decodeFromString<SeriesWrapperDto<ChunksPageDto>>(body)
val result = mutableListOf<Page>() val result = mutableListOf<Page>()
page.content.pages.forEach { exPage.data.forEach {
it.filter { page -> page.height > heightEmptyChunks }.forEach { page -> it.filter { page -> page.height > heightEmptyChunks }.forEach { page ->
result.add(Page(result.size, "", fixLink(page.link))) result.add(Page(result.size, "", page.link))
} }
} }
return result return result
} catch (e: SerializationException) {
return try {
val page = json.decodeFromString<SeriesWrapperDto<PageDto>>(body)
page.content.pages.filter { it.height > heightEmptyChunks }.map {
Page(it.page, "", fixLink(it.link))
}
} catch (e: SerializationException) {
val page = json.decodeFromString<SeriesWrapperDto<ChunksPageDto>>(body)
val result = mutableListOf<Page>()
page.content.pages.forEach {
it.filter { page -> page.height > heightEmptyChunks }.forEach { page ->
result.add(Page(result.size, "", fixLink(page.link)))
}
}
return result
}
} }
} }
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
return GET(baseUrl + "/api/titles/chapters/" + chapter.url.substringAfterLast("/ch"), headers) return if (chapter.url.contains(exManga)) {
GET(chapter.url, exHeaders())
} else {
if (chapter.name.contains("\uD83D\uDCB2")) {
throw Exception("Глава платная. Если вы покупаете главу, то, пожалуйста, поделитесь с другими через браузерное расширение exmanga.")
}
GET(baseUrl + "/api/titles/chapters/" + chapter.url.substringAfterLast("/ch"), headers)
}
}
override fun getChapterUrl(chapter: SChapter): String {
return if (chapter.url.contains(exManga)) chapter.url else baseUrl.replace("api.", "") + chapter.url
} }
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!) override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
@ -479,7 +543,11 @@ class Remanga : ConfigurableSource, HttpSource() {
override fun imageRequest(page: Page): Request { override fun imageRequest(page: Page): Request {
val refererHeaders = headersBuilder().build() val refererHeaders = headersBuilder().build()
return GET(page.imageUrl!!, refererHeaders) return if (page.imageUrl!!.contains(exManga)) {
GET(page.imageUrl!!, exHeaders())
} else {
GET(page.imageUrl!!, refererHeaders)
}
} }
private class SearchFilter(name: String, val id: String) : Filter.TriState(name) private class SearchFilter(name: String, val id: String) : Filter.TriState(name)
@ -686,18 +754,18 @@ class Remanga : ConfigurableSource, HttpSource() {
private fun getMyList() = listOf( private fun getMyList() = listOf(
MyListUnit("Каталог", "-"), MyListUnit("Каталог", "-"),
MyListUnit("Читаю", "0"), MyListUnit("Читаю", "1"),
MyListUnit("Буду читать", "1"), MyListUnit("Буду читать", "2"),
MyListUnit("Прочитано", "2"), MyListUnit("Прочитано", "3"),
MyListUnit("Отложено", "4"), MyListUnit("Брошено ", "4"),
MyListUnit("Брошено ", "3"), MyListUnit("Отложено", "5"),
MyListUnit("Не интересно ", "5"), MyListUnit("Не интересно ", "6"),
) )
private var isEng: String? = preferences.getString(LANGUAGE_PREF, "eng") private var isEng: String? = preferences.getString(LANGUAGE_PREF, "eng")
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) { override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
val domainPref = ListPreference(screen.context).apply { val domainPref = ListPreference(screen.context).apply {
key = DOMAIN_PREF key = DOMAIN_PREF
title = DOMAIN_PREF_Title title = "Выбор домена"
entries = arrayOf("Основной (remanga.org)", "Зеркало (реманга.орг)") entries = arrayOf("Основной (remanga.org)", "Зеркало (реманга.орг)")
entryValues = arrayOf(baseOrig, baseMirr) entryValues = arrayOf(baseOrig, baseMirr)
summary = "%s" summary = "%s"
@ -730,7 +798,7 @@ class Remanga : ConfigurableSource, HttpSource() {
} }
val paidChapterShow = androidx.preference.CheckBoxPreference(screen.context).apply { val paidChapterShow = androidx.preference.CheckBoxPreference(screen.context).apply {
key = PAID_PREF key = PAID_PREF
title = PAID_PREF_Title title = "Показывать платные главы"
summary = "Показывает не купленные\uD83D\uDCB2 главы(может вызвать ошибки при обновлении/автозагрузке)" summary = "Показывает не купленные\uD83D\uDCB2 главы(может вызвать ошибки при обновлении/автозагрузке)"
setDefaultValue(false) setDefaultValue(false)
@ -739,6 +807,36 @@ class Remanga : ConfigurableSource, HttpSource() {
preferences.edit().putBoolean(key, checkValue).commit() preferences.edit().putBoolean(key, checkValue).commit()
} }
} }
val exChapterShow = androidx.preference.CheckBoxPreference(screen.context).apply {
key = exPAID_PREF
title = "Показывать главы из exmanga"
summary = "Показывает главы купленные другими людьми и поделившиеся ими через браузерное расширение exmanga"
setDefaultValue(true)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit().putBoolean(key, checkValue).commit()
}
}
val domainExPref = ListPreference(screen.context).apply {
key = exDOMAIN_PREF
title = "Выбор домена для exmanga"
entries = arrayOf("Россия (exmanga.ru)", "Украина (ex.euromc.com.ua)")
entryValues = arrayOf(baseRuss, baseUkr)
summary = "%s"
setDefaultValue(baseRuss)
setOnPreferenceChangeListener { _, newValue ->
try {
val res = preferences.edit().putString(exDOMAIN_PREF, newValue as String).commit()
val warning = "Для смены домена необходимо перезапустить приложение с полной остановкой."
Toast.makeText(screen.context, warning, Toast.LENGTH_LONG).show()
res
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
val bookmarksHide = androidx.preference.CheckBoxPreference(screen.context).apply { val bookmarksHide = androidx.preference.CheckBoxPreference(screen.context).apply {
key = isLib_PREF key = isLib_PREF
title = isLib_PREF_Title title = isLib_PREF_Title
@ -753,24 +851,29 @@ class Remanga : ConfigurableSource, HttpSource() {
screen.addPreference(domainPref) screen.addPreference(domainPref)
screen.addPreference(titleLanguagePref) screen.addPreference(titleLanguagePref)
screen.addPreference(paidChapterShow) screen.addPreference(paidChapterShow)
screen.addPreference(exChapterShow)
screen.addPreference(domainExPref)
screen.addPreference(bookmarksHide) screen.addPreference(bookmarksHide)
} }
private val json: Json by injectLazy() private val json: Json by injectLazy()
companion object { companion object {
private var USER_ID = "" private var USER_ID = ""
const val PREFIX_SLUG_SEARCH = "slug:" const val PREFIX_SLUG_SEARCH = "slug:"
private const val DOMAIN_PREF = "REMangaDomain" private const val DOMAIN_PREF = "REMangaDomain"
private const val DOMAIN_PREF_Title = "Выбор домена"
private const val exDOMAIN_PREF = "EXMangaDomain"
private const val LANGUAGE_PREF = "ReMangaTitleLanguage" private const val LANGUAGE_PREF = "ReMangaTitleLanguage"
private const val LANGUAGE_PREF_Title = "Выбор языка на обложке" private const val LANGUAGE_PREF_Title = "Выбор языка на обложке"
private const val PAID_PREF = "PaidChapter" private const val PAID_PREF = "PaidChapter"
private const val PAID_PREF_Title = "Показывать платные главы"
private const val exPAID_PREF = "ExChapter"
private const val isLib_PREF = "LibBookmarks" private const val isLib_PREF = "LibBookmarks"
private const val isLib_PREF_Title = "Скрыть «Закладки»" private const val isLib_PREF_Title = "Скрыть «Закладки»"

View File

@ -96,6 +96,16 @@ data class BookDto(
val publishers: List<PublisherDto>, val publishers: List<PublisherDto>,
) )
@Serializable
data class SeriesExWrapperDto<T>(
val data: T,
)
@Serializable
data class ExBookDto(
val id: Long,
)
@Serializable @Serializable
data class PagesDto( data class PagesDto(
val id: Int, val id: Int,