2016-12-22 21:17:47 +01:00
|
|
|
package eu.kanade.tachiyomi.data.track.myanimelist
|
|
|
|
|
|
|
|
import android.net.Uri
|
|
|
|
import eu.kanade.tachiyomi.data.database.models.Track
|
2017-01-20 21:34:15 +01:00
|
|
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
2018-02-17 13:04:49 +01:00
|
|
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
2017-01-20 21:24:31 +01:00
|
|
|
import eu.kanade.tachiyomi.network.GET
|
|
|
|
import eu.kanade.tachiyomi.network.POST
|
|
|
|
import eu.kanade.tachiyomi.network.asObservable
|
|
|
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
2020-02-03 04:22:54 +01:00
|
|
|
import eu.kanade.tachiyomi.util.system.selectInt
|
|
|
|
import eu.kanade.tachiyomi.util.system.selectText
|
2020-01-05 17:29:27 +01:00
|
|
|
import okhttp3.FormBody
|
|
|
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
|
|
import okhttp3.OkHttpClient
|
|
|
|
import okhttp3.RequestBody
|
2020-01-08 01:20:08 +01:00
|
|
|
import okhttp3.RequestBody.Companion.toRequestBody
|
2020-01-05 17:29:27 +01:00
|
|
|
import okhttp3.Response
|
2018-11-11 14:00:47 +01:00
|
|
|
import org.json.JSONObject
|
2016-12-22 21:17:47 +01:00
|
|
|
import org.jsoup.Jsoup
|
2018-11-11 14:00:47 +01:00
|
|
|
import org.jsoup.nodes.Document
|
|
|
|
import org.jsoup.nodes.Element
|
2018-02-17 13:04:49 +01:00
|
|
|
import org.jsoup.parser.Parser
|
2016-12-22 21:17:47 +01:00
|
|
|
import rx.Observable
|
2018-11-11 14:00:47 +01:00
|
|
|
import java.io.BufferedReader
|
|
|
|
import java.io.InputStreamReader
|
|
|
|
import java.util.zip.GZIPInputStream
|
2016-12-22 21:17:47 +01:00
|
|
|
|
|
|
|
|
2020-01-08 01:20:08 +01:00
|
|
|
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
|
2016-12-22 21:17:47 +01:00
|
|
|
|
2019-06-09 14:31:19 +02:00
|
|
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
|
|
|
|
|
|
|
fun search(query: String): Observable<List<TrackSearch>> {
|
|
|
|
return if (query.startsWith(PREFIX_MY)) {
|
|
|
|
val realQuery = query.removePrefix(PREFIX_MY)
|
|
|
|
getList()
|
|
|
|
.flatMap { Observable.from(it) }
|
|
|
|
.filter { it.title.contains(realQuery, true) }
|
|
|
|
.toList()
|
2020-01-08 01:20:08 +01:00
|
|
|
} else {
|
2019-06-09 14:31:19 +02:00
|
|
|
client.newCall(GET(searchUrl(query)))
|
|
|
|
.asObservable()
|
|
|
|
.flatMap { response ->
|
|
|
|
Observable.from(Jsoup.parse(response.consumeBody())
|
|
|
|
.select("div.js-categories-seasonal.js-block-list.list")
|
|
|
|
.select("table").select("tbody")
|
|
|
|
.select("tr").drop(1))
|
|
|
|
}
|
|
|
|
.filter { row ->
|
|
|
|
row.select(TD)[2].text() != "Novel"
|
|
|
|
}
|
|
|
|
.map { row ->
|
|
|
|
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
|
|
|
title = row.searchTitle()
|
|
|
|
media_id = row.searchMediaId()
|
|
|
|
total_chapters = row.searchTotalChapters()
|
|
|
|
summary = row.searchSummary()
|
|
|
|
cover_url = row.searchCoverUrl()
|
|
|
|
tracking_url = mangaUrl(media_id)
|
|
|
|
publishing_status = row.searchPublishingStatus()
|
|
|
|
publishing_type = row.searchPublishingType()
|
|
|
|
start_date = row.searchStartDate()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.toList()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun addLibManga(track: Track): Observable<Track> {
|
2016-12-22 21:17:47 +01:00
|
|
|
return Observable.defer {
|
2019-06-09 14:31:19 +02:00
|
|
|
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track)))
|
2016-12-22 21:57:15 +01:00
|
|
|
.asObservableSuccess()
|
|
|
|
.map { track }
|
2016-12-22 21:17:47 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-09 14:31:19 +02:00
|
|
|
fun updateLibManga(track: Track): Observable<Track> {
|
2016-12-22 21:17:47 +01:00
|
|
|
return Observable.defer {
|
2019-06-09 14:31:19 +02:00
|
|
|
authClient.newCall(POST(url = updateUrl(), body = mangaPostPayload(track)))
|
2016-12-22 21:57:15 +01:00
|
|
|
.asObservableSuccess()
|
|
|
|
.map { track }
|
2016-12-22 21:17:47 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-09 14:31:19 +02:00
|
|
|
fun findLibManga(track: Track): Observable<Track?> {
|
|
|
|
return authClient.newCall(GET(url = listEntryUrl(track.media_id)))
|
2018-11-11 14:00:47 +01:00
|
|
|
.asObservable()
|
2019-06-09 14:31:19 +02:00
|
|
|
.map {response ->
|
|
|
|
var libTrack: Track? = null
|
|
|
|
response.use {
|
2020-01-05 17:29:27 +01:00
|
|
|
if (it.priorResponse?.isRedirect != true) {
|
2019-06-09 14:31:19 +02:00
|
|
|
val trackForm = Jsoup.parse(it.consumeBody())
|
|
|
|
|
|
|
|
libTrack = Track.create(TrackManager.MYANIMELIST).apply {
|
|
|
|
last_chapter_read = trackForm.select("#add_manga_num_read_chapters").`val`().toInt()
|
|
|
|
total_chapters = trackForm.select("#totalChap").text().toInt()
|
|
|
|
status = trackForm.select("#add_manga_status > option[selected]").`val`().toInt()
|
|
|
|
score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull() ?: 0f
|
|
|
|
}
|
|
|
|
}
|
2016-12-22 21:17:47 +01:00
|
|
|
}
|
2019-06-09 14:31:19 +02:00
|
|
|
libTrack
|
2018-11-11 14:00:47 +01:00
|
|
|
}
|
2016-12-22 21:17:47 +01:00
|
|
|
}
|
|
|
|
|
2019-06-09 14:31:19 +02:00
|
|
|
fun getLibManga(track: Track): Observable<Track> {
|
|
|
|
return findLibManga(track)
|
|
|
|
.map { it ?: throw Exception("Could not find manga") }
|
|
|
|
}
|
|
|
|
|
|
|
|
fun login(username: String, password: String): String {
|
|
|
|
val csrf = getSessionInfo()
|
|
|
|
|
|
|
|
login(username, password, csrf)
|
|
|
|
|
|
|
|
return csrf
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun getSessionInfo(): String {
|
|
|
|
val response = client.newCall(GET(loginUrl())).execute()
|
|
|
|
|
|
|
|
return Jsoup.parse(response.consumeBody())
|
|
|
|
.select("meta[name=csrf_token]")
|
|
|
|
.attr("content")
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun login(username: String, password: String, csrf: String) {
|
|
|
|
val response = client.newCall(POST(url = loginUrl(), body = loginPostBody(username, password, csrf))).execute()
|
|
|
|
|
|
|
|
response.use {
|
2020-01-05 17:29:27 +01:00
|
|
|
if (response.priorResponse?.code != 302) throw Exception("Authentication error")
|
2019-06-09 14:31:19 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun getList(): Observable<List<TrackSearch>> {
|
|
|
|
return getListUrl()
|
2018-11-11 14:00:47 +01:00
|
|
|
.flatMap { url ->
|
|
|
|
getListXml(url)
|
|
|
|
}
|
|
|
|
.flatMap { doc ->
|
|
|
|
Observable.from(doc.select("manga"))
|
|
|
|
}
|
2019-06-09 14:31:19 +02:00
|
|
|
.map {
|
2018-02-17 13:04:49 +01:00
|
|
|
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
2018-11-11 14:00:47 +01:00
|
|
|
title = it.selectText("manga_title")!!
|
|
|
|
media_id = it.selectInt("manga_mangadb_id")
|
2016-12-22 21:17:47 +01:00
|
|
|
last_chapter_read = it.selectInt("my_read_chapters")
|
2018-11-11 14:00:47 +01:00
|
|
|
status = getStatus(it.selectText("my_status")!!)
|
2016-12-22 21:17:47 +01:00
|
|
|
score = it.selectInt("my_score").toFloat()
|
2018-11-11 14:00:47 +01:00
|
|
|
total_chapters = it.selectInt("manga_chapters")
|
|
|
|
tracking_url = mangaUrl(media_id)
|
2016-12-22 21:17:47 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
.toList()
|
|
|
|
}
|
|
|
|
|
2019-06-09 14:31:19 +02:00
|
|
|
private fun getListUrl(): Observable<String> {
|
|
|
|
return authClient.newCall(POST(url = exportListUrl(), body = exportPostBody()))
|
2018-11-11 14:00:47 +01:00
|
|
|
.asObservable()
|
|
|
|
.map {response ->
|
|
|
|
baseUrl + Jsoup.parse(response.consumeBody())
|
|
|
|
.select("div.goodresult")
|
|
|
|
.select("a")
|
|
|
|
.attr("href")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-09 14:31:19 +02:00
|
|
|
private fun getListXml(url: String): Observable<Document> {
|
|
|
|
return authClient.newCall(GET(url))
|
|
|
|
.asObservable()
|
|
|
|
.map { response ->
|
|
|
|
Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
|
|
|
|
}
|
|
|
|
}
|
2016-12-22 21:17:47 +01:00
|
|
|
|
2018-11-11 14:00:47 +01:00
|
|
|
private fun Response.consumeBody(): String? {
|
|
|
|
use {
|
2020-01-05 17:29:27 +01:00
|
|
|
if (it.code != 200) throw Exception("HTTP error ${it.code}")
|
|
|
|
return it.body?.string()
|
2018-11-11 14:00:47 +01:00
|
|
|
}
|
|
|
|
}
|
2016-12-22 21:17:47 +01:00
|
|
|
|
2018-11-11 14:00:47 +01:00
|
|
|
private fun Response.consumeXmlBody(): String? {
|
|
|
|
use { res ->
|
2020-01-05 17:29:27 +01:00
|
|
|
if (res.code != 200) throw Exception("Export list error")
|
|
|
|
BufferedReader(InputStreamReader(GZIPInputStream(res.body?.source()?.inputStream()))).use { reader ->
|
2018-11-11 14:00:47 +01:00
|
|
|
val sb = StringBuilder()
|
|
|
|
reader.forEachLine { line ->
|
|
|
|
sb.append(line)
|
|
|
|
}
|
|
|
|
return sb.toString()
|
|
|
|
}
|
|
|
|
}
|
2016-12-22 21:17:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
companion object {
|
2019-06-09 14:31:19 +02:00
|
|
|
const val CSRF = "csrf_token"
|
|
|
|
|
|
|
|
private const val baseUrl = "https://myanimelist.net"
|
2018-11-11 14:00:47 +01:00
|
|
|
private const val baseMangaUrl = "$baseUrl/manga/"
|
|
|
|
private const val baseModifyListUrl = "$baseUrl/ownlist/manga"
|
2019-06-09 14:31:19 +02:00
|
|
|
private const val PREFIX_MY = "my:"
|
|
|
|
private const val TD = "td"
|
|
|
|
|
|
|
|
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
|
|
|
|
|
|
|
|
private fun loginUrl() = Uri.parse(baseUrl).buildUpon()
|
|
|
|
.appendPath("login.php")
|
|
|
|
.toString()
|
|
|
|
|
|
|
|
private fun searchUrl(query: String): String {
|
|
|
|
val col = "c[]"
|
|
|
|
return Uri.parse(baseUrl).buildUpon()
|
|
|
|
.appendPath("manga.php")
|
|
|
|
.appendQueryParameter("q", query)
|
|
|
|
.appendQueryParameter(col, "a")
|
|
|
|
.appendQueryParameter(col, "b")
|
|
|
|
.appendQueryParameter(col, "c")
|
|
|
|
.appendQueryParameter(col, "d")
|
|
|
|
.appendQueryParameter(col, "e")
|
|
|
|
.appendQueryParameter(col, "g")
|
|
|
|
.toString()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun exportListUrl() = Uri.parse(baseUrl).buildUpon()
|
|
|
|
.appendPath("panel.php")
|
|
|
|
.appendQueryParameter("go", "export")
|
|
|
|
.toString()
|
|
|
|
|
|
|
|
private fun updateUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
|
|
|
.appendPath("edit.json")
|
|
|
|
.toString()
|
2018-02-17 13:04:49 +01:00
|
|
|
|
2019-06-09 14:31:19 +02:00
|
|
|
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
|
|
|
.appendPath( "add.json")
|
|
|
|
.toString()
|
|
|
|
|
|
|
|
private fun listEntryUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
|
|
|
|
.appendPath(mediaId.toString())
|
|
|
|
.appendPath("edit")
|
|
|
|
.toString()
|
|
|
|
|
|
|
|
private fun loginPostBody(username: String, password: String, csrf: String): RequestBody {
|
|
|
|
return FormBody.Builder()
|
|
|
|
.add("user_name", username)
|
|
|
|
.add("password", password)
|
|
|
|
.add("cookie", "1")
|
|
|
|
.add("sublogin", "Login")
|
|
|
|
.add("submit", "1")
|
|
|
|
.add(CSRF, csrf)
|
|
|
|
.build()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun exportPostBody(): RequestBody {
|
|
|
|
return FormBody.Builder()
|
|
|
|
.add("type", "2")
|
|
|
|
.add("subexport", "Export My List")
|
|
|
|
.build()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun mangaPostPayload(track: Track): RequestBody {
|
|
|
|
val body = JSONObject()
|
|
|
|
.put("manga_id", track.media_id)
|
|
|
|
.put("status", track.status)
|
|
|
|
.put("score", track.score)
|
|
|
|
.put("num_read_chapters", track.last_chapter_read)
|
|
|
|
|
2020-01-08 01:20:08 +01:00
|
|
|
return body.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
2019-06-09 14:31:19 +02:00
|
|
|
}
|
2018-11-11 14:00:47 +01:00
|
|
|
|
2019-06-09 14:31:19 +02:00
|
|
|
private fun Element.searchTitle() = select("strong").text()!!
|
2018-11-11 14:00:47 +01:00
|
|
|
|
2019-06-09 14:31:19 +02:00
|
|
|
private fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
|
2018-11-11 14:00:47 +01:00
|
|
|
|
2019-06-09 14:31:19 +02:00
|
|
|
private fun Element.searchCoverUrl() = select("img")
|
2018-11-11 14:00:47 +01:00
|
|
|
.attr("data-src")
|
|
|
|
.split("\\?")[0]
|
|
|
|
.replace("/r/50x70/", "/")
|
2016-12-22 21:17:47 +01:00
|
|
|
|
2019-06-09 14:31:19 +02:00
|
|
|
private fun Element.searchMediaId() = select("div.picSurround")
|
2018-11-11 14:00:47 +01:00
|
|
|
.select("a").attr("id")
|
|
|
|
.replace("sarea", "")
|
|
|
|
.toInt()
|
|
|
|
|
2019-06-09 14:31:19 +02:00
|
|
|
private fun Element.searchSummary() = select("div.pt4")
|
2018-11-11 14:00:47 +01:00
|
|
|
.first()
|
|
|
|
.ownText()!!
|
|
|
|
|
2019-06-09 14:31:19 +02:00
|
|
|
private fun Element.searchPublishingStatus() = if (select(TD).last().text() == "-") "Publishing" else "Finished"
|
2018-11-11 14:00:47 +01:00
|
|
|
|
2019-06-09 14:31:19 +02:00
|
|
|
private fun Element.searchPublishingType() = select(TD)[2].text()!!
|
2018-11-11 14:00:47 +01:00
|
|
|
|
2019-06-09 14:31:19 +02:00
|
|
|
private fun Element.searchStartDate() = select(TD)[6].text()!!
|
2018-11-11 14:00:47 +01:00
|
|
|
|
2019-06-09 14:31:19 +02:00
|
|
|
private fun getStatus(status: String) = when (status) {
|
2018-11-11 14:00:47 +01:00
|
|
|
"Reading" -> 1
|
|
|
|
"Completed" -> 2
|
|
|
|
"On-Hold" -> 3
|
|
|
|
"Dropped" -> 4
|
|
|
|
"Plan to Read" -> 6
|
|
|
|
else -> 1
|
|
|
|
}
|
2016-12-22 21:17:47 +01:00
|
|
|
}
|
2020-01-05 17:29:27 +01:00
|
|
|
}
|