From 2f0e88d18c32330bc225179476befdf2e1a25bec Mon Sep 17 00:00:00 2001 From: Pavka Date: Sun, 7 Jun 2020 19:08:30 +0300 Subject: [PATCH] Remanga. Fixes and login (#3422) * Remanga. Fixes and login * Remanga login fix * Fix remanga chapter name format * Fix empty scanlator * Fix image loading Co-authored-by: pavkazzz --- src/ru/remanga/build.gradle | 2 +- .../tachiyomi/extension/ru/remanga/Remanga.kt | 183 ++++++++++++++++-- .../tachiyomi/extension/ru/remanga/dto/Dto.kt | 20 +- 3 files changed, 185 insertions(+), 20 deletions(-) diff --git a/src/ru/remanga/build.gradle b/src/ru/remanga/build.gradle index b26488835..dfa482f83 100644 --- a/src/ru/remanga/build.gradle +++ b/src/ru/remanga/build.gradle @@ -5,7 +5,7 @@ ext { appName = 'Tachiyomi: Remanga' pkgNameSuffix = 'ru.remanga' extClass = '.Remanga' - extVersionCode = 2 + extVersionCode = 3 libVersion = '1.2' } diff --git a/src/ru/remanga/src/eu/kanade/tachiyomi/extension/ru/remanga/Remanga.kt b/src/ru/remanga/src/eu/kanade/tachiyomi/extension/ru/remanga/Remanga.kt index 73b51c8a3..b780d1c43 100644 --- a/src/ru/remanga/src/eu/kanade/tachiyomi/extension/ru/remanga/Remanga.kt +++ b/src/ru/remanga/src/eu/kanade/tachiyomi/extension/ru/remanga/Remanga.kt @@ -8,10 +8,19 @@ import MangaDetDto import PageDto import PageWrapperDto import SeriesWrapperDto +import UserDto +import android.app.Application +import android.content.SharedPreferences +import android.support.v7.preference.EditTextPreference +import android.support.v7.preference.PreferenceScreen +import android.text.InputType +import android.widget.Toast import com.github.salomonbrys.kotson.fromJson import com.google.gson.Gson import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage @@ -24,28 +33,75 @@ import java.util.Date import java.util.Locale import okhttp3.Headers import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.MediaType +import okhttp3.OkHttpClient import okhttp3.Request +import okhttp3.RequestBody import okhttp3.Response +import org.json.JSONObject import org.jsoup.Jsoup import rx.Observable -class Remanga : HttpSource() { +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class Remanga : ConfigurableSource, HttpSource() { override val name = "Remanga" - override val baseUrl = "https://remanga.org" + override val baseUrl = "https://api.remanga.org" override val lang = "ru" override val supportsLatest = true + var token: String = "" + override fun headersBuilder() = Headers.Builder().apply { add("User-Agent", "Tachiyomi") add("Referer", baseUrl) } + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + private fun authIntercept(chain: Interceptor.Chain): Response { + val request = chain.request() + if (username.isEmpty() or password.isEmpty()) { + return chain.proceed(request) + } + + if (token.isEmpty()) { + token = this.login(chain, username, password) + } + val authRequest = request.newBuilder() + .addHeader("Authorization", "bearer $token") + .build() + return chain.proceed(authRequest) + } + + override val client: OkHttpClient = + network.client.newBuilder() + .addInterceptor { authIntercept(it) } + .build() + private val count = 30 private var branches = mutableMapOf>() + private fun login(chain: Interceptor.Chain, username: String, password: String): String { + val jsonObject = JSONObject() + jsonObject.put("user", username) + jsonObject.put("password", password) + val body = RequestBody.create(MEDIA_TYPE, jsonObject.toString()) + val response = chain.proceed(POST("$baseUrl/api/users/login/", headers, body)) + if (response.code() == 400) { + throw Exception("Failed to login") + } + val user = gson.fromJson>(response.body()?.charStream()!!) + return user.content.access_token + } + 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) @@ -59,7 +115,7 @@ class Remanga : HttpSource() { val mangas = page.content.map { it.toSManga() } - return MangasPage(mangas, !page.last) + return MangasPage(mangas, page.props.page < page.props.total_pages) } private fun LibraryDto.toSManga(): SManga = @@ -166,17 +222,24 @@ class Remanga : HttpSource() { return series.content.branches } + private fun selector(b: BranchesDto): Int = b.count_chapters override fun fetchChapterList(manga: SManga): Observable> { val branch = branches.getOrElse(manga.title) { mangaBranches(manga) } - return if (manga.status != SManga.LICENSED) { - // Use only first branch for all cases - client.newCall(chapterListRequest(branch[0].id)) - .asObservableSuccess() - .map { response -> - chapterListParse(response) - } - } else { - Observable.error(Exception("Licensed - No chapters to show")) + return when { + branch.isEmpty() -> { + return Observable.just(listOf()) + } + manga.status == SManga.LICENSED -> { + Observable.error(Exception("Licensed - No chapters to show")) + } + else -> { + val branchId = branch.maxBy { selector(it) }!!.id + client.newCall(chapterListRequest(branchId)) + .asObservableSuccess() + .map { response -> + chapterListParse(response) + } + } } } @@ -186,23 +249,26 @@ class Remanga : HttpSource() { private fun chapterName(book: BookDto): String { val chapterId = if (book.chapter % 1 == 0f) book.chapter.toInt() else book.chapter - var chapterName = "${book.tome} - $chapterId" - if (book.name.isNotBlank() && chapterName != chapterName) { - chapterName += "- $chapterName" + var chapterName = "${book.tome}. Глава $chapterId" + if (book.name.isNotBlank()) { + chapterName += " ${book.name.capitalize()}" } return chapterName } override fun chapterListParse(response: Response): List { val chapters = gson.fromJson>(response.body()?.charStream()!!) - return chapters.content.filter { !it.is_paid }.map { chapter -> + return chapters.content.filter { !it.is_paid or it.is_bought }.map { chapter -> SChapter.create().apply { chapter_number = chapter.chapter name = chapterName(chapter) url = "/api/titles/chapters/${chapter.id}" date_upload = parseDate(chapter.upload_date) + scanlator = if (chapter.publishers.isNotEmpty()) { + chapter.publishers.joinToString { it.name } + } else null } - }.sortedByDescending { it.chapter_number } + } } override fun imageUrlParse(response: Response): String = "" @@ -213,6 +279,15 @@ class Remanga : HttpSource() { Page(it.page, "", it.link) } } + + override fun imageRequest(page: Page): Request { + val refererHeaders = Headers.Builder().apply { + add("User-Agent", "Tachiyomi") + add("Referer", "https://img.remanga.org") + }.build() + return GET(page.imageUrl!!, refererHeaders) + } + private class SearchFilter(name: String, val id: String) : Filter.TriState(name) private class CheckFilter(name: String, val id: String) : Filter.CheckBox(name) @@ -234,11 +309,13 @@ class Remanga : HttpSource() { private class OrderBy : Filter.Sort("Сортировка", arrayOf("Новизне", "Последним обновлениям", "Популярности", "Лайкам", "Просмотрам", "Мне повезет"), Selection(2, false)) + private fun getAgeList() = listOf( CheckFilter("Для всех", "0"), CheckFilter("16+", "1"), CheckFilter("18+", "2") ) + private fun getTypeList() = listOf( SearchFilter("Манга", "0"), SearchFilter("Манхва", "1"), @@ -255,6 +332,7 @@ class Remanga : HttpSource() { CheckFilter("Продолжается", "1"), CheckFilter("Заморожен", "2") ) + private fun getCategoryList() = listOf( SearchFilter("алхимия", "47"), SearchFilter("ангелы", "48"), @@ -350,6 +428,7 @@ class Remanga : HttpSource() { SearchFilter("шантаж", "99"), SearchFilter("эльфы", "46") ) + private fun getGenreList() = listOf( SearchFilter("арт", "1"), SearchFilter("бдсм", "44"), @@ -396,5 +475,75 @@ class Remanga : HttpSource() { SearchFilter("яой", "43") ) + override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) { + screen.addPreference(screen.editTextPreference(USERNAME_TITLE, USERNAME_DEFAULT, username)) + screen.addPreference(screen.editTextPreference(PASSWORD_TITLE, PASSWORD_DEFAULT, password, true)) + } + + private fun androidx.preference.PreferenceScreen.editTextPreference(title: String, default: String, value: String, isPassword: Boolean = false): androidx.preference.EditTextPreference { + return androidx.preference.EditTextPreference(context).apply { + key = title + this.title = title + summary = value + this.setDefaultValue(default) + dialogTitle = title + + if (isPassword) { + setOnBindEditTextListener { + it.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + } + } + setOnPreferenceChangeListener { _, newValue -> + try { + val res = preferences.edit().putString(title, newValue as String).commit() + Toast.makeText(context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show() + res + } catch (e: Exception) { + e.printStackTrace() + false + } + } + } + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + screen.addPreference(screen.supportEditTextPreference(USERNAME_TITLE, USERNAME_DEFAULT, username)) + screen.addPreference(screen.supportEditTextPreference(PASSWORD_TITLE, PASSWORD_DEFAULT, password)) + } + + private fun PreferenceScreen.supportEditTextPreference(title: String, default: String, value: String): EditTextPreference { + return EditTextPreference(context).apply { + key = title + this.title = title + summary = value + this.setDefaultValue(default) + dialogTitle = title + + setOnPreferenceChangeListener { _, newValue -> + try { + val res = preferences.edit().putString(title, newValue as String).commit() + Toast.makeText(context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show() + res + } catch (e: Exception) { + e.printStackTrace() + false + } + } + } + } + + private fun getPrefUsername(): String = preferences.getString(USERNAME_TITLE, USERNAME_DEFAULT)!! + private fun getPrefPassword(): String = preferences.getString(PASSWORD_TITLE, PASSWORD_DEFAULT)!! + private val gson by lazy { Gson() } + private val username by lazy { getPrefUsername() } + private val password by lazy { getPrefPassword() } + + companion object { + private val MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8") + private const val USERNAME_TITLE = "Username" + private const val USERNAME_DEFAULT = "" + private const val PASSWORD_TITLE = "Password" + private const val PASSWORD_DEFAULT = "" + } } diff --git a/src/ru/remanga/src/eu/kanade/tachiyomi/extension/ru/remanga/dto/Dto.kt b/src/ru/remanga/src/eu/kanade/tachiyomi/extension/ru/remanga/dto/Dto.kt index 3e1042f84..da5e2231f 100644 --- a/src/ru/remanga/src/eu/kanade/tachiyomi/extension/ru/remanga/dto/Dto.kt +++ b/src/ru/remanga/src/eu/kanade/tachiyomi/extension/ru/remanga/dto/Dto.kt @@ -2,9 +2,12 @@ data class GenresDto( val id: Int, val name: String ) + data class BranchesDto( - val id: Long + val id: Long, + val count_chapters: Int ) + data class ImgDto( val high: String, val mid: String, @@ -39,6 +42,7 @@ data class MangaDetDto( val branches: List, val status: StatusDto ) + data class PropsDto( val total_items: Int, val total_pages: Int, @@ -58,13 +62,20 @@ data class SeriesWrapperDto( val props: PropsDto ) +data class PublisherDto( + val name: String, + val dir: String +) + data class BookDto( val id: Long, val tome: Int, val chapter: Float, val name: String, val upload_date: String, - val is_paid: Boolean + val is_paid: Boolean, + val is_bought: Boolean, + val publishers: List ) data class PagesDto( @@ -73,6 +84,11 @@ data class PagesDto( val page: Int, val count_comments: Int ) + data class PageDto( val pages: List ) + +data class UserDto( + val access_token: String +)