mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-23 17:11:54 +01:00
More tracker clean up
This commit is contained in:
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()
|
||||
mangaToUpdate.forEach { manga ->
|
||||
showProgressNotification(manga, count++, mangaToUpdate.size)
|
||||
|
||||
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()
|
||||
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,234 +11,156 @@ 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
|
||||
)
|
||||
val body = payload.toString().toRequestBody(jsonMime)
|
||||
val request = Request.Builder()
|
||||
.url(apiUrl)
|
||||
.post(body)
|
||||
.build()
|
||||
val netResponse = authClient.newCall(request).await()
|
||||
return withContext(Dispatchers.IO) {
|
||||
|
||||
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
|
||||
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()
|
||||
|
||||
return track
|
||||
}
|
||||
val netResponse = authClient.newCall(request).execute()
|
||||
|
||||
suspend fun updateLibManga(track: Track): Track {
|
||||
val query = """
|
||||
|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(
|
||||
"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 = """
|
||||
|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(
|
||||
"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 = """
|
||||
|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()
|
||||
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()
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
netResponse.close()
|
||||
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()
|
||||
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 track = findLibManga(track, userid)
|
||||
if (track == null) {
|
||||
val remoteTrack = findLibManga(track, userid)
|
||||
if (remoteTrack == null) {
|
||||
throw Exception("Could not find manga")
|
||||
} else {
|
||||
return track
|
||||
return remoteTrack
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
val query = """
|
||||
|query User {
|
||||
|Viewer {
|
||||
|id
|
||||
|mediaListOptions {
|
||||
|scoreFormat
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|""".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()
|
||||
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()
|
||||
|
||||
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)
|
||||
|
||||
return JsonParser.parseString(responseBody).obj
|
||||
}
|
||||
|
||||
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()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
@ -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 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)
|
||||
|
||||
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()
|
||||
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())
|
||||
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())
|
||||
|
||||
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
|
||||
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 withContext(Dispatchers.IO) {
|
||||
val response =
|
||||
authClient.newCall(POST(url = exportListUrl(), body = exportPostBody())).execute()
|
||||
|
||||
return baseUrl + Jsoup.parse(response.consumeBody())
|
||||
.select("div.goodresult")
|
||||
.select("a")
|
||||
.attr("href")
|
||||
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 {
|
||||
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
myanimelist.ensureLoggedIn()
|
||||
|
||||
val request = chain.request()
|
||||
var response = chain.proceed(updateRequest(request))
|
||||
|
||||
if (response.code == 400) {
|
||||
myanimelist.refreshLogin()
|
||||
response = chain.proceed(updateRequest(request))
|
||||
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,67 +12,6 @@ import uy.kohesive.injekt.injectLazy
|
||||
|
||||
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"
|
||||
|
||||
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)
|
||||
|
||||
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…
Reference in New Issue
Block a user