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 <me@pavkazzz.ru>
This commit is contained in:
parent
e69c49cf97
commit
2f0e88d18c
|
@ -5,7 +5,7 @@ ext {
|
||||||
appName = 'Tachiyomi: Remanga'
|
appName = 'Tachiyomi: Remanga'
|
||||||
pkgNameSuffix = 'ru.remanga'
|
pkgNameSuffix = 'ru.remanga'
|
||||||
extClass = '.Remanga'
|
extClass = '.Remanga'
|
||||||
extVersionCode = 2
|
extVersionCode = 3
|
||||||
libVersion = '1.2'
|
libVersion = '1.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,19 @@ import MangaDetDto
|
||||||
import PageDto
|
import PageDto
|
||||||
import PageWrapperDto
|
import PageWrapperDto
|
||||||
import SeriesWrapperDto
|
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.github.salomonbrys.kotson.fromJson
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
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.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
@ -24,28 +33,75 @@ import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import org.json.JSONObject
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import rx.Observable
|
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 name = "Remanga"
|
||||||
|
|
||||||
override val baseUrl = "https://remanga.org"
|
override val baseUrl = "https://api.remanga.org"
|
||||||
|
|
||||||
override val lang = "ru"
|
override val lang = "ru"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
var token: String = ""
|
||||||
|
|
||||||
override fun headersBuilder() = Headers.Builder().apply {
|
override fun headersBuilder() = Headers.Builder().apply {
|
||||||
add("User-Agent", "Tachiyomi")
|
add("User-Agent", "Tachiyomi")
|
||||||
add("Referer", baseUrl)
|
add("Referer", baseUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().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 val count = 30
|
||||||
|
|
||||||
private var branches = mutableMapOf<String, List<BranchesDto>>()
|
private var branches = mutableMapOf<String, List<BranchesDto>>()
|
||||||
|
|
||||||
|
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<SeriesWrapperDto<UserDto>>(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 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)
|
||||||
|
@ -59,7 +115,7 @@ class Remanga : HttpSource() {
|
||||||
val mangas = page.content.map {
|
val mangas = page.content.map {
|
||||||
it.toSManga()
|
it.toSManga()
|
||||||
}
|
}
|
||||||
return MangasPage(mangas, !page.last)
|
return MangasPage(mangas, page.props.page < page.props.total_pages)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun LibraryDto.toSManga(): SManga =
|
private fun LibraryDto.toSManga(): SManga =
|
||||||
|
@ -166,17 +222,24 @@ class Remanga : HttpSource() {
|
||||||
return series.content.branches
|
return series.content.branches
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.title) { mangaBranches(manga) }
|
||||||
return if (manga.status != SManga.LICENSED) {
|
return when {
|
||||||
// Use only first branch for all cases
|
branch.isEmpty() -> {
|
||||||
client.newCall(chapterListRequest(branch[0].id))
|
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()
|
.asObservableSuccess()
|
||||||
.map { response ->
|
.map { response ->
|
||||||
chapterListParse(response)
|
chapterListParse(response)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
Observable.error(Exception("Licensed - No chapters to show"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,23 +249,26 @@ class Remanga : HttpSource() {
|
||||||
|
|
||||||
private fun chapterName(book: BookDto): String {
|
private fun chapterName(book: BookDto): String {
|
||||||
val chapterId = if (book.chapter % 1 == 0f) book.chapter.toInt() else book.chapter
|
val chapterId = if (book.chapter % 1 == 0f) book.chapter.toInt() else book.chapter
|
||||||
var chapterName = "${book.tome} - $chapterId"
|
var chapterName = "${book.tome}. Глава $chapterId"
|
||||||
if (book.name.isNotBlank() && chapterName != chapterName) {
|
if (book.name.isNotBlank()) {
|
||||||
chapterName += "- $chapterName"
|
chapterName += " ${book.name.capitalize()}"
|
||||||
}
|
}
|
||||||
return chapterName
|
return chapterName
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val chapters = gson.fromJson<PageWrapperDto<BookDto>>(response.body()?.charStream()!!)
|
val chapters = gson.fromJson<PageWrapperDto<BookDto>>(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 {
|
SChapter.create().apply {
|
||||||
chapter_number = chapter.chapter
|
chapter_number = chapter.chapter
|
||||||
name = chapterName(chapter)
|
name = chapterName(chapter)
|
||||||
url = "/api/titles/chapters/${chapter.id}"
|
url = "/api/titles/chapters/${chapter.id}"
|
||||||
date_upload = parseDate(chapter.upload_date)
|
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 = ""
|
override fun imageUrlParse(response: Response): String = ""
|
||||||
|
@ -213,6 +279,15 @@ class Remanga : HttpSource() {
|
||||||
Page(it.page, "", it.link)
|
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 SearchFilter(name: String, val id: String) : Filter.TriState(name)
|
||||||
private class CheckFilter(name: String, val id: String) : Filter.CheckBox(name)
|
private class CheckFilter(name: String, val id: String) : Filter.CheckBox(name)
|
||||||
|
|
||||||
|
@ -234,11 +309,13 @@ class Remanga : HttpSource() {
|
||||||
private class OrderBy : Filter.Sort("Сортировка",
|
private class OrderBy : Filter.Sort("Сортировка",
|
||||||
arrayOf("Новизне", "Последним обновлениям", "Популярности", "Лайкам", "Просмотрам", "Мне повезет"),
|
arrayOf("Новизне", "Последним обновлениям", "Популярности", "Лайкам", "Просмотрам", "Мне повезет"),
|
||||||
Selection(2, false))
|
Selection(2, false))
|
||||||
|
|
||||||
private fun getAgeList() = listOf(
|
private fun getAgeList() = listOf(
|
||||||
CheckFilter("Для всех", "0"),
|
CheckFilter("Для всех", "0"),
|
||||||
CheckFilter("16+", "1"),
|
CheckFilter("16+", "1"),
|
||||||
CheckFilter("18+", "2")
|
CheckFilter("18+", "2")
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun getTypeList() = listOf(
|
private fun getTypeList() = listOf(
|
||||||
SearchFilter("Манга", "0"),
|
SearchFilter("Манга", "0"),
|
||||||
SearchFilter("Манхва", "1"),
|
SearchFilter("Манхва", "1"),
|
||||||
|
@ -255,6 +332,7 @@ class Remanga : HttpSource() {
|
||||||
CheckFilter("Продолжается", "1"),
|
CheckFilter("Продолжается", "1"),
|
||||||
CheckFilter("Заморожен", "2")
|
CheckFilter("Заморожен", "2")
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun getCategoryList() = listOf(
|
private fun getCategoryList() = listOf(
|
||||||
SearchFilter("алхимия", "47"),
|
SearchFilter("алхимия", "47"),
|
||||||
SearchFilter("ангелы", "48"),
|
SearchFilter("ангелы", "48"),
|
||||||
|
@ -350,6 +428,7 @@ class Remanga : HttpSource() {
|
||||||
SearchFilter("шантаж", "99"),
|
SearchFilter("шантаж", "99"),
|
||||||
SearchFilter("эльфы", "46")
|
SearchFilter("эльфы", "46")
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun getGenreList() = listOf(
|
private fun getGenreList() = listOf(
|
||||||
SearchFilter("арт", "1"),
|
SearchFilter("арт", "1"),
|
||||||
SearchFilter("бдсм", "44"),
|
SearchFilter("бдсм", "44"),
|
||||||
|
@ -396,5 +475,75 @@ class Remanga : HttpSource() {
|
||||||
SearchFilter("яой", "43")
|
SearchFilter("яой", "43")
|
||||||
)
|
)
|
||||||
|
|
||||||
private val gson by lazy { Gson() }
|
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 = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,12 @@ data class GenresDto(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val name: String
|
val name: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class BranchesDto(
|
data class BranchesDto(
|
||||||
val id: Long
|
val id: Long,
|
||||||
|
val count_chapters: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ImgDto(
|
data class ImgDto(
|
||||||
val high: String,
|
val high: String,
|
||||||
val mid: String,
|
val mid: String,
|
||||||
|
@ -39,6 +42,7 @@ data class MangaDetDto(
|
||||||
val branches: List<BranchesDto>,
|
val branches: List<BranchesDto>,
|
||||||
val status: StatusDto
|
val status: StatusDto
|
||||||
)
|
)
|
||||||
|
|
||||||
data class PropsDto(
|
data class PropsDto(
|
||||||
val total_items: Int,
|
val total_items: Int,
|
||||||
val total_pages: Int,
|
val total_pages: Int,
|
||||||
|
@ -58,13 +62,20 @@ data class SeriesWrapperDto<T>(
|
||||||
val props: PropsDto
|
val props: PropsDto
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class PublisherDto(
|
||||||
|
val name: String,
|
||||||
|
val dir: String
|
||||||
|
)
|
||||||
|
|
||||||
data class BookDto(
|
data class BookDto(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val tome: Int,
|
val tome: Int,
|
||||||
val chapter: Float,
|
val chapter: Float,
|
||||||
val name: String,
|
val name: String,
|
||||||
val upload_date: String,
|
val upload_date: String,
|
||||||
val is_paid: Boolean
|
val is_paid: Boolean,
|
||||||
|
val is_bought: Boolean,
|
||||||
|
val publishers: List<PublisherDto>
|
||||||
)
|
)
|
||||||
|
|
||||||
data class PagesDto(
|
data class PagesDto(
|
||||||
|
@ -73,6 +84,11 @@ data class PagesDto(
|
||||||
val page: Int,
|
val page: Int,
|
||||||
val count_comments: Int
|
val count_comments: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
data class PageDto(
|
data class PageDto(
|
||||||
val pages: List<PagesDto>
|
val pages: List<PagesDto>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class UserDto(
|
||||||
|
val access_token: String
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue