More tracker clean up

This commit is contained in:
Carlos 2020-03-14 20:00:16 -04:00
parent f83a6bd489
commit 7bc12c04c4
21 changed files with 626 additions and 592 deletions

View File

@ -181,7 +181,7 @@ class BackupRestoreService : Service() {
*/ */
private suspend fun restoreBackup(uri: Uri) { private suspend fun restoreBackup(uri: Uri) {
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader()) val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
val json = JsonParser().parse(reader).asJsonObject val json = JsonParser.parseReader(reader).asJsonObject
// Get parser version // Get parser version
val version = json.get(VERSION)?.asInt ?: 1 val version = json.get(VERSION)?.asInt ?: 1
@ -296,15 +296,15 @@ class BackupRestoreService : Service() {
* @param manga manga that needs updating. * @param manga manga that needs updating.
* @param tracks list containing tracks from restore file. * @param tracks list containing tracks from restore file.
*/ */
private fun trackingFetch(manga: Manga, tracks: List<Track>) { private suspend fun trackingFetch(manga: Manga, tracks: List<Track>) {
tracks.forEach { track -> tracks.forEach { track ->
val service = trackManager.getService(track.sync_id) val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) { if (service != null && service.isLogged) {
try {
service.refresh(track) service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() } db.insertTrack(track).executeAsBlocking()
.onErrorReturn { }catch (e : Exception){
errors.add("${manga.title} - ${it.message}") errors.add("${manga.title} - ${e.message}")
track
} }
} else { } else {
errors.add("${manga.title} - ${service?.name} not logged in") errors.add("${manga.title} - ${service?.name} not logged in")

View File

@ -81,7 +81,6 @@ class LibraryUpdateService(
*/ */
private var subscription: Subscription? = null private var subscription: Subscription? = null
/** /**
* Pending intent of action that cancels the library update * Pending intent of action that cancels the library update
*/ */
@ -108,14 +107,19 @@ class LibraryUpdateService(
/** /**
* Cached progress notification to avoid creating a lot. * Cached progress notification to avoid creating a lot.
*/ */
private val progressNotification by lazy { NotificationCompat.Builder(this, Notifications.CHANNEL_LIBRARY) private val progressNotification by lazy {
NotificationCompat.Builder(this, Notifications.CHANNEL_LIBRARY)
.setContentTitle(getString(R.string.app_name)) .setContentTitle(getString(R.string.app_name))
.setSmallIcon(R.drawable.ic_refresh_white_24dp_img) .setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
.setLargeIcon(notificationBitmap) .setLargeIcon(notificationBitmap)
.setOngoing(true) .setOngoing(true)
.setOnlyAlertOnce(true) .setOnlyAlertOnce(true)
.setColor(ContextCompat.getColor(this, R.color.colorAccent)) .setColor(ContextCompat.getColor(this, R.color.colorAccent))
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent) .addAction(
R.drawable.ic_clear_grey_24dp_img,
getString(android.R.string.cancel),
cancelIntent
)
} }
/** /**
@ -172,8 +176,7 @@ class LibraryUpdateService(
} else { } else {
context.startForegroundService(intent) context.startForegroundService(intent)
} }
} } else {
else {
if (target == Target.CHAPTERS) category?.id?.let { if (target == Target.CHAPTERS) category?.id?.let {
instance?.addCategory(it) instance?.addCategory(it)
} }
@ -212,7 +215,8 @@ class LibraryUpdateService(
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault() val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
val mangas = val mangas =
getMangaToUpdate(categoryId, Target.CHAPTERS).sortedWith( getMangaToUpdate(categoryId, Target.CHAPTERS).sortedWith(
rankingScheme[selectedScheme]) rankingScheme[selectedScheme]
)
categoryIds.add(categoryId) categoryIds.add(categoryId)
addManga(mangas) addManga(mangas)
} }
@ -228,9 +232,9 @@ class LibraryUpdateService(
var listToUpdate = if (categoryId != -1) { var listToUpdate = if (categoryId != -1) {
categoryIds.add(categoryId) categoryIds.add(categoryId)
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId } db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
} } else {
else { val categoriesToUpdate =
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map(String::toInt) preferences.libraryUpdateCategories().getOrDefault().map(String::toInt)
categoryIds.addAll(categoriesToUpdate) categoryIds.addAll(categoriesToUpdate)
if (categoriesToUpdate.isNotEmpty()) if (categoriesToUpdate.isNotEmpty())
db.getLibraryMangas().executeAsBlocking() db.getLibraryMangas().executeAsBlocking()
@ -259,7 +263,8 @@ class LibraryUpdateService(
super.onCreate() super.onCreate()
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build()) startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build())
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock") PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock"
)
wakeLock.acquire(TimeUnit.MINUTES.toMillis(30)) wakeLock.acquire(TimeUnit.MINUTES.toMillis(30))
} }
@ -307,41 +312,44 @@ class LibraryUpdateService(
val mangaList = val mangaList =
getMangaToUpdate(intent, target).sortedWith(rankingScheme[selectedScheme]) getMangaToUpdate(intent, target).sortedWith(rankingScheme[selectedScheme])
// Update favorite manga. Destroy service when completed or in case of an error. // Update favorite manga. Destroy service when completed or in case of an error.
if (target == Target.CHAPTERS) { if (target == Target.DETAILS) {
updateChapters(mangaList, startId)
}
else {
// Update either chapter list or manga details. // Update either chapter list or manga details.
subscription = Observable.defer { subscription = Observable.defer {
when (target) { updateDetails(mangaList)
Target.DETAILS -> updateDetails(mangaList)
else -> updateTrackings(mangaList)
}
}.subscribeOn(Schedulers.io()).subscribe({}, { }.subscribeOn(Schedulers.io()).subscribe({}, {
Timber.e(it) Timber.e(it)
stopSelf(startId) stopSelf(startId)
}, { }, {
stopSelf(startId) stopSelf(startId)
}) })
} else {
launchTarget(target, mangaList, startId)
} }
return START_REDELIVER_INTENT return START_REDELIVER_INTENT
} }
private fun updateChapters(mangaToAdd: List<LibraryManga>, startId: Int) { private fun launchTarget(target: Target, mangaToAdd: List<LibraryManga>, startId: Int) {
val handler = CoroutineExceptionHandler { _, exception -> val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception) Timber.e(exception)
// Boolean to determine if user wants to automatically download new chapters.
stopSelf(startId) stopSelf(startId)
} }
if (target == Target.CHAPTERS) {
job = GlobalScope.launch(handler) { job = GlobalScope.launch(handler) {
updateChaptersJob(mangaToAdd) updateChaptersJob(mangaToAdd)
} }
} else {
job = GlobalScope.launch(handler) {
updateTrackings(mangaToAdd)
}
}
job?.invokeOnCompletion { stopSelf(startId) } job?.invokeOnCompletion { stopSelf(startId) }
} }
private suspend fun updateChaptersJob(mangaToAdd: List<LibraryManga>) { private suspend fun updateChaptersJob(mangaToAdd: List<LibraryManga>) {
// List containing categories that get included in downloads. // List containing categories that get included in downloads.
val categoriesToDownload = preferences.downloadNewCategories().getOrDefault().map(String::toInt) val categoriesToDownload =
preferences.downloadNewCategories().getOrDefault().map(String::toInt)
// Boolean to determine if user wants to automatically download new chapters. // Boolean to determine if user wants to automatically download new chapters.
val downloadNew = preferences.downloadNew().getOrDefault() val downloadNew = preferences.downloadNew().getOrDefault()
// Boolean to determine if DownloadManager has downloads // Boolean to determine if DownloadManager has downloads
@ -370,8 +378,7 @@ class LibraryUpdateService(
} }
} }
.subscribeOn(Schedulers.io()).subscribe {} .subscribeOn(Schedulers.io()).subscribe {}
} } else if (downloadNew && hasDownloads) {
else if (downloadNew && hasDownloads) {
DownloadService.start(this) DownloadService.start(this)
} }
} }
@ -379,7 +386,11 @@ class LibraryUpdateService(
cancelProgressNotification() cancelProgressNotification()
} }
private suspend fun updateMangaChapters(manga: LibraryManga, progess: Int, shouldDownload: Boolean): private suspend fun updateMangaChapters(
manga: LibraryManga,
progess: Int,
shouldDownload: Boolean
):
Boolean { Boolean {
try { try {
var hasDownloads = false var hasDownloads = false
@ -406,8 +417,7 @@ class LibraryUpdateService(
) )
} }
return hasDownloads return hasDownloads
} } catch (e: Exception) {
catch (e: Exception) {
Timber.e("Failed updating: ${manga.title}: $e") Timber.e("Failed updating: ${manga.title}: $e")
return false return false
} }
@ -475,37 +485,32 @@ class LibraryUpdateService(
* Method that updates the metadata of the connected tracking services. It's called in a * Method that updates the metadata of the connected tracking services. It's called in a
* background thread, so it's safe to do heavy operations or network calls here. * background thread, so it's safe to do heavy operations or network calls here.
*/ */
private fun updateTrackings(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
private suspend fun updateTrackings(mangaToUpdate: List<LibraryManga>) {
// Initialize the variables holding the progress of the updates. // Initialize the variables holding the progress of the updates.
var count = 0 var count = 0
val loggedServices = trackManager.services.filter { it.isLogged } val loggedServices = trackManager.services.filter { it.isLogged }
// Emit each manga and update it sequentially. mangaToUpdate.forEach { manga ->
return Observable.from(mangaToUpdate) showProgressNotification(manga, count++, mangaToUpdate.size)
// Notify manga that will update.
.doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) }
// Update the tracking details.
.concatMap { manga ->
val tracks = db.getTracks(manga).executeAsBlocking() val tracks = db.getTracks(manga).executeAsBlocking()
Observable.from(tracks) tracks.forEach { track ->
.concatMap { track ->
val service = trackManager.getService(track.sync_id) val service = trackManager.getService(track.sync_id)
if (service != null && service in loggedServices) { if (service != null && service in loggedServices) {
try {
service.refresh(track) service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() } db.insertTrack(track).executeAsBlocking()
.onErrorReturn { track } } catch (e: Exception) {
} else { Timber.e(e)
Observable.empty() }
} }
} }
.map { manga }
} }
.doOnCompleted {
cancelProgressNotification() cancelProgressNotification()
} }
}
/** /**
* Shows the notification containing the currently updating manga and the progress. * Shows the notification containing the currently updating manga and the progress.
@ -515,10 +520,12 @@ class LibraryUpdateService(
* @param total the total progress. * @param total the total progress.
*/ */
private fun showProgressNotification(manga: Manga, current: Int, total: Int) { private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification notificationManager.notify(
Notifications.ID_LIBRARY_PROGRESS, progressNotification
.setContentTitle(manga.currentTitle()) .setContentTitle(manga.currentTitle())
.setProgress(total, current, false) .setProgress(total, current, false)
.build()) .build()
)
} }
/** /**
@ -539,15 +546,17 @@ class LibraryUpdateService(
.asBitmap().load(manga).dontTransform().centerCrop().circleCrop() .asBitmap().load(manga).dontTransform().centerCrop().circleCrop()
.override(256, 256).submit().get() .override(256, 256).submit().get()
setLargeIcon(icon) setLargeIcon(icon)
} catch (e: Exception) {
} }
catch (e: Exception) { }
setGroupAlertBehavior(GROUP_ALERT_SUMMARY) setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
setContentTitle(manga.currentTitle()) setContentTitle(manga.currentTitle())
color = ContextCompat.getColor(this@LibraryUpdateService, R.color.colorAccent) color = ContextCompat.getColor(this@LibraryUpdateService, R.color.colorAccent)
val chaptersNames = if (chapterNames.size > 5) { val chaptersNames = if (chapterNames.size > 5) {
"${chapterNames.take(4).joinToString(", ")}, " + "${chapterNames.take(4).joinToString(", ")}, " +
resources.getQuantityString(R.plurals.notification_and_n_more, resources.getQuantityString(
(chapterNames.size - 4), (chapterNames.size - 4)) R.plurals.notification_and_n_more,
(chapterNames.size - 4), (chapterNames.size - 4)
)
} else chapterNames.joinToString(", ") } else chapterNames.joinToString(", ")
setContentText(chaptersNames) setContentText(chaptersNames)
setStyle(NotificationCompat.BigTextStyle().bigText(chaptersNames)) setStyle(NotificationCompat.BigTextStyle().bigText(chaptersNames))
@ -558,32 +567,48 @@ class LibraryUpdateService(
this@LibraryUpdateService, manga, chapters.first() this@LibraryUpdateService, manga, chapters.first()
) )
) )
addAction(R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read), addAction(
NotificationReceiver.markAsReadPendingBroadcast(this@LibraryUpdateService, R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read),
manga, chapters, Notifications.ID_NEW_CHAPTERS)) NotificationReceiver.markAsReadPendingBroadcast(
addAction(R.drawable.ic_book_white_24dp, getString(R.string.action_view_chapters), this@LibraryUpdateService,
NotificationReceiver.openChapterPendingActivity(this@LibraryUpdateService, manga, chapters, Notifications.ID_NEW_CHAPTERS
manga, Notifications.ID_NEW_CHAPTERS)) )
)
addAction(
R.drawable.ic_book_white_24dp, getString(R.string.action_view_chapters),
NotificationReceiver.openChapterPendingActivity(
this@LibraryUpdateService,
manga, Notifications.ID_NEW_CHAPTERS
)
)
setAutoCancel(true) setAutoCancel(true)
}, manga.id.hashCode())) }, manga.id.hashCode()))
} }
NotificationManagerCompat.from(this).apply { NotificationManagerCompat.from(this).apply {
notify(Notifications.ID_NEW_CHAPTERS, notification(Notifications.CHANNEL_NEW_CHAPTERS) { notify(
Notifications.ID_NEW_CHAPTERS,
notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setSmallIcon(R.drawable.ic_tachi) setSmallIcon(R.drawable.ic_tachi)
setLargeIcon(notificationBitmap) setLargeIcon(notificationBitmap)
setContentTitle(getString(R.string.notification_new_chapters)) setContentTitle(getString(R.string.notification_new_chapters))
color = ContextCompat.getColor(applicationContext, R.color.colorAccent) color = ContextCompat.getColor(applicationContext, R.color.colorAccent)
if (updates.size > 1) { if (updates.size > 1) {
setContentText(resources.getQuantityString(R.plurals setContentText(
resources.getQuantityString(
R.plurals
.notification_new_chapters_text, .notification_new_chapters_text,
updates.size, updates.size)) updates.size, updates.size
setStyle(NotificationCompat.BigTextStyle().bigText(updates.keys.joinToString("\n") { )
)
setStyle(
NotificationCompat.BigTextStyle()
.bigText(updates.keys.joinToString("\n") {
it.currentTitle().chop(45) it.currentTitle().chop(45)
})) })
} )
else { } else {
setContentText(updates.keys.first().currentTitle().chop(45)) setContentText(updates.keys.first().currentTitle().chop(45))
} }
priority = NotificationCompat.PRIORITY_HIGH priority = NotificationCompat.PRIORITY_HIGH

View File

@ -37,8 +37,6 @@ abstract class TrackService(val id: Int) {
abstract fun displayScore(track: Track): String abstract fun displayScore(track: Track): String
abstract suspend fun add(track: Track): Track
abstract suspend fun update(track: Track): Track abstract suspend fun update(track: Track): Track
abstract suspend fun bind(track: Track): Track abstract suspend fun bind(track: Track): Track

View File

@ -8,28 +8,11 @@ import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class Anilist(private val context: Context, id: Int) : TrackService(id) { class Anilist(private val context: Context, id: Int) : TrackService(id) {
companion object {
const val READING = 1
const val COMPLETED = 2
const val PAUSED = 3
const val DROPPED = 4
const val PLANNING = 5
const val REPEATING = 6
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
const val POINT_100 = "POINT_100"
const val POINT_10 = "POINT_10"
const val POINT_10_DECIMAL = "POINT_10_DECIMAL"
const val POINT_5 = "POINT_5"
const val POINT_3 = "POINT_3"
}
override val name = "AniList" override val name = "AniList"
private val gson: Gson by injectLazy() private val gson: Gson by injectLazy()
@ -54,9 +37,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
override fun getLogoColor() = Color.rgb(18, 25, 35) override fun getLogoColor() = Color.rgb(18, 25, 35)
override fun getStatusList(): List<Int> { override fun getStatusList() = listOf(READING, PLANNING, COMPLETED, REPEATING, PAUSED, DROPPED)
return listOf(READING, PLANNING, COMPLETED, REPEATING, PAUSED, DROPPED)
}
override fun getStatus(status: Int): String = with(context) { override fun getStatus(status: Int): String = with(context) {
when (status) { when (status) {
@ -93,13 +74,13 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
// 100 point // 100 point
POINT_100 -> index.toFloat() POINT_100 -> index.toFloat()
// 5 stars // 5 stars
POINT_5 -> when { POINT_5 -> when (index) {
index == 0 -> 0f 0 -> 0f
else -> index * 20f - 10f else -> index * 20f - 10f
} }
// Smiley // Smiley
POINT_3 -> when { POINT_3 -> when (index) {
index == 0 -> 0f 0 -> 0f
else -> index * 25f + 10f else -> index * 25f + 10f
} }
// 10 point decimal // 10 point decimal
@ -112,8 +93,8 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
val score = track.score val score = track.score
return when (scorePreference.getOrDefault()) { return when (scorePreference.getOrDefault()) {
POINT_5 -> when { POINT_5 -> when (score) {
score == 0f -> "0 ★" 0f -> "0 ★"
else -> "${((score + 10) / 20).toInt()}" else -> "${((score + 10) / 20).toInt()}"
} }
POINT_3 -> when { POINT_3 -> when {
@ -126,10 +107,6 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
} }
} }
override suspend fun add(track: Track): Track {
return api.addLibManga(track)
}
override suspend fun update(track: Track): Track { override suspend fun update(track: Track): Track {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = COMPLETED track.status = COMPLETED
@ -137,34 +114,30 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
// If user was using API v1 fetch library_id // If user was using API v1 fetch library_id
if (track.library_id == null || track.library_id!! == 0L) { if (track.library_id == null || track.library_id!! == 0L) {
val libManga = api.findLibManga(track, getUsername().toInt()) val libManga = api.findLibManga(track, getUsername().toInt())
?: throw Exception("$track not found on user library")
if (libManga == null) {
throw Exception("$track not found on user library")
}
track.library_id = libManga.library_id track.library_id = libManga.library_id
} }
return api.updateLibManga(track) return api.updateLibraryManga(track)
} }
override suspend fun bind(track: Track): Track { override suspend fun bind(track: Track): Track {
val remoteTrack = api.findLibManga(track, getUsername().toInt()) val remoteTrack = api.findLibManga(track, getUsername().toInt())
if (remoteTrack != null) { return if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id track.library_id = remoteTrack.library_id
return update(track) update(track)
} else { } else {
// Set default fields if it's not found in the list // Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat() track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS track.status = DEFAULT_STATUS
return add(track) api.addLibManga(track)
} }
} }
override suspend fun search(query: String): List<TrackSearch> { override suspend fun search(query: String) = api.search(query)
return api.search(query)
}
override suspend fun refresh(track: Track): Track { override suspend fun refresh(track: Track): Track {
val remoteTrack = api.getLibManga(track, getUsername().toInt()) val remoteTrack = api.getLibManga(track, getUsername().toInt())
@ -180,14 +153,15 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
val oauth = api.createOAuth(token) val oauth = api.createOAuth(token)
interceptor.setAuth(oauth) interceptor.setAuth(oauth)
try { return try {
val currentUser = api.getCurrentUser() val currentUser = api.getCurrentUser()
scorePreference.set(currentUser.second) scorePreference.set(currentUser.second)
saveCredentials(currentUser.first.toString(), oauth.access_token) saveCredentials(currentUser.first.toString(), oauth.access_token)
return true true
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e)
logout() logout()
return false false
} }
} }
@ -205,9 +179,29 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
return try { return try {
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java) gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e)
null null
} }
} }
companion object {
const val READING = 1
const val COMPLETED = 2
const val PAUSED = 3
const val DROPPED = 4
const val PLANNING = 5
const val REPEATING = 6
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
const val POINT_100 = "POINT_100"
const val POINT_10 = "POINT_10"
const val POINT_10_DECIMAL = "POINT_10_DECIMAL"
const val POINT_5 = "POINT_5"
const val POINT_3 = "POINT_3"
}
} }

View File

@ -11,64 +11,51 @@ import com.google.gson.JsonObject
import com.google.gson.JsonParser import com.google.gson.JsonParser
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.jsonType
import okhttp3.MediaType.Companion.toMediaTypeOrNull import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import java.util.Calendar import java.util.Calendar
import java.util.concurrent.TimeUnit
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull()
private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
suspend fun addLibManga(track: Track): Track { suspend fun addLibManga(track: Track): Track {
val query = """ return withContext(Dispatchers.IO) {
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
| id
| status
|}
|}
|""".trimMargin()
val variables = jsonObject( val variables = jsonObject(
"mangaId" to track.media_id, "mangaId" to track.media_id,
"progress" to track.last_chapter_read, "progress" to track.last_chapter_read,
"status" to track.toAnilistStatus() "status" to track.toAnilistStatus()
) )
val payload = jsonObject( val payload = jsonObject(
"query" to query, "query" to addToLibraryQuery(),
"variables" to variables "variables" to variables
) )
val body = payload.toString().toRequestBody(jsonMime) val body = payload.toString().toRequestBody(MediaType.jsonType())
val request = Request.Builder() val request = Request.Builder().url(apiUrl).post(body).build()
.url(apiUrl)
.post(body) val netResponse = authClient.newCall(request).execute()
.build()
val netResponse = authClient.newCall(request).await()
val responseBody = netResponse.body?.string().orEmpty() val responseBody = netResponse.body?.string().orEmpty()
netResponse.close() netResponse.close()
if (responseBody.isEmpty()) { if (responseBody.isEmpty()) {
throw Exception("Null Response") throw Exception("Null Response")
} }
val response = JsonParser().parse(responseBody).obj val response = JsonParser.parseString(responseBody).obj
track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
track
return track }
} }
suspend fun updateLibManga(track: Track): Track { suspend fun updateLibraryManga(track: Track): Track {
val query = """ return withContext(Dispatchers.IO) {
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|id
|status
|progress
|}
|}
|""".trimMargin()
val variables = jsonObject( val variables = jsonObject(
"listId" to track.library_id, "listId" to track.library_id,
"progress" to track.last_chapter_read, "progress" to track.last_chapter_read,
@ -76,169 +63,104 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
"score" to track.score.toInt() "score" to track.score.toInt()
) )
val payload = jsonObject( val payload = jsonObject(
"query" to query, "query" to updateInLibraryQuery(),
"variables" to variables "variables" to variables
) )
val body = payload.toString().toRequestBody(jsonMime) val body = payload.toString().toRequestBody(MediaType.jsonType())
val request = Request.Builder() val request = Request.Builder().url(apiUrl).post(body).build()
.url(apiUrl) val response = authClient.newCall(request).execute()
.post(body)
.build() track
authClient.newCall(request).execute() }
return track
} }
suspend fun search(search: String): List<TrackSearch> { suspend fun search(search: String): List<TrackSearch> {
val query = """ return withContext(Dispatchers.IO) {
|query Search(${'$'}query: String) {
|Page (perPage: 50) {
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|id
|title {
|romaji
|}
|coverImage {
|large
|}
|type
|status
|chapters
|description
|startDate {
|year
|month
|day
|}
|}
|}
|}
|""".trimMargin()
val variables = jsonObject( val variables = jsonObject(
"query" to search "query" to search
) )
val payload = jsonObject( val payload = jsonObject(
"query" to query, "query" to searchQuery(),
"variables" to variables "variables" to variables
) )
val body = payload.toString().toRequestBody(jsonMime) val body = payload.toString().toRequestBody(MediaType.jsonType())
val request = Request.Builder() val request = Request.Builder().url(apiUrl).post(body).build()
.url(apiUrl) val netResponse = authClient.newCall(request).execute()
.post(body) val response = responseToJson(netResponse)
.build()
val netResponse = authClient.newCall(request).await() val media = response["data"]!!.obj["Page"].obj["mediaList"].array
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
val response = JsonParser().parse(responseBody).obj
val data = response["data"]!!.obj
val page = data["Page"].obj
val media = page["media"].array
val entries = media.map { jsonToALManga(it.obj) } val entries = media.map { jsonToALManga(it.obj) }
return entries.map { it.toTrack() } entries.map { it.toTrack() }
}
} }
suspend fun findLibManga(track: Track, userid: Int): Track? { suspend fun findLibManga(track: Track, userid: Int): Track? {
val query = """
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) { return withContext(Dispatchers.IO) {
|Page {
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|id
|status
|scoreRaw: score(format: POINT_100)
|progress
|media {
|id
|title {
|romaji
|}
|coverImage {
|large
|}
|type
|status
|chapters
|description
|startDate {
|year
|month
|day
|}
|}
|}
|}
|}
|""".trimMargin()
val variables = jsonObject( val variables = jsonObject(
"id" to userid, "id" to userid,
"manga_id" to track.media_id "manga_id" to track.media_id
) )
val payload = jsonObject( val payload = jsonObject(
"query" to query, "query" to findLibraryMangaQuery(),
"variables" to variables "variables" to variables
) )
val body = payload.toString().toRequestBody(jsonMime) val body = payload.toString().toRequestBody(MediaType.jsonType())
val request = Request.Builder() val request = Request.Builder().url(apiUrl).post(body).build()
.url(apiUrl) val result = authClient.newCall(request).execute()
.post(body)
.build() result.let { resp ->
val result = authClient.newCall(request).await() val response = responseToJson(resp)
return result.let { resp -> val media = response["data"]!!.obj["Page"].obj["mediaList"].array
val responseBody = resp.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
val response = JsonParser().parse(responseBody).obj
val data = response["data"]!!.obj
val page = data["Page"].obj
val media = page["mediaList"].array
val entries = media.map { jsonToALUserManga(it.obj) } val entries = media.map { jsonToALUserManga(it.obj) }
entries.firstOrNull()?.toTrack() entries.firstOrNull()?.toTrack()
} }
} }
}
suspend fun getLibManga(track: Track, userid: Int): Track { suspend fun getLibManga(track: Track, userid: Int): Track {
val track = findLibManga(track, userid) val remoteTrack = findLibManga(track, userid)
if (track == null) { if (remoteTrack == null) {
throw Exception("Could not find manga") throw Exception("Could not find manga")
} else { } else {
return track return remoteTrack
} }
} }
fun createOAuth(token: String): OAuth { fun createOAuth(token: String): OAuth {
return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000) return OAuth(
token,
"Bearer",
System.currentTimeMillis() + TimeUnit.DAYS.toMillis(365),
TimeUnit.DAYS.toMillis(365)
)
} }
suspend fun getCurrentUser(): Pair<Int, String> { suspend fun getCurrentUser(): Pair<Int, String> {
val query = """ return withContext(Dispatchers.IO) {
|query User {
|Viewer {
|id
|mediaListOptions {
|scoreFormat
|}
|}
|}
|""".trimMargin()
val payload = jsonObject( val payload = jsonObject(
"query" to query "query" to currentUserQuery()
) )
val body = payload.toString().toRequestBody(jsonMime) val body = payload.toString().toRequestBody(MediaType.jsonType())
val request = Request.Builder() val request = Request.Builder().url(apiUrl).post(body).build()
.url(apiUrl) val netResponse = authClient.newCall(request).execute()
.post(body)
.build()
val netResponse = authClient.newCall(request).await()
val response = responseToJson(netResponse)
val viewer = response["data"]!!.obj["Viewer"].obj
Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
}
}
private fun responseToJson(netResponse: Response): JsonObject {
val responseBody = netResponse.body?.string().orEmpty() val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) { if (responseBody.isEmpty()) {
throw Exception("Null Response") throw Exception("Null Response")
} }
val response = JsonParser().parse(responseBody).obj
val data = response["data"]!!.obj return JsonParser.parseString(responseBody).obj
val viewer = data["Viewer"].obj
return Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
} }
private fun jsonToALManga(struct: JsonObject): ALManga { private fun jsonToALManga(struct: JsonObject): ALManga {
@ -289,6 +211,92 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon() fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", clientId)
.appendQueryParameter("response_type", "token") .appendQueryParameter("response_type", "token")
.build() .build()!!
fun addToLibraryQuery() = """
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
| id
| status
|}
|}
|""".trimMargin()
fun updateInLibraryQuery() = """
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|id
|status
|progress
|}
|}
|""".trimMargin()
fun searchQuery() = """
|query Search(${'$'}query: String) {
|Page (perPage: 50) {
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|id
|title {
|romaji
|}
|coverImage {
|large
|}
|type
|status
|chapters
|description
|startDate {
|year
|month
|day
|}
|}
|}
|}
|""".trimMargin()
fun findLibraryMangaQuery() = """
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|Page {
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|id
|status
|scoreRaw: score(format: POINT_100)
|progress
|media {
|id
|title {
|romaji
|}
|coverImage {
|large
|}
|type
|status
|chapters
|description
|startDate {
|year
|month
|day
|}
|}
|}
|}
|}
|""".trimMargin()
fun currentUserQuery() = """
|query User {
|Viewer {
|id
|mediaListOptions {
|scoreFormat
|}
|}
|}
|""".trimMargin()
} }
} }

View File

@ -4,7 +4,7 @@ import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor { class AnilistInterceptor(private val anilist: Anilist, private var token: String?) : Interceptor {
/** /**
* OAuth object used for authenticated requests. * OAuth object used for authenticated requests.

View File

@ -9,6 +9,15 @@ import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
data class OAuth(
val access_token: String,
val token_type: String,
val expires: Long,
val expires_in: Long) {
fun isExpired() = System.currentTimeMillis() > expires
}
data class ALManga( data class ALManga(
val media_id: Int, val media_id: Int,
val title_romaji: String, val title_romaji: String,
@ -56,7 +65,7 @@ data class ALUserManga(
total_chapters = manga.total_chapters total_chapters = manga.total_chapters
} }
fun toTrackStatus() = when (list_status) { private fun toTrackStatus() = when (list_status) {
"CURRENT" -> Anilist.READING "CURRENT" -> Anilist.READING
"COMPLETED" -> Anilist.COMPLETED "COMPLETED" -> Anilist.COMPLETED
"PAUSED" -> Anilist.PAUSED "PAUSED" -> Anilist.PAUSED

View File

@ -1,10 +0,0 @@
package eu.kanade.tachiyomi.data.track.anilist
data class OAuth(
val access_token: String,
val token_type: String,
val expires: Long,
val expires_in: Long) {
fun isExpired() = System.currentTimeMillis() > expires
}

View File

@ -28,10 +28,6 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
return track.score.toInt().toString() return track.score.toInt().toString()
} }
override suspend fun add(track: Track): Track {
return api.addLibManga(track)
}
override suspend fun update(track: Track): Track { override suspend fun update(track: Track): Track {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = COMPLETED track.status = COMPLETED
@ -51,7 +47,7 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
} else { } else {
track.score = DEFAULT_SCORE.toFloat() track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS track.status = DEFAULT_STATUS
add(track) api.addLibManga(track)
update(track) update(track)
} }
return track return track

View File

@ -69,10 +69,6 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
return df.format(track.score) return df.format(track.score)
} }
override suspend fun add(track: Track): Track {
return api.addLibManga(track, getUserId())
}
override suspend fun update(track: Track): Track { override suspend fun update(track: Track): Track {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = COMPLETED track.status = COMPLETED
@ -90,7 +86,7 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
} else { } else {
track.score = DEFAULT_SCORE track.score = DEFAULT_SCORE
track.status = DEFAULT_STATUS track.status = DEFAULT_STATUS
return add(track) return api.addLibManga(track, getUserId())
} }
} }

View File

@ -8,29 +8,14 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import timber.log.Timber
class Myanimelist(private val context: Context, id: Int) : TrackService(id) { class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
companion object {
const val READING = 1
const val COMPLETED = 2
const val ON_HOLD = 3
const val DROPPED = 4
const val PLAN_TO_READ = 6
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
const val BASE_URL = "https://myanimelist.net"
const val USER_SESSION_COOKIE = "MALSESSIONID"
const val LOGGED_IN_COOKIE = "is_logged_in"
}
private val interceptor by lazy { MyAnimeListInterceptor(this) } private val interceptor by lazy { MyAnimeListInterceptor(this) }
private val api by lazy { MyAnimeListApi(client, interceptor) } private val api by lazy { MyAnimeListApi(client, interceptor) }
override val name: String override val name = "MyAnimeList"
get() = "MyAnimeList"
override fun getLogo() = R.drawable.tracker_mal override fun getLogo() = R.drawable.tracker_mal
@ -59,10 +44,6 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
return track.score.toInt().toString() return track.score.toInt().toString()
} }
override suspend fun add(track: Track): Track {
return api.addLibManga(track)
}
override suspend fun update(track: Track): Track { override suspend fun update(track: Track): Track {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = COMPLETED track.status = COMPLETED
@ -80,7 +61,7 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
// Set default fields if it's not found in the list // Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat() track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS track.status = DEFAULT_STATUS
add(track) return api.addLibManga(track)
} }
return track return track
} }
@ -98,18 +79,19 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
override suspend fun login(username: String, password: String): Boolean { override suspend fun login(username: String, password: String): Boolean {
logout() logout()
try { return try {
val csrf = api.login(username, password) val csrf = api.login(username, password)
saveCSRF(csrf) saveCSRF(csrf)
saveCredentials(username, password) saveCredentials(username, password)
return true true
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e)
logout() logout()
return false false
} }
} }
fun refreshLogin() { private suspend fun refreshLogin() {
val username = getUsername() val username = getUsername()
val password = getPassword() val password = getPassword()
logout() logout()
@ -119,13 +101,14 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
saveCSRF(csrf) saveCSRF(csrf)
saveCredentials(username, password) saveCredentials(username, password)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e)
logout() logout()
throw e throw e
} }
} }
// Attempt to login again if cookies have been cleared but credentials are still filled // Attempt to login again if cookies have been cleared but credentials are still filled
fun ensureLoggedIn() { suspend fun ensureLoggedIn() {
if (isAuthorized) return if (isAuthorized) return
if (!isLogged) throw Exception("MAL Login Credentials not found") if (!isLogged) throw Exception("MAL Login Credentials not found")
@ -138,10 +121,7 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
networkService.cookieManager.remove(BASE_URL.toHttpUrlOrNull()!!) networkService.cookieManager.remove(BASE_URL.toHttpUrlOrNull()!!)
} }
val isAuthorized: Boolean private val isAuthorized = super.isLogged && getCSRF().isNotEmpty() && checkCookies()
get() = super.isLogged &&
getCSRF().isNotEmpty() &&
checkCookies()
fun getCSRF(): String = preferences.trackToken(this).getOrDefault() fun getCSRF(): String = preferences.trackToken(this).getOrDefault()
@ -157,4 +137,19 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
return ckCount == 2 return ckCount == 2
} }
companion object {
const val READING = 1
const val COMPLETED = 2
const val ON_HOLD = 3
const val DROPPED = 4
const val PLAN_TO_READ = 6
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
const val BASE_URL = "https://myanimelist.net"
const val USER_SESSION_COOKIE = "MALSESSIONID"
const val LOGGED_IN_COOKIE = "is_logged_in"
}
} }

View File

@ -11,6 +11,8 @@ import eu.kanade.tachiyomi.network.consumeBody
import eu.kanade.tachiyomi.network.consumeXmlBody import eu.kanade.tachiyomi.network.consumeXmlBody
import eu.kanade.tachiyomi.util.selectInt import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText import eu.kanade.tachiyomi.util.selectText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -27,9 +29,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
suspend fun search(query: String): List<TrackSearch> { suspend fun search(query: String): List<TrackSearch> {
return withContext(Dispatchers.IO) {
if (query.startsWith(PREFIX_MY)) { if (query.startsWith(PREFIX_MY)) {
val realQuery = query.removePrefix(PREFIX_MY) queryUsersList(query)
return getList().filter { it.title.contains(realQuery, true) }.toList()
} else { } else {
val realQuery = query.take(100) val realQuery = query.take(100)
val response = client.newCall(GET(searchUrl(realQuery))).await() val response = client.newCall(GET(searchUrl(realQuery))).await()
@ -38,7 +40,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.select("table").select("tbody") .select("table").select("tbody")
.select("tr").drop(1) .select("tr").drop(1)
return matches.filter { row -> row.select(TD)[2].text() != "Novel" } matches.filter { row -> row.select(TD)[2].text() != "Novel" }
.map { row -> .map { row ->
TrackSearch.create(TrackManager.MYANIMELIST).apply { TrackSearch.create(TrackManager.MYANIMELIST).apply {
title = row.searchTitle() title = row.searchTitle()
@ -55,6 +57,12 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.toList() .toList()
} }
} }
}
private suspend fun queryUsersList(query: String): List<TrackSearch> {
val realQuery = query.removePrefix(PREFIX_MY).take(100)
return getList().filter { it.title.contains(realQuery, true) }.toList()
}
suspend fun addLibManga(track: Track): Track { suspend fun addLibManga(track: Track): Track {
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track))).await() authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track))).await()
@ -67,13 +75,14 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
} }
suspend fun findLibManga(track: Track): Track? { suspend fun findLibManga(track: Track): Track? {
return withContext(Dispatchers.IO) {
val response = authClient.newCall(GET(url = listEntryUrl(track.media_id))).await() val response = authClient.newCall(GET(url = listEntryUrl(track.media_id))).await()
var libTrack: Track? = null var remoteTrack: Track? = null
response.use { response.use {
if (it.priorResponse?.isRedirect != true) { if (it.priorResponse?.isRedirect != true) {
val trackForm = Jsoup.parse(it.consumeBody()) val trackForm = Jsoup.parse(it.consumeBody())
libTrack = Track.create(TrackManager.MYANIMELIST).apply { remoteTrack = Track.create(TrackManager.MYANIMELIST).apply {
last_chapter_read = last_chapter_read =
trackForm.select("#add_manga_num_read_chapters").`val`().toInt() trackForm.select("#add_manga_num_read_chapters").`val`().toInt()
total_chapters = trackForm.select("#totalChap").text().toInt() total_chapters = trackForm.select("#totalChap").text().toInt()
@ -84,7 +93,8 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
} }
} }
} }
return libTrack remoteTrack
}
} }
suspend fun getLibManga(track: Track): Track { suspend fun getLibManga(track: Track): Track {
@ -96,15 +106,15 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
} }
} }
fun login(username: String, password: String): String { suspend fun login(username: String, password: String): String {
return withContext(Dispatchers.IO) {
val csrf = getSessionInfo() val csrf = getSessionInfo()
login(username, password, csrf) login(username, password, csrf)
csrf
return csrf }
} }
private fun getSessionInfo(): String { private suspend fun getSessionInfo(): String {
val response = client.newCall(GET(loginUrl())).execute() val response = client.newCall(GET(loginUrl())).execute()
return Jsoup.parse(response.consumeBody()) return Jsoup.parse(response.consumeBody())
@ -112,15 +122,17 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.attr("content") .attr("content")
} }
private fun login(username: String, password: String, csrf: String) { private suspend fun login(username: String, password: String, csrf: String) {
withContext(Dispatchers.IO) {
val response = val response =
client.newCall(POST(url = loginUrl(), body = loginPostBody(username, password, csrf))) client.newCall(POST(loginUrl(), body = loginPostBody(username, password, csrf)))
.execute() .execute()
response.use { response.use {
if (response.priorResponse?.code != 302) throw Exception("Authentication error") if (response.priorResponse?.code != 302) throw Exception("Authentication error")
} }
} }
}
private suspend fun getList(): List<TrackSearch> { private suspend fun getList(): List<TrackSearch> {
val results = getListXml(getListUrl()).select("manga") val results = getListXml(getListUrl()).select("manga")
@ -140,14 +152,16 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
} }
private suspend fun getListUrl(): String { private suspend fun getListUrl(): String {
return withContext(Dispatchers.IO) {
val response = val response =
authClient.newCall(POST(url = exportListUrl(), body = exportPostBody())).await() authClient.newCall(POST(url = exportListUrl(), body = exportPostBody())).execute()
return baseUrl + Jsoup.parse(response.consumeBody()) baseUrl + Jsoup.parse(response.consumeBody())
.select("div.goodresult") .select("div.goodresult")
.select("a") .select("a")
.attr("href") .attr("href")
} }
}
private suspend fun getListXml(url: String): Document { private suspend fun getListXml(url: String): Document {
val response = authClient.newCall(GET(url)).await() val response = authClient.newCall(GET(url)).await()

View File

@ -1,5 +1,9 @@
package eu.kanade.tachiyomi.data.track.myanimelist package eu.kanade.tachiyomi.data.track.myanimelist
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
@ -8,20 +12,17 @@ import okhttp3.Response
import okio.Buffer import okio.Buffer
import org.json.JSONObject import org.json.JSONObject
class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor { class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor {
val scope = CoroutineScope(Job() + Dispatchers.Main)
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
scope.launch {
myanimelist.ensureLoggedIn() myanimelist.ensureLoggedIn()
val request = chain.request()
var response = chain.proceed(updateRequest(request))
if (response.code == 400) {
myanimelist.refreshLogin()
response = chain.proceed(updateRequest(request))
} }
val request = chain.request()
return chain.proceed(updateRequest(request))
return response
} }
private fun updateRequest(request: Request): Request { private fun updateRequest(request: Request): Request {
@ -46,7 +47,9 @@ class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor
private fun updateFormBody(requestBody: RequestBody): RequestBody { private fun updateFormBody(requestBody: RequestBody): RequestBody {
val formString = bodyToString(requestBody) val formString = bodyToString(requestBody)
return "$formString${if (formString.isNotEmpty()) "&" else ""}${MyAnimeListApi.CSRF}=${myanimelist.getCSRF()}".toRequestBody(requestBody.contentType()) return "$formString${if (formString.isNotEmpty()) "&" else ""}${MyAnimeListApi.CSRF}=${myanimelist.getCSRF()}".toRequestBody(
requestBody.contentType()
)
} }
private fun updateJsonBody(requestBody: RequestBody): RequestBody { private fun updateJsonBody(requestBody: RequestBody): RequestBody {

View File

@ -1,13 +0,0 @@
package eu.kanade.tachiyomi.data.track.shikimori
data class OAuth(
val access_token: String,
val token_type: String,
val created_at: Long,
val expires_in: Long,
val refresh_token: String?) {
// Access token lives 1 day
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
}

View File

@ -12,67 +12,6 @@ import uy.kohesive.injekt.injectLazy
class Shikimori(private val context: Context, id: Int) : TrackService(id) { class Shikimori(private val context: Context, id: Int) : TrackService(id) {
override fun getScoreList(): List<String> {
return IntRange(0, 10).map(Int::toString)
}
override fun displayScore(track: Track): String {
return track.score.toInt().toString()
}
override suspend fun add(track: Track): Track {
return api.addLibManga(track, getUsername())
}
override suspend fun update(track: Track): Track {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = COMPLETED
}
return api.updateLibManga(track, getUsername())
}
override suspend fun bind(track: Track): Track {
val remoteTrack = api.findLibManga(track, getUsername())
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
update(track)
} else {
// Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS
add(track)
}
return track
}
override suspend fun search(query: String): List<TrackSearch> {
return api.search(query)
}
override suspend fun refresh(track: Track): Track {
val remoteTrack = api.findLibManga(track, getUsername())
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
}
return track
}
companion object {
const val READING = 1
const val COMPLETED = 2
const val ON_HOLD = 3
const val DROPPED = 4
const val PLANNING = 5
const val REPEATING = 6
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
}
override val name = "Shikimori" override val name = "Shikimori"
private val gson: Gson by injectLazy() private val gson: Gson by injectLazy()
@ -101,6 +40,49 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
} }
} }
override fun getScoreList(): List<String> {
return IntRange(0, 10).map(Int::toString)
}
override fun displayScore(track: Track): String {
return track.score.toInt().toString()
}
override suspend fun update(track: Track): Track {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = COMPLETED
}
return api.updateLibManga(track, getUsername())
}
override suspend fun bind(track: Track): Track {
val remoteTrack = api.findLibManga(track, getUsername())
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
update(track)
} else {
// Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS
return api.addLibManga(track, getUsername())
}
return track
}
override suspend fun search(query: String) = api.search(query)
override suspend fun refresh(track: Track): Track {
val remoteTrack = api.findLibManga(track, getUsername())
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
}
return track
}
override suspend fun login(username: String, password: String) = login(password) override suspend fun login(username: String, password: String) = login(password)
suspend fun login(code: String): Boolean { suspend fun login(code: String): Boolean {
@ -136,4 +118,16 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
preferences.trackToken(this).set(null) preferences.trackToken(this).set(null)
interceptor.newAuth(null) interceptor.newAuth(null)
} }
companion object {
const val READING = 1
const val COMPLETED = 2
const val ON_HOLD = 3
const val DROPPED = 4
const val PLANNING = 5
const val REPEATING = 6
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
}
} }

View File

@ -22,3 +22,15 @@ fun toTrackStatus(status: String) = when (status) {
else -> throw Exception("Unknown status") else -> throw Exception("Unknown status")
} }
data class OAuth(
val access_token: String,
val token_type: String,
val created_at: Long,
val expires_in: Long,
val refresh_token: String?) {
// Access token lives 1 day
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
}

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.network
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import rx.Observable import rx.Observable
import rx.Producer import rx.Producer
import rx.Subscription import rx.Subscription
@ -98,6 +99,8 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene
return progressClient.newCall(request) return progressClient.newCall(request)
} }
fun MediaType.Companion.jsonType() : MediaType = "application/json; charset=utf-8".toMediaTypeOrNull()!!
fun Response.consumeBody(): String? { fun Response.consumeBody(): String? {
use { use {
if (it.code != 200) throw Exception("HTTP error ${it.code}") if (it.code != 200) throw Exception("HTTP error ${it.code}")

View File

@ -101,6 +101,7 @@ import jp.wasabeef.glide.transformations.MaskTransformation
import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.manga_details_controller.* import kotlinx.android.synthetic.main.manga_details_controller.*
import kotlinx.android.synthetic.main.manga_header_item.* import kotlinx.android.synthetic.main.manga_header_item.*
import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
@ -929,10 +930,12 @@ open class MangaDetailsController : BaseController,
} }
fun trackRefreshError(error: Exception) { fun trackRefreshError(error: Exception) {
Timber.e(error)
trackingBottomSheet?.onRefreshError(error) trackingBottomSheet?.onRefreshError(error)
} }
fun trackSearchError(error: Exception) { fun trackSearchError(error: Exception) {
Timber.e(error)
trackingBottomSheet?.onSearchResultsError(error) trackingBottomSheet?.onSearchResultsError(error)
} }

View File

@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.ui.main.MainActivity
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -40,6 +41,11 @@ class AnilistLoginActivity : AppCompatActivity() {
} }
} }
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
private fun returnToSettings() { private fun returnToSettings() {
finish() finish()

View File

@ -13,24 +13,27 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.widget.SimpleTextWatcher import eu.kanade.tachiyomi.widget.SimpleTextWatcher
import kotlinx.android.synthetic.main.pref_account_login.view.login import kotlinx.android.synthetic.main.pref_account_login.view.*
import kotlinx.android.synthetic.main.pref_account_login.view.password
import kotlinx.android.synthetic.main.pref_account_login.view.show_password
import kotlinx.android.synthetic.main.pref_account_login.view.username_label
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import rx.Subscription import rx.Subscription
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
abstract class LoginDialogPreference(private val usernameLabel: String? = null, bundle: Bundle? = null) : abstract class LoginDialogPreference(
DialogController(bundle), CoroutineScope { private val usernameLabel: String? = null,
bundle: Bundle? = null
) :
DialogController(bundle) {
var v: View? = null var v: View? = null
private set private set
val preferences: PreferencesHelper by injectLazy() val preferences: PreferencesHelper by injectLazy()
val scope = CoroutineScope(Job() + Dispatchers.Main)
var requestSubscription: Subscription? = null var requestSubscription: Subscription? = null
open var canLogout = false open var canLogout = false
@ -79,7 +82,6 @@ abstract class LoginDialogPreference(private val usernameLabel: String? = null,
} }
}) })
} }
} }
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
@ -90,11 +92,11 @@ abstract class LoginDialogPreference(private val usernameLabel: String? = null,
} }
open fun onDialogClosed() { open fun onDialogClosed() {
scope.cancel()
requestSubscription?.unsubscribe() requestSubscription?.unsubscribe()
} }
protected abstract fun checkLogin() protected abstract fun checkLogin()
protected abstract fun setCredentialsOnView(view: View) protected abstract fun setCredentialsOnView(view: View)
} }

View File

@ -7,12 +7,9 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.android.synthetic.main.pref_account_login.view.* import kotlinx.android.synthetic.main.pref_account_login.view.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import kotlin.coroutines.CoroutineContext
class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) : class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) :
LoginDialogPreference(usernameLabel, bundle) { LoginDialogPreference(usernameLabel, bundle) {
@ -32,11 +29,7 @@ class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) :
password.setText(service.getPassword()) password.setText(service.getPassword())
} }
override val coroutineContext: CoroutineContext
get() = TODO("Not yet implemented")
override fun checkLogin() { override fun checkLogin() {
requestSubscription?.unsubscribe()
v?.apply { v?.apply {
if (username.text.isEmpty() || password.text.isEmpty()) if (username.text.isEmpty() || password.text.isEmpty())
@ -46,24 +39,30 @@ class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) :
val user = username.text.toString() val user = username.text.toString()
val pass = password.text.toString() val pass = password.text.toString()
launch { scope.launch {
try { try {
withContext(Dispatchers.IO) { val result = service.login(user, pass)
service.login(user, pass) if (result) {
}
withContext(Dispatchers.Main) {
dialog?.dismiss() dialog?.dismiss()
context.toast(R.string.login_success) context.toast(R.string.login_success)
} else {
errorResult(this@apply)
} }
} catch (error: Exception) { } catch (error: Exception) {
login.progress = -1 errorResult(this@apply)
login.setText(R.string.unknown_error)
error.message?.let { context.toast(it) } error.message?.let { context.toast(it) }
} }
} }
} }
} }
fun errorResult(view: View?) {
v?.apply {
login.progress = -1
login.setText(R.string.unknown_error)
}
}
override fun logout() { override fun logout() {
if (service.isLogged) { if (service.isLogged) {
service.logout() service.logout()