More tracker clean up

pull/3117/head
Carlos 5 years ago
parent f83a6bd489
commit 7bc12c04c4

@ -181,7 +181,7 @@ class BackupRestoreService : Service() {
*/
private suspend fun restoreBackup(uri: Uri) {
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
val json = JsonParser().parse(reader).asJsonObject
val json = JsonParser.parseReader(reader).asJsonObject
// Get parser version
val version = json.get(VERSION)?.asInt ?: 1
@ -296,16 +296,16 @@ class BackupRestoreService : Service() {
* @param manga manga that needs updating.
* @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 ->
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() }
.onErrorReturn {
errors.add("${manga.title} - ${it.message}")
track
}
try {
service.refresh(track)
db.insertTrack(track).executeAsBlocking()
}catch (e : Exception){
errors.add("${manga.title} - ${e.message}")
}
} else {
errors.add("${manga.title} - ${service?.name} not logged in")
val notLoggedIn = getString(R.string.not_logged_into, service?.name)

@ -64,11 +64,11 @@ import java.util.concurrent.atomic.AtomicInteger
* destroyed.
*/
class LibraryUpdateService(
val db: DatabaseHelper = Injekt.get(),
val sourceManager: SourceManager = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(),
val downloadManager: DownloadManager = Injekt.get(),
val trackManager: TrackManager = Injekt.get()
val db: DatabaseHelper = Injekt.get(),
val sourceManager: SourceManager = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(),
val downloadManager: DownloadManager = Injekt.get(),
val trackManager: TrackManager = Injekt.get()
) : Service() {
/**
@ -81,7 +81,6 @@ class LibraryUpdateService(
*/
private var subscription: Subscription? = null
/**
* Pending intent of action that cancels the library update
*/
@ -96,7 +95,7 @@ class LibraryUpdateService(
BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
}
private var job:Job? = null
private var job: Job? = null
private val mangaToUpdate = mutableListOf<LibraryManga>()
@ -108,14 +107,19 @@ class LibraryUpdateService(
/**
* 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))
.setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
.setLargeIcon(notificationBitmap)
.setOngoing(true)
.setOnlyAlertOnce(true)
.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 {
context.startForegroundService(intent)
}
}
else {
} else {
if (target == Target.CHAPTERS) category?.id?.let {
instance?.addCategory(it)
}
@ -190,7 +193,7 @@ class LibraryUpdateService(
context.stopService(Intent(context, LibraryUpdateService::class.java))
}
private var listener:LibraryServiceListener? = null
private var listener: LibraryServiceListener? = null
fun setListener(listener: LibraryServiceListener) {
this.listener = listener
@ -212,7 +215,8 @@ class LibraryUpdateService(
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
val mangas =
getMangaToUpdate(categoryId, Target.CHAPTERS).sortedWith(
rankingScheme[selectedScheme])
rankingScheme[selectedScheme]
)
categoryIds.add(categoryId)
addManga(mangas)
}
@ -228,9 +232,9 @@ class LibraryUpdateService(
var listToUpdate = if (categoryId != -1) {
categoryIds.add(categoryId)
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
}
else {
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map(String::toInt)
} else {
val categoriesToUpdate =
preferences.libraryUpdateCategories().getOrDefault().map(String::toInt)
categoryIds.addAll(categoriesToUpdate)
if (categoriesToUpdate.isNotEmpty())
db.getLibraryMangas().executeAsBlocking()
@ -259,7 +263,8 @@ class LibraryUpdateService(
super.onCreate()
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build())
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))
}
@ -297,7 +302,7 @@ class LibraryUpdateService(
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) return START_NOT_STICKY
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
?: return START_NOT_STICKY
?: return START_NOT_STICKY
// Unsubscribe from any previous subscription if needed.
subscription?.unsubscribe()
@ -307,41 +312,44 @@ class LibraryUpdateService(
val mangaList =
getMangaToUpdate(intent, target).sortedWith(rankingScheme[selectedScheme])
// Update favorite manga. Destroy service when completed or in case of an error.
if (target == Target.CHAPTERS) {
updateChapters(mangaList, startId)
}
else {
if (target == Target.DETAILS) {
// Update either chapter list or manga details.
subscription = Observable.defer {
when (target) {
Target.DETAILS -> updateDetails(mangaList)
else -> updateTrackings(mangaList)
}
updateDetails(mangaList)
}.subscribeOn(Schedulers.io()).subscribe({}, {
Timber.e(it)
stopSelf(startId)
}, {
stopSelf(startId)
})
} else {
launchTarget(target, mangaList, startId)
}
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 ->
Timber.e(exception)
// Boolean to determine if user wants to automatically download new chapters.
stopSelf(startId)
}
job = GlobalScope.launch(handler) {
updateChaptersJob(mangaToAdd)
if (target == Target.CHAPTERS) {
job = GlobalScope.launch(handler) {
updateChaptersJob(mangaToAdd)
}
} else {
job = GlobalScope.launch(handler) {
updateTrackings(mangaToAdd)
}
}
job?.invokeOnCompletion { stopSelf(startId) }
}
private suspend fun updateChaptersJob(mangaToAdd: List<LibraryManga>) {
// 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.
val downloadNew = preferences.downloadNew().getOrDefault()
// Boolean to determine if DownloadManager has downloads
@ -352,7 +360,7 @@ class LibraryUpdateService(
mangaToUpdate.addAll(mangaToAdd)
while (count < mangaToUpdate.size) {
val shouldDownload = (downloadNew && (categoriesToDownload.isEmpty() ||
mangaToUpdate[count].category in categoriesToDownload))
mangaToUpdate[count].category in categoriesToDownload))
if (updateMangaChapters(mangaToUpdate[count], count, shouldDownload)) {
hasDownloads = true
}
@ -370,8 +378,7 @@ class LibraryUpdateService(
}
}
.subscribeOn(Schedulers.io()).subscribe {}
}
else if (downloadNew && hasDownloads) {
} else if (downloadNew && hasDownloads) {
DownloadService.start(this)
}
}
@ -379,8 +386,12 @@ class LibraryUpdateService(
cancelProgressNotification()
}
private suspend fun updateMangaChapters(manga: LibraryManga, progess: Int, shouldDownload: Boolean):
Boolean {
private suspend fun updateMangaChapters(
manga: LibraryManga,
progess: Int,
shouldDownload: Boolean
):
Boolean {
try {
var hasDownloads = false
if (job?.isCancelled == true) {
@ -389,7 +400,7 @@ class LibraryUpdateService(
showProgressNotification(manga, progess, mangaToUpdate.size)
val source = sourceManager.get(manga.source) as? HttpSource ?: return false
val fetchedChapters = withContext(Dispatchers.IO) {
source.fetchChapterList(manga).toBlocking().single()
source.fetchChapterList(manga).toBlocking().single()
} ?: emptyList()
if (fetchedChapters.isNotEmpty()) {
val newChapters = syncChaptersWithSource(db, fetchedChapters, manga, source)
@ -406,8 +417,7 @@ class LibraryUpdateService(
)
}
return hasDownloads
}
catch (e: Exception) {
} catch (e: Exception) {
Timber.e("Failed updating: ${manga.title}: $e")
return false
}
@ -433,7 +443,7 @@ class LibraryUpdateService(
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty()
return source.fetchChapterList(manga)
.map { syncChaptersWithSource(db, it, manga, source) }
.map { syncChaptersWithSource(db, it, manga, source) }
}
/**
@ -449,62 +459,57 @@ class LibraryUpdateService(
// Emit each manga and update it sequentially.
return Observable.from(mangaToUpdate)
// Notify manga that will update.
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
// Update the details of the manga.
.concatMap { manga ->
val source = sourceManager.get(manga.source) as? HttpSource
?: return@concatMap Observable.empty<LibraryManga>()
source.fetchMangaDetails(manga)
.map { networkManga ->
val thumbnailUrl = manga.thumbnail_url
manga.copyFrom(networkManga)
db.insertManga(manga).executeAsBlocking()
if (thumbnailUrl != networkManga.thumbnail_url)
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
manga
}
.onErrorReturn { manga }
}
.doOnCompleted {
cancelProgressNotification()
}
// Notify manga that will update.
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
// Update the details of the manga.
.concatMap { manga ->
val source = sourceManager.get(manga.source) as? HttpSource
?: return@concatMap Observable.empty<LibraryManga>()
source.fetchMangaDetails(manga)
.map { networkManga ->
val thumbnailUrl = manga.thumbnail_url
manga.copyFrom(networkManga)
db.insertManga(manga).executeAsBlocking()
if (thumbnailUrl != networkManga.thumbnail_url)
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
manga
}
.onErrorReturn { manga }
}
.doOnCompleted {
cancelProgressNotification()
}
}
/**
* 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.
*/
private fun updateTrackings(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
private suspend fun updateTrackings(mangaToUpdate: List<LibraryManga>) {
// Initialize the variables holding the progress of the updates.
var count = 0
val loggedServices = trackManager.services.filter { it.isLogged }
// Emit each manga and update it sequentially.
return Observable.from(mangaToUpdate)
// Notify manga that will update.
.doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) }
// Update the tracking details.
.concatMap { manga ->
val tracks = db.getTracks(manga).executeAsBlocking()
Observable.from(tracks)
.concatMap { track ->
val service = trackManager.getService(track.sync_id)
if (service != null && service in loggedServices) {
service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() }
.onErrorReturn { track }
} else {
Observable.empty()
}
}
.map { manga }
}
.doOnCompleted {
cancelProgressNotification()
mangaToUpdate.forEach { manga ->
showProgressNotification(manga, count++, mangaToUpdate.size)
val tracks = db.getTracks(manga).executeAsBlocking()
tracks.forEach { track ->
val service = trackManager.getService(track.sync_id)
if (service != null && service in loggedServices) {
try {
service.refresh(track)
db.insertTrack(track).executeAsBlocking()
} catch (e: Exception) {
Timber.e(e)
}
}
}
}
cancelProgressNotification()
}
/**
@ -515,10 +520,12 @@ class LibraryUpdateService(
* @param total the total progress.
*/
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())
.setProgress(total, current, false)
.build())
.build()
)
}
/**
@ -539,15 +546,17 @@ class LibraryUpdateService(
.asBitmap().load(manga).dontTransform().centerCrop().circleCrop()
.override(256, 256).submit().get()
setLargeIcon(icon)
} catch (e: Exception) {
}
catch (e: Exception) { }
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
setContentTitle(manga.currentTitle())
color = ContextCompat.getColor(this@LibraryUpdateService, R.color.colorAccent)
val chaptersNames = if (chapterNames.size > 5) {
"${chapterNames.take(4).joinToString(", ")}, " +
resources.getQuantityString(R.plurals.notification_and_n_more,
(chapterNames.size - 4), (chapterNames.size - 4))
resources.getQuantityString(
R.plurals.notification_and_n_more,
(chapterNames.size - 4), (chapterNames.size - 4)
)
} else chapterNames.joinToString(", ")
setContentText(chaptersNames)
setStyle(NotificationCompat.BigTextStyle().bigText(chaptersNames))
@ -558,41 +567,57 @@ class LibraryUpdateService(
this@LibraryUpdateService, manga, chapters.first()
)
)
addAction(R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read),
NotificationReceiver.markAsReadPendingBroadcast(this@LibraryUpdateService,
manga, chapters, 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))
addAction(
R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read),
NotificationReceiver.markAsReadPendingBroadcast(
this@LibraryUpdateService,
manga, chapters, 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)
}, manga.id.hashCode()))
}
NotificationManagerCompat.from(this).apply {
notify(Notifications.ID_NEW_CHAPTERS, notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setSmallIcon(R.drawable.ic_tachi)
setLargeIcon(notificationBitmap)
setContentTitle(getString(R.string.notification_new_chapters))
color = ContextCompat.getColor(applicationContext, R.color.colorAccent)
if (updates.size > 1) {
setContentText(resources.getQuantityString(R.plurals
.notification_new_chapters_text,
updates.size, updates.size))
setStyle(NotificationCompat.BigTextStyle().bigText(updates.keys.joinToString("\n") {
it.currentTitle().chop(45)
}))
}
else {
setContentText(updates.keys.first().currentTitle().chop(45))
}
priority = NotificationCompat.PRIORITY_HIGH
setGroup(Notifications.GROUP_NEW_CHAPTERS)
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
setGroupSummary(true)
setContentIntent(getNotificationIntent())
setAutoCancel(true)
})
notify(
Notifications.ID_NEW_CHAPTERS,
notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setSmallIcon(R.drawable.ic_tachi)
setLargeIcon(notificationBitmap)
setContentTitle(getString(R.string.notification_new_chapters))
color = ContextCompat.getColor(applicationContext, R.color.colorAccent)
if (updates.size > 1) {
setContentText(
resources.getQuantityString(
R.plurals
.notification_new_chapters_text,
updates.size, updates.size
)
)
setStyle(
NotificationCompat.BigTextStyle()
.bigText(updates.keys.joinToString("\n") {
it.currentTitle().chop(45)
})
)
} else {
setContentText(updates.keys.first().currentTitle().chop(45))
}
priority = NotificationCompat.PRIORITY_HIGH
setGroup(Notifications.GROUP_NEW_CHAPTERS)
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
setGroupSummary(true)
setContentIntent(getNotificationIntent())
setAutoCancel(true)
})
notifications.forEach {
notify(it.second, it.first)

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

@ -8,28 +8,11 @@ import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
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"
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 getStatusList(): List<Int> {
return listOf(READING, PLANNING, COMPLETED, REPEATING, PAUSED, DROPPED)
}
override fun getStatusList() = listOf(READING, PLANNING, COMPLETED, REPEATING, PAUSED, DROPPED)
override fun getStatus(status: Int): String = with(context) {
when (status) {
@ -93,13 +74,13 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
// 100 point
POINT_100 -> index.toFloat()
// 5 stars
POINT_5 -> when {
index == 0 -> 0f
POINT_5 -> when (index) {
0 -> 0f
else -> index * 20f - 10f
}
// Smiley
POINT_3 -> when {
index == 0 -> 0f
POINT_3 -> when (index) {
0 -> 0f
else -> index * 25f + 10f
}
// 10 point decimal
@ -112,8 +93,8 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
val score = track.score
return when (scorePreference.getOrDefault()) {
POINT_5 -> when {
score == 0f -> "0 ★"
POINT_5 -> when (score) {
0f -> "0 ★"
else -> "${((score + 10) / 20).toInt()}"
}
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 {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
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 (track.library_id == null || track.library_id!! == 0L) {
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
}
return api.updateLibManga(track)
return api.updateLibraryManga(track)
}
override suspend fun bind(track: Track): Track {
val remoteTrack = api.findLibManga(track, getUsername().toInt())
if (remoteTrack != null) {
return if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
return update(track)
update(track)
} else {
// Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS
return add(track)
api.addLibManga(track)
}
}
override suspend fun search(query: String): List<TrackSearch> {
return api.search(query)
}
override suspend fun search(query: String) = api.search(query)
override suspend fun refresh(track: Track): Track {
val remoteTrack = api.getLibManga(track, getUsername().toInt())
@ -180,15 +153,16 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
val oauth = api.createOAuth(token)
interceptor.setAuth(oauth)
try {
val currentUser = api.getCurrentUser()
scorePreference.set(currentUser.second)
saveCredentials(currentUser.first.toString(), oauth.access_token)
return true
} catch (e: Exception) {
logout()
return false
}
return try {
val currentUser = api.getCurrentUser()
scorePreference.set(currentUser.second)
saveCredentials(currentUser.first.toString(), oauth.access_token)
true
} catch (e: Exception) {
Timber.e(e)
logout()
false
}
}
override fun logout() {
@ -205,9 +179,29 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
return try {
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
} catch (e: Exception) {
Timber.e(e)
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"
}
}

@ -11,56 +11,218 @@ import com.google.gson.JsonObject
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.await
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import eu.kanade.tachiyomi.network.jsonType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import java.util.Calendar
import java.util.concurrent.TimeUnit
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull()
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
suspend fun addLibManga(track: Track): Track {
val query = """
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
| id
| status
|}
|}
|""".trimMargin()
val variables = jsonObject(
"mangaId" to track.media_id,
"progress" to track.last_chapter_read,
"status" to track.toAnilistStatus()
)
val payload = jsonObject(
"query" to query,
"variables" to variables
return withContext(Dispatchers.IO) {
val variables = jsonObject(
"mangaId" to track.media_id,
"progress" to track.last_chapter_read,
"status" to track.toAnilistStatus()
)
val payload = jsonObject(
"query" to addToLibraryQuery(),
"variables" to variables
)
val body = payload.toString().toRequestBody(MediaType.jsonType())
val request = Request.Builder().url(apiUrl).post(body).build()
val netResponse = authClient.newCall(request).execute()
val responseBody = netResponse.body?.string().orEmpty()
netResponse.close()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
val response = JsonParser.parseString(responseBody).obj
track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
track
}
}
suspend fun updateLibraryManga(track: Track): Track {
return withContext(Dispatchers.IO) {
val variables = jsonObject(
"listId" to track.library_id,
"progress" to track.last_chapter_read,
"status" to track.toAnilistStatus(),
"score" to track.score.toInt()
)
val payload = jsonObject(
"query" to updateInLibraryQuery(),
"variables" to variables
)
val body = payload.toString().toRequestBody(MediaType.jsonType())
val request = Request.Builder().url(apiUrl).post(body).build()
val response = authClient.newCall(request).execute()
track
}
}
suspend fun search(search: String): List<TrackSearch> {
return withContext(Dispatchers.IO) {
val variables = jsonObject(
"query" to search
)
val payload = jsonObject(
"query" to searchQuery(),
"variables" to variables
)
val body = payload.toString().toRequestBody(MediaType.jsonType())
val request = Request.Builder().url(apiUrl).post(body).build()
val netResponse = authClient.newCall(request).execute()
val response = responseToJson(netResponse)
val media = response["data"]!!.obj["Page"].obj["mediaList"].array
val entries = media.map { jsonToALManga(it.obj) }
entries.map { it.toTrack() }
}
}
suspend fun findLibManga(track: Track, userid: Int): Track? {
return withContext(Dispatchers.IO) {
val variables = jsonObject(
"id" to userid,
"manga_id" to track.media_id
)
val payload = jsonObject(
"query" to findLibraryMangaQuery(),
"variables" to variables
)
val body = payload.toString().toRequestBody(MediaType.jsonType())
val request = Request.Builder().url(apiUrl).post(body).build()
val result = authClient.newCall(request).execute()
result.let { resp ->
val response = responseToJson(resp)
val media = response["data"]!!.obj["Page"].obj["mediaList"].array
val entries = media.map { jsonToALUserManga(it.obj) }
entries.firstOrNull()?.toTrack()
}
}
}
suspend fun getLibManga(track: Track, userid: Int): Track {
val remoteTrack = findLibManga(track, userid)
if (remoteTrack == null) {
throw Exception("Could not find manga")
} else {
return remoteTrack
}
}
fun createOAuth(token: String): OAuth {
return OAuth(
token,
"Bearer",
System.currentTimeMillis() + TimeUnit.DAYS.toMillis(365),
TimeUnit.DAYS.toMillis(365)
)
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
val netResponse = authClient.newCall(request).await()
}
suspend fun getCurrentUser(): Pair<Int, String> {
return withContext(Dispatchers.IO) {
val payload = jsonObject(
"query" to currentUserQuery()
)
val body = payload.toString().toRequestBody(MediaType.jsonType())
val request = Request.Builder().url(apiUrl).post(body).build()
val netResponse = authClient.newCall(request).execute()
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()
netResponse.close()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
val response = JsonParser().parse(responseBody).obj
track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
return track
return JsonParser.parseString(responseBody).obj
}
private fun jsonToALManga(struct: JsonObject): ALManga {
val date = try {
val date = Calendar.getInstance()
date.set(
struct["startDate"]["year"].nullInt ?: 0,
(struct["startDate"]["month"].nullInt ?: 0) - 1,
struct["startDate"]["day"].nullInt ?: 0
)
date.timeInMillis
} catch (_: Exception) {
0L
}
return ALManga(
struct["id"].asInt,
struct["title"]["romaji"].asString,
struct["coverImage"]["large"].asString,
struct["description"].nullString.orEmpty(),
struct["type"].asString,
struct["status"].asString,
date,
struct["chapters"].nullInt ?: 0
)
}
private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
return ALUserManga(
struct["id"].asLong,
struct["status"].asString,
struct["scoreRaw"].asInt,
struct["progress"].asInt,
jsonToALManga(struct["media"].obj)
)
}
suspend fun updateLibManga(track: Track): Track {
val query = """
companion object {
private const val clientId = "385"
private const val apiUrl = "https://graphql.anilist.co/"
private const val baseUrl = "https://anilist.co/api/v2/"
private const val baseMangaUrl = "https://anilist.co/manga/"
fun mangaUrl(mediaId: Int): String {
return baseMangaUrl + mediaId
}
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
.appendQueryParameter("client_id", clientId)
.appendQueryParameter("response_type", "token")
.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
@ -69,27 +231,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".trimMargin()
val variables = jsonObject(
"listId" to track.library_id,
"progress" to track.last_chapter_read,
"status" to track.toAnilistStatus(),
"score" to track.score.toInt()
)
val payload = jsonObject(
"query" to query,
"variables" to variables
)
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
authClient.newCall(request).execute()
return track
}
suspend fun search(search: String): List<TrackSearch> {
val query = """
fun searchQuery() = """
|query Search(${'$'}query: String) {
|Page (perPage: 50) {
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
@ -113,33 +256,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".trimMargin()
val variables = jsonObject(
"query" to search
)
val payload = jsonObject(
"query" to query,
"variables" to variables
)
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
val netResponse = authClient.newCall(request).await()
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) }
return entries.map { it.toTrack() }
}
suspend fun findLibManga(track: Track, userid: Int): Track? {
val query = """
fun findLibraryMangaQuery() = """
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|Page {
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
@ -169,49 +287,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".trimMargin()
val variables = jsonObject(
"id" to userid,
"manga_id" to track.media_id
)
val payload = jsonObject(
"query" to query,
"variables" to variables
)
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
val result = authClient.newCall(request).await()
return result.let { resp ->
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) }
entries.firstOrNull()?.toTrack()
}
}
suspend fun getLibManga(track: Track, userid: Int): Track {
val track = findLibManga(track, userid)
if (track == null) {
throw Exception("Could not find manga")
} else {
return track
}
}
fun createOAuth(token: String): OAuth {
return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
}
suspend fun getCurrentUser(): Pair<Int, String> {
val query = """
fun currentUserQuery() = """
|query User {
|Viewer {
|id
@ -221,74 +298,5 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".trimMargin()
val payload = jsonObject(
"query" to query
)
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
val netResponse = authClient.newCall(request).await()
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 viewer = data["Viewer"].obj
return Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
}
private fun jsonToALManga(struct: JsonObject): ALManga {
val date = try {
val date = Calendar.getInstance()
date.set(
struct["startDate"]["year"].nullInt ?: 0,
(struct["startDate"]["month"].nullInt ?: 0) - 1,
struct["startDate"]["day"].nullInt ?: 0
)
date.timeInMillis
} catch (_: Exception) {
0L
}
return ALManga(
struct["id"].asInt,
struct["title"]["romaji"].asString,
struct["coverImage"]["large"].asString,
struct["description"].nullString.orEmpty(),
struct["type"].asString,
struct["status"].asString,
date,
struct["chapters"].nullInt ?: 0
)
}
private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
return ALUserManga(
struct["id"].asLong,
struct["status"].asString,
struct["scoreRaw"].asInt,
struct["progress"].asInt,
jsonToALManga(struct["media"].obj)
)
}
companion object {
private const val clientId = "385"
private const val apiUrl = "https://graphql.anilist.co/"
private const val baseUrl = "https://anilist.co/api/v2/"
private const val baseMangaUrl = "https://anilist.co/manga/"
fun mangaUrl(mediaId: Int): String {
return baseMangaUrl + mediaId
}
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
.appendQueryParameter("client_id", clientId)
.appendQueryParameter("response_type", "token")
.build()
}
}

@ -4,7 +4,7 @@ import okhttp3.Interceptor
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.

@ -9,6 +9,15 @@ import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
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(
val media_id: Int,
val title_romaji: String,
@ -56,7 +65,7 @@ data class ALUserManga(
total_chapters = manga.total_chapters
}
fun toTrackStatus() = when (list_status) {
private fun toTrackStatus() = when (list_status) {
"CURRENT" -> Anilist.READING
"COMPLETED" -> Anilist.COMPLETED
"PAUSED" -> Anilist.PAUSED

@ -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
}

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

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

@ -8,29 +8,14 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import timber.log.Timber
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"
}
class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
private val interceptor by lazy { MyAnimeListInterceptor(this) }
private val api by lazy { MyAnimeListApi(client, interceptor) }
override val name: String
get() = "MyAnimeList"
override val name = "MyAnimeList"
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()
}
override suspend fun add(track: Track): Track {
return api.addLibManga(track)
}
override suspend fun update(track: Track): Track {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
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
track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS
add(track)
return api.addLibManga(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 {
logout()
try {
return try {
val csrf = api.login(username, password)
saveCSRF(csrf)
saveCredentials(username, password)
return true
true
} catch (e: Exception) {
Timber.e(e)
logout()
return false
false
}
}
fun refreshLogin() {
private suspend fun refreshLogin() {
val username = getUsername()
val password = getPassword()
logout()
@ -119,13 +101,14 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
saveCSRF(csrf)
saveCredentials(username, password)
} catch (e: Exception) {
Timber.e(e)
logout()
throw e
}
}
// Attempt to login again if cookies have been cleared but credentials are still filled
fun ensureLoggedIn() {
suspend fun ensureLoggedIn() {
if (isAuthorized) return
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()!!)
}
val isAuthorized: Boolean
get() = super.isLogged &&
getCSRF().isNotEmpty() &&
checkCookies()
private val isAuthorized = super.isLogged && getCSRF().isNotEmpty() && checkCookies()
fun getCSRF(): String = preferences.trackToken(this).getOrDefault()
@ -157,4 +137,19 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
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"
}
}

@ -11,6 +11,8 @@ import eu.kanade.tachiyomi.network.consumeBody
import eu.kanade.tachiyomi.network.consumeXmlBody
import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
@ -27,35 +29,41 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
suspend fun search(query: String): List<TrackSearch> {
if (query.startsWith(PREFIX_MY)) {
val realQuery = query.removePrefix(PREFIX_MY)
return getList().filter { it.title.contains(realQuery, true) }.toList()
} else {
val realQuery = query.take(100)
val response = client.newCall(GET(searchUrl(realQuery))).await()
val matches = Jsoup.parse(response.consumeBody())
.select("div.js-categories-seasonal.js-block-list.list")
.select("table").select("tbody")
.select("tr").drop(1)
return matches.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()
return withContext(Dispatchers.IO) {
if (query.startsWith(PREFIX_MY)) {
queryUsersList(query)
} else {
val realQuery = query.take(100)
val response = client.newCall(GET(searchUrl(realQuery))).await()
val matches = Jsoup.parse(response.consumeBody())
.select("div.js-categories-seasonal.js-block-list.list")
.select("table").select("tbody")
.select("tr").drop(1)
matches.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()
.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 {
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track))).await()
return track
@ -67,24 +75,26 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
}
suspend fun findLibManga(track: Track): Track? {
val response = authClient.newCall(GET(url = listEntryUrl(track.media_id))).await()
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
return withContext(Dispatchers.IO) {
val response = authClient.newCall(GET(url = listEntryUrl(track.media_id))).await()
var remoteTrack: Track? = null
response.use {
if (it.priorResponse?.isRedirect != true) {
val trackForm = Jsoup.parse(it.consumeBody())
remoteTrack = 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
}
}
}
remoteTrack
}
return libTrack
}
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 {
val csrf = getSessionInfo()
login(username, password, csrf)
return csrf
suspend fun login(username: String, password: String): String {
return withContext(Dispatchers.IO) {
val csrf = getSessionInfo()
login(username, password, csrf)
csrf
}
}
private fun getSessionInfo(): String {
private suspend fun getSessionInfo(): String {
val response = client.newCall(GET(loginUrl())).execute()
return Jsoup.parse(response.consumeBody())
@ -112,13 +122,15 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.attr("content")
}
private fun login(username: String, password: String, csrf: String) {
val response =
client.newCall(POST(url = loginUrl(), body = loginPostBody(username, password, csrf)))
.execute()
private suspend fun login(username: String, password: String, csrf: String) {
withContext(Dispatchers.IO) {
val response =
client.newCall(POST(loginUrl(), body = loginPostBody(username, password, csrf)))
.execute()
response.use {
if (response.priorResponse?.code != 302) throw Exception("Authentication error")
response.use {
if (response.priorResponse?.code != 302) throw Exception("Authentication error")
}
}
}
@ -140,13 +152,15 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
}
private suspend fun getListUrl(): String {
val response =
authClient.newCall(POST(url = exportListUrl(), body = exportPostBody())).await()
return baseUrl + Jsoup.parse(response.consumeBody())
.select("div.goodresult")
.select("a")
.attr("href")
return withContext(Dispatchers.IO) {
val response =
authClient.newCall(POST(url = exportListUrl(), body = exportPostBody())).execute()
baseUrl + Jsoup.parse(response.consumeBody())
.select("div.goodresult")
.select("a")
.attr("href")
}
}
private suspend fun getListXml(url: String): Document {

@ -1,5 +1,9 @@
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.Request
import okhttp3.RequestBody
@ -8,20 +12,17 @@ import okhttp3.Response
import okio.Buffer
import org.json.JSONObject
class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor {
class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
myanimelist.ensureLoggedIn()
val request = chain.request()
var response = chain.proceed(updateRequest(request))
val scope = CoroutineScope(Job() + Dispatchers.Main)
if (response.code == 400) {
myanimelist.refreshLogin()
response = chain.proceed(updateRequest(request))
override fun intercept(chain: Interceptor.Chain): Response {
scope.launch {
myanimelist.ensureLoggedIn()
}
val request = chain.request()
return chain.proceed(updateRequest(request))
return response
}
private fun updateRequest(request: Request): Request {
@ -46,13 +47,15 @@ class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor
private fun updateFormBody(requestBody: RequestBody): 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 {
val jsonString = bodyToString(requestBody)
val newBody = JSONObject(jsonString)
.put(MyAnimeListApi.CSRF, myanimelist.getCSRF())
.put(MyAnimeListApi.CSRF, myanimelist.getCSRF())
return newBody.toString().toRequestBody(requestBody.contentType())
}

@ -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)
}

@ -12,6 +12,34 @@ import uy.kohesive.injekt.injectLazy
class Shikimori(private val context: Context, id: Int) : TrackService(id) {
override val name = "Shikimori"
private val gson: Gson by injectLazy()
private val interceptor by lazy { ShikimoriInterceptor(this, gson) }
private val api by lazy { ShikimoriApi(client, interceptor) }
override fun getLogo() = R.drawable.tracker_shikimori
override fun getLogoColor() = Color.rgb(40, 40, 40)
override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING)
}
override fun getStatus(status: Int): String = with(context) {
when (status) {
READING -> getString(R.string.reading)
COMPLETED -> getString(R.string.completed)
ON_HOLD -> getString(R.string.on_hold)
DROPPED -> getString(R.string.dropped)
PLANNING -> getString(R.string.plan_to_read)
REPEATING -> getString(R.string.repeating)
else -> ""
}
}
override fun getScoreList(): List<String> {
return IntRange(0, 10).map(Int::toString)
}
@ -20,10 +48,6 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
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
@ -42,14 +66,12 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
// Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS
add(track)
return api.addLibManga(track, getUsername())
}
return track
}
override suspend fun search(query: String): List<TrackSearch> {
return api.search(query)
}
override suspend fun search(query: String) = api.search(query)
override suspend fun refresh(track: Track): Track {
val remoteTrack = api.findLibManga(track, getUsername())
@ -61,46 +83,6 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
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"
private val gson: Gson by injectLazy()
private val interceptor by lazy { ShikimoriInterceptor(this, gson) }
private val api by lazy { ShikimoriApi(client, interceptor) }
override fun getLogo() = R.drawable.tracker_shikimori
override fun getLogoColor() = Color.rgb(40, 40, 40)
override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING)
}
override fun getStatus(status: Int): String = with(context) {
when (status) {
READING -> getString(R.string.reading)
COMPLETED -> getString(R.string.completed)
ON_HOLD -> getString(R.string.on_hold)
DROPPED -> getString(R.string.dropped)
PLANNING -> getString(R.string.plan_to_read)
REPEATING -> getString(R.string.repeating)
else -> ""
}
}
override suspend fun login(username: String, password: String) = login(password)
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)
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
}
}

@ -22,3 +22,15 @@ fun toTrackStatus(status: String) = when (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)
}

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

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

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

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

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

Loading…
Cancel
Save