@ -22,26 +22,20 @@ import java.io.InputStreamReader
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPInputStream
class MyanimelistApi ( private val client : OkHttpClient ) {
class MyanimelistApi ( private val client : OkHttpClient , interceptor : MyAnimeListInterceptor ) {
fun addLibManga ( track : Track , csrf : String ) : Observable < Track > {
private val authClient = client . newBuilder ( ) . addInterceptor ( interceptor ) . build ( )
return Observable . defer {
client . newCall ( POST ( url = getAddUrl ( ) , body = getMangaPostPayload ( track , csrf ) ) )
. asObservableSuccess ( )
. map { track }
}
}
fun updateLibManga ( track : Track , csrf : String ) : Observable < Track > {
return Observable . defer {
client . newCall ( POST ( url = getUpdateUrl ( ) , body = getMangaPostPayload ( track , csrf ) ) )
. asObservableSuccess ( )
. map { track }
}
}
fun search ( query : String ) : Observable < List < TrackSearch > > {
fun search ( query : String ) : Observable < List < TrackSearch > > {
return client . newCall ( GET ( getSearchUrl ( query ) ) )
return if ( query . startsWith ( PREFIX _MY ) ) {
val realQuery = query . removePrefix ( PREFIX _MY )
getList ( )
. flatMap { Observable . from ( it ) }
. filter { it . title . contains ( realQuery , true ) }
. toList ( )
}
else {
client . newCall ( GET ( searchUrl ( query ) ) )
. asObservable ( )
. asObservable ( )
. flatMap { response ->
. flatMap { response ->
Observable . from ( Jsoup . parse ( response . consumeBody ( ) )
Observable . from ( Jsoup . parse ( response . consumeBody ( ) )
@ -67,16 +61,83 @@ class MyanimelistApi(private val client: OkHttpClient) {
}
}
. toList ( )
. toList ( )
}
}
}
fun addLibManga ( track : Track ) : Observable < Track > {
return Observable . defer {
authClient . newCall ( POST ( url = addUrl ( ) , body = mangaPostPayload ( track ) ) )
. asObservableSuccess ( )
. map { track }
}
}
fun updateLibManga ( track : Track ) : Observable < Track > {
return Observable . defer {
authClient . newCall ( POST ( url = updateUrl ( ) , body = mangaPostPayload ( track ) ) )
. asObservableSuccess ( )
. map { track }
}
}
fun findLibManga ( track : Track ) : Observable < Track ? > {
return authClient . newCall ( GET ( url = listEntryUrl ( track . media _id ) ) )
. asObservable ( )
. map { response ->
var libTrack : Track ? = null
response . use {
if ( it . priorResponse ( ) ?. isRedirect != true ) {
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
}
}
}
libTrack
}
}
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 )
private fun getList ( csrf : String ) : Observable < List < TrackSearch > > {
return csrf
return getListUrl ( 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 {
if ( response . priorResponse ( ) ?. code ( ) != 302 ) throw Exception ( " Authentication error " )
}
}
private fun getList ( ) : Observable < List < TrackSearch > > {
return getListUrl ( )
. flatMap { url ->
. flatMap { url ->
getListXml ( url )
getListXml ( url )
}
}
. flatMap { doc ->
. flatMap { doc ->
Observable . from ( doc . select ( " manga " ) )
Observable . from ( doc . select ( " manga " ) )
}
}
. map { it ->
. map {
TrackSearch . create ( TrackManager . MYANIMELIST ) . apply {
TrackSearch . create ( TrackManager . MYANIMELIST ) . apply {
title = it . selectText ( " manga_title " ) !!
title = it . selectText ( " manga_title " ) !!
media _id = it . selectInt ( " manga_mangadb_id " )
media _id = it . selectInt ( " manga_mangadb_id " )
@ -90,87 +151,61 @@ class MyanimelistApi(private val client: OkHttpClient) {
. toList ( )
. toList ( )
}
}
private fun getList Xml( url : String ) : Observable < Document > {
private fun getList Url( ) : Observable < String > {
return client. newCall ( GET ( url ) )
return authClient. newCall ( POST ( url = exportListUrl ( ) , body = exportPostBody ( ) ) )
. asObservable ( )
. asObservable ( )
. map { response ->
. map { response ->
Jsoup . parse ( response . consumeXmlBody ( ) , " " , Parser . xmlParser ( ) )
baseUrl + Jsoup . parse ( response . consumeBody ( ) )
}
. select ( " div.goodresult " )
}
. select ( " a " )
. attr ( " href " )
fun findLibManga ( track : Track , csrf : String ) : Observable < Track ? > {
return getList ( csrf )
. map { list -> list . find { it . media _id == track . media _id } }
}
}
fun getLibManga ( track : Track , csrf : String ) : Observable < Track > {
return findLibManga ( track , csrf )
. map { it ?: throw Exception ( " Could not find manga " ) }
}
}
fun login ( username : String , password : String ) : Observable < String > {
private fun getListXml ( url : String ) : Observable < Document > {
return getSessionInfo ( )
return authClient . newCall ( GET ( url ) )
. flatMap { csrf ->
. asObservable ( )
login ( username , password , csrf )
. map { response ->
Jsoup . parse ( response . consumeXmlBody ( ) , " " , Parser . xmlParser ( ) )
}
}
}
}
private fun getSessionInfo ( ) : Observable < String > {
private fun Response . consumeBody ( ) : String ? {
return client . newCall ( GET ( getLoginUrl ( ) ) )
use {
. asObservable ( )
if ( it . code ( ) != 200 ) throw Exception ( " HTTP error ${it.code()} " )
. map { response ->
return it . body ( ) ?. string ( )
Jsoup . parse ( response . consumeBody ( ) )
. select ( " meta[name=csrf_token] " )
. attr ( " content " )
}
}
}
}
private fun login ( username : String , password : String , csrf : String ) : Observable < String > {
private fun Response . consumeXmlBody ( ) : String ? {
return client . newCall ( POST ( url = getLoginUrl ( ) , body = getLoginPostBody ( username , password , csrf ) ) )
use { res ->
. asObservable ( )
if ( res . code ( ) != 200 ) throw Exception ( " Export list error " )
. map { response ->
BufferedReader ( InputStreamReader ( GZIPInputStream ( res . body ( ) ?. source ( ) ?. inputStream ( ) ) ) ) . use { reader ->
response . use {
val sb = StringBuilder ( )
if ( response . priorResponse ( ) ?. code ( ) != 302 ) throw Exception ( " Authentication error " )
reader . forEachLine { line ->
sb . append ( line )
}
}
csrf
return sb . toString ( )
}
}
}
}
private fun getLoginPostBody ( 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 getExportPostBody ( csrf : String ) : RequestBody {
companion object {
return FormBody . Builder ( )
const val CSRF = " csrf_token "
. add ( " type " , " 2 " )
. add ( " subexport " , " Export My List " )
. add ( CSRF , csrf )
. build ( )
}
private fun getMangaPostPayload ( track : Track , csrf : String ) : RequestBody {
private const val baseUrl = " https://myanimelist.net "
val body = JSONObject ( )
private const val baseMangaUrl = " $baseUrl /manga/ "
. put ( " manga_id " , track . media _id )
private const val baseModifyListUrl = " $baseUrl /ownlist/manga "
. put ( " status " , track . status )
private const val PREFIX _MY = " my: "
. put ( " score " , track . score )
private const val TD = " td "
. put ( " num_read_chapters " , track . last _chapter _read )
. put ( CSRF , csrf )
return RequestBody . create ( MediaType . parse ( " application/json; charset=utf-8 " ) , body . toString ( ) )
private fun mangaUrl ( remoteId : Int ) = baseMangaUrl + remoteId
}
private fun getL oginUrl( ) = Uri . parse ( baseUrl ) . buildUpon ( )
private fun l oginUrl( ) = Uri . parse ( baseUrl ) . buildUpon ( )
. appendPath ( " login.php " )
. appendPath ( " login.php " )
. toString ( )
. toString ( )
private fun getS earchUrl( query : String ) : String {
private fun s earchUrl( query : String ) : String {
val col = " c[] "
val col = " c[] "
return Uri . parse ( baseUrl ) . buildUpon ( )
return Uri . parse ( baseUrl ) . buildUpon ( )
. appendPath ( " manga.php " )
. appendPath ( " manga.php " )
@ -184,82 +219,77 @@ class MyanimelistApi(private val client: OkHttpClient) {
. toString ( )
. toString ( )
}
}
private fun g etE xportListUrl( ) = Uri . parse ( baseUrl ) . buildUpon ( )
private fun exportListUrl( ) = Uri . parse ( baseUrl ) . buildUpon ( )
. appendPath ( " panel.php " )
. appendPath ( " panel.php " )
. appendQueryParameter ( " go " , " export " )
. appendQueryParameter ( " go " , " export " )
. toString ( )
. toString ( )
private fun getListUrl ( csrf : String ) : Observable < String > {
private fun updateUrl ( ) = Uri . parse ( baseModifyListUrl ) . buildUpon ( )
return client . newCall ( POST ( url = getExportListUrl ( ) , body = getExportPostBody ( csrf ) ) )
. asObservable ( )
. map { response ->
baseUrl + Jsoup . parse ( response . consumeBody ( ) )
. select ( " div.goodresult " )
. select ( " a " )
. attr ( " href " )
}
}
private fun getUpdateUrl ( ) = Uri . parse ( baseModifyListUrl ) . buildUpon ( )
. appendPath ( " edit.json " )
. appendPath ( " edit.json " )
. toString ( )
. toString ( )
private fun getA ddUrl( ) = Uri . parse ( baseModifyListUrl ) . buildUpon ( )
private fun addUrl ( ) = Uri . parse ( baseModifyListUrl ) . buildUpon ( )
. appendPath ( " add.json " )
. appendPath ( " add.json " )
. toString ( )
. toString ( )
private fun Response . consumeBody ( ) : String ? {
private fun listEntryUrl ( mediaId : Int ) = Uri . parse ( baseModifyListUrl ) . buildUpon ( )
use {
. appendPath ( mediaId . toString ( ) )
if ( it . code ( ) != 200 ) throw Exception ( " Login error " )
. appendPath ( " edit " )
return it . body ( ) ?. string ( )
. toString ( )
}
}
private fun Response . consumeXmlBody ( ) : String ? {
private fun loginPostBody ( username : String , password : String , csrf : String ) : RequestBody {
use { res ->
return FormBody . Builder ( )
if ( res . code ( ) != 200 ) throw Exception ( " Export list error " )
. add ( " user_name " , username )
BufferedReader ( InputStreamReader ( GZIPInputStream ( res . body ( ) ?. source ( ) ?. inputStream ( ) ) ) ) . use { reader ->
. add ( " password " , password )
val sb = StringBuilder ( )
. add ( " cookie " , " 1 " )
reader . forEachLine { line ->
. add ( " sublogin " , " Login " )
sb . append ( line )
. add ( " submit " , " 1 " )
}
. add ( CSRF , csrf )
return sb . toString ( )
. build ( )
}
}
}
private fun exportPostBody ( ) : RequestBody {
return FormBody . Builder ( )
. add ( " type " , " 2 " )
. add ( " subexport " , " Export My List " )
. build ( )
}
}
companion object {
private fun mangaPostPayload ( track : Track ) : RequestBody {
const val baseUrl = " https://myanimelist.net "
val body = JSONObject ( )
private const val baseMangaUrl = " $baseUrl /manga/ "
. put ( " manga_id " , track . media _id )
private const val baseModifyListUrl = " $baseUrl /ownlist/manga "
. put ( " status " , track . status )
. put ( " score " , track . score )
. put ( " num_read_chapters " , track . last _chapter _read )
fun mangaUrl ( remoteId : Int ) = baseMangaUrl + remoteId
return RequestBody . create ( MediaType . parse ( " application/json; charset=utf-8 " ) , body . toString ( ) )
}
fun Element . searchTitle ( ) = select ( " strong " ) . text ( ) !!
private fun Element . searchTitle ( ) = select ( " strong " ) . text ( ) !!
fun Element . searchTotalChapters ( ) = if ( select ( TD ) [ 4 ] . text ( ) == " - " ) 0 else select ( TD ) [ 4 ] . text ( ) . toInt ( )
private fun Element . searchTotalChapters ( ) = if ( select ( TD ) [ 4 ] . text ( ) == " - " ) 0 else select ( TD ) [ 4 ] . text ( ) . toInt ( )
fun Element . searchCoverUrl ( ) = select ( " img " )
private fun Element . searchCoverUrl ( ) = select ( " img " )
. attr ( " data-src " )
. attr ( " data-src " )
. split ( " \\ ? " ) [ 0 ]
. split ( " \\ ? " ) [ 0 ]
. replace ( " /r/50x70/ " , " / " )
. replace ( " /r/50x70/ " , " / " )
fun Element . searchMediaId ( ) = select ( " div.picSurround " )
private fun Element . searchMediaId ( ) = select ( " div.picSurround " )
. select ( " a " ) . attr ( " id " )
. select ( " a " ) . attr ( " id " )
. replace ( " sarea " , " " )
. replace ( " sarea " , " " )
. toInt ( )
. toInt ( )
fun Element . searchSummary ( ) = select ( " div.pt4 " )
private fun Element . searchSummary ( ) = select ( " div.pt4 " )
. first ( )
. first ( )
. ownText ( ) !!
. ownText ( ) !!
fun Element . searchPublishingStatus ( ) = if ( select ( TD ) . last ( ) . text ( ) == " - " ) PUBLISHING else FINISHED
private fun Element . searchPublishingStatus ( ) = if ( select ( TD ) . last ( ) . text ( ) == " - " ) " Publishing " else " Finished "
fun Element . searchPublishingType ( ) = select ( TD ) [ 2 ] . text ( ) !!
private fun Element . searchPublishingType ( ) = select ( TD ) [ 2 ] . text ( ) !!
fun Element . searchStartDate ( ) = select ( TD ) [ 6 ] . text ( ) !!
private fun Element . searchStartDate ( ) = select ( TD ) [ 6 ] . text ( ) !!
fun getStatus ( status : String ) = when ( status ) {
private fun getStatus ( status : String ) = when ( status ) {
" Reading " -> 1
" Reading " -> 1
" Completed " -> 2
" Completed " -> 2
" On-Hold " -> 3
" On-Hold " -> 3
@ -267,10 +297,5 @@ class MyanimelistApi(private val client: OkHttpClient) {
" Plan to Read " -> 6
" Plan to Read " -> 6
else -> 1
else -> 1
}
}
const val CSRF = " csrf_token "
const val TD = " td "
private const val FINISHED = " Finished "
private const val PUBLISHING = " Publishing "
}
}
}
}