mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-12 21:35:18 +01:00
commit
9a044e9037
@ -135,7 +135,6 @@ dependencies {
|
||||
val retrofit_version = "2.7.1"
|
||||
implementation("com.squareup.retrofit2:retrofit:$retrofit_version")
|
||||
implementation("com.squareup.retrofit2:converter-gson:$retrofit_version")
|
||||
implementation("com.squareup.retrofit2:adapter-rxjava:$retrofit_version")
|
||||
|
||||
// JSON
|
||||
implementation("com.google.code.gson:gson:2.8.6")
|
||||
|
@ -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)
|
||||
|
@ -3,11 +3,11 @@ package eu.kanade.tachiyomi.data.track
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||
|
||||
class TrackManager(private val context: Context) {
|
||||
class TrackManager(context: Context) {
|
||||
|
||||
companion object {
|
||||
const val MYANIMELIST = 1
|
||||
@ -17,7 +17,7 @@ class TrackManager(private val context: Context) {
|
||||
const val BANGUMI = 5
|
||||
}
|
||||
|
||||
val myAnimeList = Myanimelist(context, MYANIMELIST)
|
||||
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
||||
|
||||
val aniList = Anilist(context, ANILIST)
|
||||
|
||||
|
@ -7,8 +7,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import okhttp3.OkHttpClient
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class TrackService(val id: Int) {
|
||||
@ -39,17 +37,15 @@ abstract class TrackService(val id: Int) {
|
||||
|
||||
abstract fun displayScore(track: Track): String
|
||||
|
||||
abstract fun add(track: Track): Observable<Track>
|
||||
abstract suspend fun update(track: Track): Track
|
||||
|
||||
abstract fun update(track: Track): Observable<Track>
|
||||
abstract suspend fun bind(track: Track): Track
|
||||
|
||||
abstract fun bind(track: Track): Observable<Track>
|
||||
abstract suspend fun search(query: String): List<TrackSearch>
|
||||
|
||||
abstract fun search(query: String): Observable<List<TrackSearch>>
|
||||
abstract suspend fun refresh(track: Track): Track
|
||||
|
||||
abstract fun refresh(track: Track): Observable<Track>
|
||||
|
||||
abstract fun login(username: String, password: String): Completable
|
||||
abstract suspend fun login(username: String, password: String): Boolean
|
||||
|
||||
@CallSuper
|
||||
open fun logout() {
|
||||
|
@ -8,30 +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 rx.Completable
|
||||
import rx.Observable
|
||||
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()
|
||||
@ -56,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) {
|
||||
@ -95,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
|
||||
@ -114,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 {
|
||||
@ -128,68 +107,62 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
return api.addLibManga(track)
|
||||
}
|
||||
|
||||
override fun update(track: Track): Observable<Track> {
|
||||
override suspend fun update(track: Track): Track {
|
||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||
track.status = COMPLETED
|
||||
}
|
||||
// If user was using API v1 fetch library_id
|
||||
if (track.library_id == null || track.library_id!! == 0L){
|
||||
return api.findLibManga(track, getUsername().toInt()).flatMap {
|
||||
if (it == null) {
|
||||
throw Exception("$track not found on user library")
|
||||
}
|
||||
track.library_id = it.library_id
|
||||
api.updateLibManga(track)
|
||||
}
|
||||
if (track.library_id == null || track.library_id!! == 0L) {
|
||||
val libManga = api.findLibManga(track, getUsername().toInt())
|
||||
?: throw Exception("$track not found on user library")
|
||||
|
||||
track.library_id = libManga.library_id
|
||||
}
|
||||
|
||||
return api.updateLibManga(track)
|
||||
return api.updateLibraryManga(track)
|
||||
}
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
return api.findLibManga(track, getUsername().toInt())
|
||||
.flatMap { remoteTrack ->
|
||||
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)
|
||||
}
|
||||
}
|
||||
override suspend fun bind(track: Track): Track {
|
||||
val remoteTrack = api.findLibManga(track, getUsername().toInt())
|
||||
|
||||
return 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
|
||||
api.addLibManga(track)
|
||||
}
|
||||
}
|
||||
|
||||
override fun search(query: String): Observable<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())
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
return track
|
||||
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
return api.getLibManga(track, getUsername().toInt())
|
||||
.map { remoteTrack ->
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
track
|
||||
}
|
||||
}
|
||||
override suspend fun login(username: String, password: String) = login(password)
|
||||
|
||||
override fun login(username: String, password: String) = login(password)
|
||||
|
||||
fun login(token: String): Completable {
|
||||
suspend fun login(token: String): Boolean {
|
||||
val oauth = api.createOAuth(token)
|
||||
interceptor.setAuth(oauth)
|
||||
return api.getCurrentUser().map { (username, scoreType) ->
|
||||
scorePreference.set(scoreType)
|
||||
saveCredentials(username.toString(), oauth.access_token)
|
||||
}.doOnError{
|
||||
logout()
|
||||
}.toCompletable()
|
||||
|
||||
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() {
|
||||
@ -206,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,25 +11,209 @@ 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.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.jsonType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import rx.Observable
|
||||
import okhttp3.Response
|
||||
import java.util.Calendar
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
private val parser = JsonParser()
|
||||
private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
fun addLibManga(track: Track): Observable<Track> {
|
||||
val query = """
|
||||
suspend fun addLibManga(track: Track): Track {
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
@ -37,36 +221,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|}
|
||||
|}
|
||||
|""".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()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
netResponse.close()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = parser.parse(responseBody).obj
|
||||
track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track): Observable<Track> {
|
||||
val query = """
|
||||
fun updateInLibraryQuery() = """
|
||||
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|
||||
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|
||||
|id
|
||||
@ -75,30 +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()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
fun search(search: String): Observable<List<TrackSearch>> {
|
||||
val query = """
|
||||
fun searchQuery() = """
|
||||
|query Search(${'$'}query: String) {
|
||||
|Page (perPage: 50) {
|
||||
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
||||
@ -122,37 +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()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = parser.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) }
|
||||
entries.map { it.toTrack() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun findLibManga(track: Track, userid: Int): Observable<Track?> {
|
||||
val query = """
|
||||
fun findLibraryMangaQuery() = """
|
||||
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
||||
|Page {
|
||||
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
||||
@ -182,47 +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()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = parser.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()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibManga(track: Track, userid: Int): Observable<Track> {
|
||||
return findLibManga(track, userid)
|
||||
.map { it ?: throw Exception("Could not find manga") }
|
||||
}
|
||||
|
||||
fun createOAuth(token: String): OAuth {
|
||||
return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
|
||||
}
|
||||
|
||||
fun getCurrentUser(): Observable<Pair<Int, String>> {
|
||||
val query = """
|
||||
fun currentUserQuery() = """
|
||||
|query User {
|
||||
|Viewer {
|
||||
|id
|
||||
@ -232,62 +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()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = parser.parse(responseBody).obj
|
||||
val data = response["data"]!!.obj
|
||||
val viewer = data["Viewer"].obj
|
||||
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 clientUrl = "tachiyomi://anilist-auth"
|
||||
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
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
data class Avatar(
|
||||
val large: String? = "",
|
||||
val medium: String? = "",
|
||||
val small: String? = ""
|
||||
)
|
@ -7,8 +7,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
||||
@ -29,55 +28,44 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
||||
return track.score.toInt().toString()
|
||||
}
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
return api.addLibManga(track)
|
||||
}
|
||||
|
||||
override fun update(track: Track): Observable<Track> {
|
||||
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)
|
||||
}
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
return api.statusLibManga(track)
|
||||
.flatMap {
|
||||
api.findLibManga(track).flatMap { remoteTrack ->
|
||||
if (remoteTrack != null && it != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.library_id = remoteTrack.library_id
|
||||
track.status = remoteTrack.status
|
||||
track.last_chapter_read = remoteTrack.last_chapter_read
|
||||
refresh(track)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
track.score = DEFAULT_SCORE.toFloat()
|
||||
track.status = DEFAULT_STATUS
|
||||
add(track)
|
||||
update(track)
|
||||
}
|
||||
}
|
||||
}
|
||||
override suspend fun bind(track: Track): Track {
|
||||
val statusTrack = api.statusLibManga(track)
|
||||
val remoteTrack = api.findLibManga(track)
|
||||
if (statusTrack != null && remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.library_id = remoteTrack.library_id
|
||||
track.status = remoteTrack.status
|
||||
track.last_chapter_read = remoteTrack.last_chapter_read
|
||||
refresh(track)
|
||||
} else {
|
||||
track.score = DEFAULT_SCORE.toFloat()
|
||||
track.status = DEFAULT_STATUS
|
||||
api.addLibManga(track)
|
||||
update(track)
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
||||
override suspend fun search(query: String): List<TrackSearch> {
|
||||
return api.search(query)
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
return api.statusLibManga(track)
|
||||
.flatMap {
|
||||
track.copyPersonalFrom(it!!)
|
||||
api.findLibManga(track)
|
||||
.map { remoteTrack ->
|
||||
if (remoteTrack != null) {
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
track.status = remoteTrack.status
|
||||
}
|
||||
track
|
||||
}
|
||||
}
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
val statusTrack = api.statusLibManga(track)
|
||||
track.copyPersonalFrom(statusTrack!!)
|
||||
val remoteTrack = api.findLibManga(track)
|
||||
if(remoteTrack != null){
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
track.status = remoteTrack.status
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
override fun getLogo() = R.drawable.tracker_bangumi
|
||||
@ -99,17 +87,20 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun login(username: String, password: String) = login(password)
|
||||
override suspend fun login(username: String, password: String): Boolean = login(password)
|
||||
|
||||
fun login(code: String): Completable {
|
||||
return api.accessToken(code).map { oauth: OAuth? ->
|
||||
suspend fun login(code: String): Boolean {
|
||||
try {
|
||||
|
||||
val oauth = api.accessToken(code)
|
||||
interceptor.newAuth(oauth)
|
||||
if (oauth != null) {
|
||||
saveCredentials(oauth.user_id.toString(), oauth.access_token)
|
||||
}
|
||||
}.doOnError {
|
||||
saveCredentials(oauth.user_id.toString(), oauth.access_token)
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
logout()
|
||||
}.toCompletable()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun saveToken(oauth: OAuth?) {
|
||||
@ -128,15 +119,15 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
||||
override fun logout() {
|
||||
super.logout()
|
||||
preferences.trackToken(this).set(null)
|
||||
interceptor.newAuth(null)
|
||||
interceptor.clearOauth()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val READING = 3
|
||||
const val PLANNING = 1
|
||||
const val COMPLETED = 2
|
||||
const val READING = 3
|
||||
const val ON_HOLD = 4
|
||||
const val DROPPED = 5
|
||||
const val PLANNING = 1
|
||||
|
||||
const val DEFAULT_STATUS = READING
|
||||
const val DEFAULT_SCORE = 0
|
||||
|
@ -10,91 +10,86 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
|
||||
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
|
||||
|
||||
private val gson: Gson by injectLazy()
|
||||
private val parser = JsonParser()
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
fun addLibManga(track: Track): Observable<Track> {
|
||||
suspend fun addLibManga(track: Track): Track {
|
||||
val body = FormBody.Builder()
|
||||
.add("rating", track.score.toInt().toString())
|
||||
.add("status", track.toBangumiStatus())
|
||||
.build()
|
||||
.add("rating", track.score.toInt().toString())
|
||||
.add("status", track.toBangumiStatus())
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url("$apiUrl/collection/${track.media_id}/update")
|
||||
.post(body)
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
}
|
||||
.url("$apiUrl/collection/${track.media_id}/update")
|
||||
.post(body)
|
||||
.build()
|
||||
val response = authClient.newCall(request).await()
|
||||
return track
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track): Observable<Track> {
|
||||
suspend fun updateLibManga(track: Track): Track {
|
||||
// chapter update
|
||||
val body = FormBody.Builder()
|
||||
return withContext(Dispatchers.IO) {
|
||||
val body = FormBody.Builder()
|
||||
.add("watched_eps", track.last_chapter_read.toString())
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
val request = Request.Builder()
|
||||
.url("$apiUrl/subject/${track.media_id}/update/watched_eps")
|
||||
.post(body)
|
||||
.build()
|
||||
|
||||
// read status update
|
||||
val sbody = FormBody.Builder()
|
||||
// read status update
|
||||
val sbody = FormBody.Builder()
|
||||
.add("status", track.toBangumiStatus())
|
||||
.build()
|
||||
val srequest = Request.Builder()
|
||||
val srequest = Request.Builder()
|
||||
.url("$apiUrl/collection/${track.media_id}/update")
|
||||
.post(sbody)
|
||||
.build()
|
||||
return authClient.newCall(srequest)
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
}.flatMap {
|
||||
authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
}
|
||||
}
|
||||
authClient.newCall(srequest).execute()
|
||||
authClient.newCall(request).execute()
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
fun search(search: String): Observable<List<TrackSearch>> {
|
||||
val url = Uri.parse(
|
||||
"$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}").buildUpon()
|
||||
suspend fun search(search: String): List<TrackSearch> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val url = Uri.parse(
|
||||
"$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
|
||||
).buildUpon()
|
||||
.appendQueryParameter("max_results", "20")
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
val request = Request.Builder()
|
||||
.url(url.toString())
|
||||
.get()
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
var responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
if (responseBody.contains("\"code\":404")) {
|
||||
responseBody = "{\"results\":0,\"list\":[]}"
|
||||
}
|
||||
val response = parser.parse(responseBody).obj["list"]?.array
|
||||
response?.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) }
|
||||
}
|
||||
|
||||
val netResponse = authClient.newCall(request).await()
|
||||
var responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
if (responseBody.contains("\"code\":404")) {
|
||||
responseBody = "{\"results\":0,\"list\":[]}"
|
||||
}
|
||||
val response = JsonParser.parseString(responseBody).obj["list"]?.array
|
||||
if (response != null) {
|
||||
response.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) }
|
||||
} else {
|
||||
listOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||
@ -119,60 +114,56 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
}
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track): Observable<Track?> {
|
||||
val urlMangas = "$apiUrl/subject/${track.media_id}"
|
||||
val requestMangas = Request.Builder()
|
||||
suspend fun findLibManga(track: Track): Track? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val urlMangas = "$apiUrl/subject/${track.media_id}"
|
||||
val requestMangas = Request.Builder()
|
||||
.url(urlMangas)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
return authClient.newCall(requestMangas)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
// get comic info
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
jsonToTrack(parser.parse(responseBody).obj)
|
||||
}
|
||||
val netResponse = authClient.newCall(requestMangas).execute()
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
jsonToTrack(JsonParser.parseString(responseBody).obj)
|
||||
}
|
||||
}
|
||||
|
||||
fun statusLibManga(track: Track): Observable<Track?> {
|
||||
suspend fun statusLibManga(track: Track): Track? {
|
||||
val urlUserRead = "$apiUrl/collection/${track.media_id}"
|
||||
val requestUserRead = Request.Builder()
|
||||
.url(urlUserRead)
|
||||
.cacheControl(CacheControl.FORCE_NETWORK)
|
||||
.get()
|
||||
.build()
|
||||
.url(urlUserRead)
|
||||
.cacheControl(CacheControl.FORCE_NETWORK)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
// todo get user readed chapter here
|
||||
return authClient.newCall(requestUserRead)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val resp = netResponse.body?.string()
|
||||
val coll = gson.fromJson(resp, Collection::class.java)
|
||||
track.status = coll.status?.id!!
|
||||
track.last_chapter_read = coll.ep_status!!
|
||||
track
|
||||
}
|
||||
val response = authClient.newCall(requestUserRead).await()
|
||||
val resp = response.body?.toString()
|
||||
val coll = gson.fromJson(resp, Collection::class.java)
|
||||
track.status = coll.status?.id!!
|
||||
track.last_chapter_read = coll.ep_status!!
|
||||
return track
|
||||
}
|
||||
|
||||
fun accessToken(code: String): Observable<OAuth> {
|
||||
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
|
||||
suspend fun accessToken(code: String): OAuth {
|
||||
return withContext(Dispatchers.IO){
|
||||
val netResponse = client.newCall(accessTokenRequest(code)).execute()
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
if(responseBody.isEmpty()){
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
gson.fromJson(responseBody, OAuth::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private fun accessTokenRequest(code: String) = POST(oauthUrl,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "authorization_code")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("code", code)
|
||||
.add("redirect_uri", redirectUrl)
|
||||
.build()
|
||||
private fun accessTokenRequest(code: String) = POST(
|
||||
oauthUrl,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "authorization_code")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("code", code)
|
||||
.add("redirect_uri", redirectUrl)
|
||||
.build()
|
||||
)
|
||||
|
||||
companion object {
|
||||
@ -192,20 +183,21 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
}
|
||||
|
||||
fun authUrl() =
|
||||
Uri.parse(loginUrl).buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||
.build()
|
||||
Uri.parse(loginUrl).buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||
.build()
|
||||
|
||||
fun refreshTokenRequest(token: String) = POST(oauthUrl,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("refresh_token", token)
|
||||
.add("redirect_uri", redirectUrl)
|
||||
.build())
|
||||
fun refreshTokenRequest(token: String) = POST(
|
||||
oauthUrl,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("refresh_token", token)
|
||||
.add("redirect_uri", redirectUrl)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -47,8 +47,8 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
|
||||
return chain.proceed(authRequest)
|
||||
}
|
||||
|
||||
fun newAuth(oauth: OAuth?) {
|
||||
this.oauth = if (oauth == null) null else OAuth(
|
||||
fun newAuth(oauth: OAuth) {
|
||||
this.oauth = OAuth(
|
||||
oauth.access_token,
|
||||
oauth.token_type,
|
||||
System.currentTimeMillis() / 1000,
|
||||
@ -58,4 +58,8 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
|
||||
|
||||
bangumi.saveToken(oauth)
|
||||
}
|
||||
|
||||
fun clearOauth(){
|
||||
bangumi.saveToken(null)
|
||||
}
|
||||
}
|
||||
|
@ -11,3 +11,39 @@ data class Collection(
|
||||
val user: User? = User(),
|
||||
val vol_status: Int? = 0
|
||||
)
|
||||
|
||||
data class OAuth(
|
||||
val access_token: String,
|
||||
val token_type: String,
|
||||
val created_at: Long,
|
||||
val expires_in: Long,
|
||||
val refresh_token: String?,
|
||||
val user_id: Long?
|
||||
) {
|
||||
// Access token refresh before expired
|
||||
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
|
||||
}
|
||||
|
||||
data class Status(
|
||||
val id: Int? = 0,
|
||||
val name: String? = "",
|
||||
val type: String? = ""
|
||||
)
|
||||
|
||||
data class User(
|
||||
val avatar: Avatar? = Avatar(),
|
||||
val id: Int? = 0,
|
||||
val nickname: String? = "",
|
||||
val sign: String? = "",
|
||||
val url: String? = "",
|
||||
val usergroup: Int? = 0,
|
||||
val username: String? = ""
|
||||
)
|
||||
|
||||
data class Avatar(
|
||||
val large: String? = "",
|
||||
val medium: String? = "",
|
||||
val small: String? = ""
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,16 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
data class OAuth(
|
||||
val access_token: String,
|
||||
val token_type: String,
|
||||
val created_at: Long,
|
||||
val expires_in: Long,
|
||||
val refresh_token: String?,
|
||||
val user_id: Long?
|
||||
) {
|
||||
|
||||
// Access token refresh before expired
|
||||
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
|
||||
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
data class Status(
|
||||
val id: Int? = 0,
|
||||
val name: String? = "",
|
||||
val type: String? = ""
|
||||
)
|
@ -1,11 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
data class User(
|
||||
val avatar: Avatar? = Avatar(),
|
||||
val id: Int? = 0,
|
||||
val nickname: String? = "",
|
||||
val sign: String? = "",
|
||||
val url: String? = "",
|
||||
val usergroup: Int? = 0,
|
||||
val username: String? = ""
|
||||
)
|
@ -7,8 +7,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.DecimalFormat
|
||||
|
||||
@ -70,11 +69,7 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||
return df.format(track.score)
|
||||
}
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
return api.addLibManga(track, getUserId())
|
||||
}
|
||||
|
||||
override fun update(track: Track): Observable<Track> {
|
||||
override suspend fun update(track: Track): Track {
|
||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||
track.status = COMPLETED
|
||||
}
|
||||
@ -82,41 +77,41 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||
return api.updateLibManga(track)
|
||||
}
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
return api.findLibManga(track, getUserId())
|
||||
.flatMap { remoteTrack ->
|
||||
if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.media_id = remoteTrack.media_id
|
||||
update(track)
|
||||
} else {
|
||||
track.score = DEFAULT_SCORE
|
||||
track.status = DEFAULT_STATUS
|
||||
add(track)
|
||||
}
|
||||
}
|
||||
override suspend fun bind(track: Track): Track {
|
||||
val remoteTrack = api.findLibManga(track, getUserId())
|
||||
if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.media_id = remoteTrack.media_id
|
||||
return update(track)
|
||||
} else {
|
||||
track.score = DEFAULT_SCORE
|
||||
track.status = DEFAULT_STATUS
|
||||
return api.addLibManga(track, getUserId())
|
||||
}
|
||||
}
|
||||
|
||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
||||
override suspend fun search(query: String): List<TrackSearch> {
|
||||
return api.search(query)
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
return api.getLibManga(track)
|
||||
.map { remoteTrack ->
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
track
|
||||
}
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
val remoteTrack = api.getLibManga(track)
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
return track
|
||||
}
|
||||
|
||||
override fun login(username: String, password: String): Completable {
|
||||
return api.login(username, password)
|
||||
.doOnNext { interceptor.newAuth(it) }
|
||||
.flatMap { api.getCurrentUser() }
|
||||
.doOnNext { userId -> saveCredentials(username, userId) }
|
||||
.doOnError { logout() }
|
||||
.toCompletable()
|
||||
override suspend fun login(username: String, password: String): Boolean {
|
||||
try {
|
||||
val oauth = api.login(username, password)
|
||||
interceptor.newAuth(oauth)
|
||||
val userId = api.getCurrentUser()
|
||||
saveCredentials(username, userId)
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
@ -140,5 +135,4 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,11 @@
|
||||
package eu.kanade.tachiyomi.data.track.kitsu
|
||||
|
||||
import com.github.salomonbrys.kotson.*
|
||||
import com.github.salomonbrys.kotson.array
|
||||
import com.github.salomonbrys.kotson.get
|
||||
import com.github.salomonbrys.kotson.int
|
||||
import com.github.salomonbrys.kotson.jsonObject
|
||||
import com.github.salomonbrys.kotson.obj
|
||||
import com.github.salomonbrys.kotson.string
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonObject
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
@ -9,240 +14,228 @@ import eu.kanade.tachiyomi.network.POST
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.*
|
||||
import rx.Observable
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.PATCH
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
|
||||
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
private val rest = Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.client(authClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().serializeNulls().create()))
|
||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||
.build()
|
||||
.create(KitsuApi.Rest::class.java)
|
||||
.baseUrl(baseUrl)
|
||||
.client(authClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().serializeNulls().create()))
|
||||
.build()
|
||||
.create(KitsuApi.Rest::class.java)
|
||||
|
||||
private val searchRest = Retrofit.Builder()
|
||||
.baseUrl(algoliaKeyUrl)
|
||||
.client(authClient)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||
.build()
|
||||
.create(KitsuApi.SearchKeyRest::class.java)
|
||||
.baseUrl(algoliaKeyUrl)
|
||||
.client(authClient)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(KitsuApi.SearchKeyRest::class.java)
|
||||
|
||||
private val algoliaRest = Retrofit.Builder()
|
||||
.baseUrl(algoliaUrl)
|
||||
.baseUrl(algoliaUrl)
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(KitsuApi.AgoliaSearchRest::class.java)
|
||||
|
||||
suspend fun addLibManga(track: Track, userId: String): Track {
|
||||
// @formatter:off
|
||||
val data = jsonObject(
|
||||
"type" to "libraryEntries",
|
||||
"attributes" to jsonObject(
|
||||
"status" to track.toKitsuStatus(),
|
||||
"progress" to track.last_chapter_read
|
||||
),
|
||||
"relationships" to jsonObject(
|
||||
"user" to jsonObject(
|
||||
"data" to jsonObject(
|
||||
"id" to userId,
|
||||
"type" to "users"
|
||||
)
|
||||
),
|
||||
"media" to jsonObject(
|
||||
"data" to jsonObject(
|
||||
"id" to track.media_id,
|
||||
"type" to "manga"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val json = rest.addLibManga(jsonObject("data" to data))
|
||||
track.media_id = json["data"]["id"].int
|
||||
return track
|
||||
}
|
||||
|
||||
suspend fun updateLibManga(track: Track): Track {
|
||||
// @formatter:off
|
||||
val data = jsonObject(
|
||||
"type" to "libraryEntries",
|
||||
"id" to track.media_id,
|
||||
"attributes" to jsonObject(
|
||||
"status" to track.toKitsuStatus(),
|
||||
"progress" to track.last_chapter_read,
|
||||
"ratingTwenty" to track.toKitsuScore()
|
||||
)
|
||||
)
|
||||
// @formatter:on
|
||||
|
||||
rest.updateLibManga(track.media_id, jsonObject("data" to data))
|
||||
return track
|
||||
}
|
||||
|
||||
suspend fun search(query: String): List<TrackSearch> {
|
||||
val key = searchRest.getKey()["media"].asJsonObject["key"].string
|
||||
return algoliaSearch(key, query)
|
||||
}
|
||||
|
||||
private suspend fun algoliaSearch(key: String, query: String): List<TrackSearch> {
|
||||
val jsonObject = jsonObject("params" to "query=$query$algoliaFilter")
|
||||
val json = algoliaRest.getSearchQuery(algoliaAppId, key, jsonObject)
|
||||
val data = json["hits"].array
|
||||
return data.map { KitsuSearchManga(it.obj) }
|
||||
.filter { it.subType != "novel" }
|
||||
.map { it.toTrack() }
|
||||
}
|
||||
|
||||
suspend fun findLibManga(track: Track, userId: String): Track? {
|
||||
val json = rest.findLibManga(track.media_id, userId)
|
||||
val data = json["data"].array
|
||||
return if (data.size() > 0) {
|
||||
val manga = json["included"].array[0].obj
|
||||
KitsuLibManga(data[0].obj, manga).toTrack()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getLibManga(track: Track): Track {
|
||||
val json = rest.getLibManga(track.media_id)
|
||||
val data = json["data"].array
|
||||
if (data.size() > 0) {
|
||||
val manga = json["included"].array[0].obj
|
||||
return KitsuLibManga(data[0].obj, manga).toTrack()
|
||||
} else {
|
||||
throw Exception("Could not find manga")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun login(username: String, password: String): OAuth {
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(loginUrl)
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||
.build()
|
||||
.create(KitsuApi.AgoliaSearchRest::class.java)
|
||||
|
||||
fun addLibManga(track: Track, userId: String): Observable<Track> {
|
||||
return Observable.defer {
|
||||
// @formatter:off
|
||||
val data = jsonObject(
|
||||
"type" to "libraryEntries",
|
||||
"attributes" to jsonObject(
|
||||
"status" to track.toKitsuStatus(),
|
||||
"progress" to track.last_chapter_read
|
||||
),
|
||||
"relationships" to jsonObject(
|
||||
"user" to jsonObject(
|
||||
"data" to jsonObject(
|
||||
"id" to userId,
|
||||
"type" to "users"
|
||||
)
|
||||
),
|
||||
"media" to jsonObject(
|
||||
"data" to jsonObject(
|
||||
"id" to track.media_id,
|
||||
"type" to "manga"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
rest.addLibManga(jsonObject("data" to data))
|
||||
.map { json ->
|
||||
track.media_id = json["data"]["id"].int
|
||||
track
|
||||
}
|
||||
}
|
||||
.create(KitsuApi.LoginRest::class.java)
|
||||
.requestAccessToken(username, password)
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track): Observable<Track> {
|
||||
return Observable.defer {
|
||||
// @formatter:off
|
||||
val data = jsonObject(
|
||||
"type" to "libraryEntries",
|
||||
"id" to track.media_id,
|
||||
"attributes" to jsonObject(
|
||||
"status" to track.toKitsuStatus(),
|
||||
"progress" to track.last_chapter_read,
|
||||
"ratingTwenty" to track.toKitsuScore()
|
||||
)
|
||||
)
|
||||
// @formatter:on
|
||||
|
||||
rest.updateLibManga(track.media_id, jsonObject("data" to data))
|
||||
.map { track }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun search(query: String): Observable<List<TrackSearch>> {
|
||||
return searchRest
|
||||
.getKey().map { json ->
|
||||
json["media"].asJsonObject["key"].string
|
||||
}.flatMap { key ->
|
||||
algoliaSearch(key, query)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun algoliaSearch(key: String, query: String): Observable<List<TrackSearch>> {
|
||||
val jsonObject = jsonObject("params" to "query=$query$algoliaFilter")
|
||||
return algoliaRest
|
||||
.getSearchQuery(algoliaAppId, key, jsonObject)
|
||||
.map { json ->
|
||||
val data = json["hits"].array
|
||||
data.map { KitsuSearchManga(it.obj) }
|
||||
.filter { it.subType != "novel" }
|
||||
.map { it.toTrack() }
|
||||
}
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track, userId: String): Observable<Track?> {
|
||||
return rest.findLibManga(track.media_id, userId)
|
||||
.map { json ->
|
||||
val data = json["data"].array
|
||||
if (data.size() > 0) {
|
||||
val manga = json["included"].array[0].obj
|
||||
KitsuLibManga(data[0].obj, manga).toTrack()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibManga(track: Track): Observable<Track> {
|
||||
return rest.getLibManga(track.media_id)
|
||||
.map { json ->
|
||||
val data = json["data"].array
|
||||
if (data.size() > 0) {
|
||||
val manga = json["included"].array[0].obj
|
||||
KitsuLibManga(data[0].obj, manga).toTrack()
|
||||
} else {
|
||||
throw Exception("Could not find manga")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun login(username: String, password: String): Observable<OAuth> {
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(loginUrl)
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||
.build()
|
||||
.create(KitsuApi.LoginRest::class.java)
|
||||
.requestAccessToken(username, password)
|
||||
}
|
||||
|
||||
fun getCurrentUser(): Observable<String> {
|
||||
return rest.getCurrentUser().map { it["data"].array[0]["id"].string }
|
||||
suspend fun getCurrentUser(): String {
|
||||
val currentUser = rest.getCurrentUser()
|
||||
return currentUser["data"].array[0]["id"].string
|
||||
}
|
||||
|
||||
private interface Rest {
|
||||
|
||||
@Headers("Content-Type: application/vnd.api+json")
|
||||
@POST("library-entries")
|
||||
fun addLibManga(
|
||||
@Body data: JsonObject
|
||||
): Observable<JsonObject>
|
||||
suspend fun addLibManga(
|
||||
@Body data: JsonObject
|
||||
): JsonObject
|
||||
|
||||
@Headers("Content-Type: application/vnd.api+json")
|
||||
@PATCH("library-entries/{id}")
|
||||
fun updateLibManga(
|
||||
@Path("id") remoteId: Int,
|
||||
@Body data: JsonObject
|
||||
): Observable<JsonObject>
|
||||
|
||||
suspend fun updateLibManga(
|
||||
@Path("id") remoteId: Int,
|
||||
@Body data: JsonObject
|
||||
): JsonObject
|
||||
|
||||
@GET("library-entries")
|
||||
fun findLibManga(
|
||||
@Query("filter[manga_id]", encoded = true) remoteId: Int,
|
||||
@Query("filter[user_id]", encoded = true) userId: String,
|
||||
@Query("include") includes: String = "manga"
|
||||
): Observable<JsonObject>
|
||||
suspend fun findLibManga(
|
||||
@Query("filter[manga_id]", encoded = true) remoteId: Int,
|
||||
@Query("filter[user_id]", encoded = true) userId: String,
|
||||
@Query("include") includes: String = "manga"
|
||||
): JsonObject
|
||||
|
||||
@GET("library-entries")
|
||||
fun getLibManga(
|
||||
@Query("filter[id]", encoded = true) remoteId: Int,
|
||||
@Query("include") includes: String = "manga"
|
||||
): Observable<JsonObject>
|
||||
suspend fun getLibManga(
|
||||
@Query("filter[id]", encoded = true) remoteId: Int,
|
||||
@Query("include") includes: String = "manga"
|
||||
): JsonObject
|
||||
|
||||
@GET("users")
|
||||
fun getCurrentUser(
|
||||
@Query("filter[self]", encoded = true) self: Boolean = true
|
||||
): Observable<JsonObject>
|
||||
|
||||
suspend fun getCurrentUser(
|
||||
@Query("filter[self]", encoded = true) self: Boolean = true
|
||||
): JsonObject
|
||||
}
|
||||
|
||||
private interface SearchKeyRest {
|
||||
@GET("media/")
|
||||
fun getKey(): Observable<JsonObject>
|
||||
suspend fun getKey(): JsonObject
|
||||
}
|
||||
|
||||
private interface AgoliaSearchRest {
|
||||
@POST("query/")
|
||||
fun getSearchQuery(@Header("X-Algolia-Application-Id") appid: String, @Header("X-Algolia-API-Key") key: String, @Body json: JsonObject): Observable<JsonObject>
|
||||
suspend fun getSearchQuery(
|
||||
@Header("X-Algolia-Application-Id") appid: String,
|
||||
@Header("X-Algolia-API-Key") key: String,
|
||||
@Body json: JsonObject
|
||||
): JsonObject
|
||||
}
|
||||
|
||||
private interface LoginRest {
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("oauth/token")
|
||||
fun requestAccessToken(
|
||||
@Field("username") username: String,
|
||||
@Field("password") password: String,
|
||||
@Field("grant_type") grantType: String = "password",
|
||||
@Field("client_id") client_id: String = clientId,
|
||||
@Field("client_secret") client_secret: String = clientSecret
|
||||
): Observable<OAuth>
|
||||
|
||||
suspend fun requestAccessToken(
|
||||
@Field("username") username: String,
|
||||
@Field("password") password: String,
|
||||
@Field("grant_type") grantType: String = "password",
|
||||
@Field("client_id") client_id: String = clientId,
|
||||
@Field("client_secret") client_secret: String = clientSecret
|
||||
): OAuth
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val clientId = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
||||
private const val clientSecret = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
||||
private const val clientId =
|
||||
"dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
||||
private const val clientSecret =
|
||||
"54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
||||
private const val baseUrl = "https://kitsu.io/api/edge/"
|
||||
private const val loginUrl = "https://kitsu.io/api/"
|
||||
private const val baseMangaUrl = "https://kitsu.io/manga/"
|
||||
private const val algoliaKeyUrl = "https://kitsu.io/api/edge/algolia-keys/"
|
||||
private const val algoliaUrl = "https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/"
|
||||
private const val algoliaUrl =
|
||||
"https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/"
|
||||
private const val algoliaAppId = "AWQO5J657S"
|
||||
private const val algoliaFilter = "&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
|
||||
|
||||
private const val algoliaFilter =
|
||||
"&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
|
||||
|
||||
fun mangaUrl(remoteId: Int): String {
|
||||
return baseMangaUrl + remoteId
|
||||
}
|
||||
|
||||
|
||||
fun refreshTokenRequest(token: String) = POST("${loginUrl}oauth/token",
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("refresh_token", token)
|
||||
.build())
|
||||
|
||||
fun refreshTokenRequest(token: String) = POST(
|
||||
"${loginUrl}oauth/token",
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("refresh_token", token)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,33 +7,15 @@ 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 okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
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
|
||||
|
||||
@ -62,11 +44,7 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
||||
return track.score.toInt().toString()
|
||||
}
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
return api.addLibManga(track)
|
||||
}
|
||||
|
||||
override fun update(track: Track): Observable<Track> {
|
||||
override suspend fun update(track: Track): Track {
|
||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||
track.status = COMPLETED
|
||||
}
|
||||
@ -74,45 +52,46 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
||||
return api.updateLibManga(track)
|
||||
}
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
return api.findLibManga(track)
|
||||
.flatMap { remoteTrack ->
|
||||
if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
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)
|
||||
}
|
||||
}
|
||||
override suspend fun bind(track: Track): Track {
|
||||
val remoteTrack = api.findLibManga(track)
|
||||
if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
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)
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
||||
override suspend fun search(query: String): List<TrackSearch> {
|
||||
return api.search(query)
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
return api.getLibManga(track)
|
||||
.map { remoteTrack ->
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
track
|
||||
}
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
val remoteTrack = api.getLibManga(track)
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
return track
|
||||
}
|
||||
|
||||
override fun login(username: String, password: String): Completable {
|
||||
override suspend fun login(username: String, password: String): Boolean {
|
||||
logout()
|
||||
|
||||
return Observable.fromCallable { api.login(username, password) }
|
||||
.doOnNext { csrf -> saveCSRF(csrf) }
|
||||
.doOnNext { saveCredentials(username, password) }
|
||||
.doOnError { logout() }
|
||||
.toCompletable()
|
||||
return try {
|
||||
val csrf = api.login(username, password)
|
||||
saveCSRF(csrf)
|
||||
saveCredentials(username, password)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
logout()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshLogin() {
|
||||
private suspend fun refreshLogin() {
|
||||
val username = getUsername()
|
||||
val password = getPassword()
|
||||
logout()
|
||||
@ -122,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")
|
||||
|
||||
@ -141,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()
|
||||
|
||||
@ -161,4 +138,18 @@ 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"
|
||||
}
|
||||
}
|
||||
|
@ -6,50 +6,41 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservable
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
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
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import org.json.JSONObject
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.parser.Parser
|
||||
import rx.Observable
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
|
||||
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
|
||||
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
fun search(query: String): Observable<List<TrackSearch>> {
|
||||
return if (query.startsWith(PREFIX_MY)) {
|
||||
val realQuery = query.removePrefix(PREFIX_MY)
|
||||
getList()
|
||||
.flatMap { Observable.from(it) }
|
||||
.filter { it.title.contains(realQuery, true) }
|
||||
.toList()
|
||||
} else {
|
||||
client.newCall(GET(searchUrl(query)))
|
||||
.asObservable()
|
||||
.flatMap { response ->
|
||||
Observable.from(Jsoup.parse(response.consumeBody())
|
||||
.select("div.js-categories-seasonal.js-block-list.list")
|
||||
.select("table").select("tbody")
|
||||
.select("tr").drop(1))
|
||||
}
|
||||
.filter { row ->
|
||||
row.select(TD)[2].text() != "Novel"
|
||||
}
|
||||
suspend fun search(query: String): List<TrackSearch> {
|
||||
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()
|
||||
@ -64,136 +55,119 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addLibManga(track: Track): Observable<Track> {
|
||||
return Observable.defer {
|
||||
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track)))
|
||||
.asObservableSuccess()
|
||||
.map { track }
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track): Observable<Track> {
|
||||
return Observable.defer {
|
||||
authClient.newCall(POST(url = updateUrl(), body = mangaPostPayload(track)))
|
||||
.asObservableSuccess()
|
||||
.map { track }
|
||||
}
|
||||
suspend fun addLibManga(track: Track): Track {
|
||||
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track))).await()
|
||||
return track
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track): Observable<Track?> {
|
||||
return authClient.newCall(GET(url = listEntryUrl(track.media_id)))
|
||||
.asObservable()
|
||||
.map {response ->
|
||||
var libTrack: Track? = null
|
||||
response.use {
|
||||
if (it.priorResponse?.isRedirect != true) {
|
||||
val trackForm = Jsoup.parse(it.consumeBody())
|
||||
suspend fun updateLibManga(track: Track): Track {
|
||||
authClient.newCall(POST(url = updateUrl(), body = mangaPostPayload(track))).await()
|
||||
return track
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
suspend fun findLibManga(track: Track): Track? {
|
||||
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
|
||||
}
|
||||
libTrack
|
||||
}
|
||||
}
|
||||
remoteTrack
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibManga(track: Track): Observable<Track> {
|
||||
return findLibManga(track)
|
||||
.map { it ?: throw Exception("Could not find manga") }
|
||||
suspend fun getLibManga(track: Track): Track {
|
||||
val result = findLibManga(track)
|
||||
if (result == null) {
|
||||
throw Exception("Could not find manga")
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
.select("meta[name=csrf_token]")
|
||||
.attr("content")
|
||||
.select("meta[name=csrf_token]")
|
||||
.attr("content")
|
||||
}
|
||||
|
||||
private fun login(username: String, password: String, csrf: String) {
|
||||
val response = client.newCall(POST(url = loginUrl(), body = loginPostBody(username, password, csrf))).execute()
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getList(): Observable<List<TrackSearch>> {
|
||||
return getListUrl()
|
||||
.flatMap { url ->
|
||||
getListXml(url)
|
||||
}
|
||||
.flatMap { doc ->
|
||||
Observable.from(doc.select("manga"))
|
||||
}
|
||||
.map {
|
||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||
title = it.selectText("manga_title")!!
|
||||
media_id = it.selectInt("manga_mangadb_id")
|
||||
last_chapter_read = it.selectInt("my_read_chapters")
|
||||
status = getStatus(it.selectText("my_status")!!)
|
||||
score = it.selectInt("my_score").toFloat()
|
||||
total_chapters = it.selectInt("manga_chapters")
|
||||
tracking_url = mangaUrl(media_id)
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun getListUrl(): Observable<String> {
|
||||
return authClient.newCall(POST(url = exportListUrl(), body = exportPostBody()))
|
||||
.asObservable()
|
||||
.map {response ->
|
||||
baseUrl + Jsoup.parse(response.consumeBody())
|
||||
.select("div.goodresult")
|
||||
.select("a")
|
||||
.attr("href")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getListXml(url: String): Observable<Document> {
|
||||
return authClient.newCall(GET(url))
|
||||
.asObservable()
|
||||
.map { response ->
|
||||
Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Response.consumeBody(): String? {
|
||||
use {
|
||||
if (it.code != 200) throw Exception("HTTP error ${it.code}")
|
||||
return it.body?.string()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Response.consumeXmlBody(): String? {
|
||||
use { res ->
|
||||
if (res.code != 200) throw Exception("Export list error")
|
||||
BufferedReader(InputStreamReader(GZIPInputStream(res.body?.source()?.inputStream()))).use { reader ->
|
||||
val sb = StringBuilder()
|
||||
reader.forEachLine { line ->
|
||||
sb.append(line)
|
||||
}
|
||||
return sb.toString()
|
||||
response.use {
|
||||
if (response.priorResponse?.code != 302) throw Exception("Authentication error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getList(): List<TrackSearch> {
|
||||
val results = getListXml(getListUrl()).select("manga")
|
||||
|
||||
return results.map {
|
||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||
title = it.selectText("manga_title")!!
|
||||
media_id = it.selectInt("manga_mangadb_id")
|
||||
last_chapter_read = it.selectInt("my_read_chapters")
|
||||
status = getStatus(it.selectText("my_status")!!)
|
||||
score = it.selectInt("my_score").toFloat()
|
||||
total_chapters = it.selectInt("manga_chapters")
|
||||
tracking_url = mangaUrl(media_id)
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
private suspend fun getListUrl(): String {
|
||||
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 {
|
||||
val response = authClient.newCall(GET(url)).await()
|
||||
return Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CSRF = "csrf_token"
|
||||
|
||||
@ -206,88 +180,91 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
||||
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
|
||||
|
||||
private fun loginUrl() = Uri.parse(baseUrl).buildUpon()
|
||||
.appendPath("login.php")
|
||||
.toString()
|
||||
.appendPath("login.php")
|
||||
.toString()
|
||||
|
||||
private fun searchUrl(query: String): String {
|
||||
val col = "c[]"
|
||||
return Uri.parse(baseUrl).buildUpon()
|
||||
.appendPath("manga.php")
|
||||
.appendQueryParameter("q", query)
|
||||
.appendQueryParameter(col, "a")
|
||||
.appendQueryParameter(col, "b")
|
||||
.appendQueryParameter(col, "c")
|
||||
.appendQueryParameter(col, "d")
|
||||
.appendQueryParameter(col, "e")
|
||||
.appendQueryParameter(col, "g")
|
||||
.toString()
|
||||
.appendPath("manga.php")
|
||||
.appendQueryParameter("q", query)
|
||||
.appendQueryParameter(col, "a")
|
||||
.appendQueryParameter(col, "b")
|
||||
.appendQueryParameter(col, "c")
|
||||
.appendQueryParameter(col, "d")
|
||||
.appendQueryParameter(col, "e")
|
||||
.appendQueryParameter(col, "g")
|
||||
.toString()
|
||||
}
|
||||
|
||||
private fun exportListUrl() = Uri.parse(baseUrl).buildUpon()
|
||||
.appendPath("panel.php")
|
||||
.appendQueryParameter("go", "export")
|
||||
.toString()
|
||||
.appendPath("panel.php")
|
||||
.appendQueryParameter("go", "export")
|
||||
.toString()
|
||||
|
||||
private fun updateUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
||||
.appendPath("edit.json")
|
||||
.toString()
|
||||
.appendPath("edit.json")
|
||||
.toString()
|
||||
|
||||
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
||||
.appendPath( "add.json")
|
||||
.toString()
|
||||
.appendPath("add.json")
|
||||
.toString()
|
||||
|
||||
private fun listEntryUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
|
||||
.appendPath(mediaId.toString())
|
||||
.appendPath("edit")
|
||||
.toString()
|
||||
.appendPath(mediaId.toString())
|
||||
.appendPath("edit")
|
||||
.toString()
|
||||
|
||||
private fun loginPostBody(username: String, password: String, csrf: String): RequestBody {
|
||||
return FormBody.Builder()
|
||||
.add("user_name", username)
|
||||
.add("password", password)
|
||||
.add("cookie", "1")
|
||||
.add("sublogin", "Login")
|
||||
.add("submit", "1")
|
||||
.add(CSRF, csrf)
|
||||
.build()
|
||||
.add("user_name", username)
|
||||
.add("password", password)
|
||||
.add("cookie", "1")
|
||||
.add("sublogin", "Login")
|
||||
.add("submit", "1")
|
||||
.add(CSRF, csrf)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun exportPostBody(): RequestBody {
|
||||
return FormBody.Builder()
|
||||
.add("type", "2")
|
||||
.add("subexport", "Export My List")
|
||||
.build()
|
||||
.add("type", "2")
|
||||
.add("subexport", "Export My List")
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun mangaPostPayload(track: Track): RequestBody {
|
||||
val body = JSONObject()
|
||||
.put("manga_id", track.media_id)
|
||||
.put("status", track.status)
|
||||
.put("score", track.score)
|
||||
.put("num_read_chapters", track.last_chapter_read)
|
||||
.put("manga_id", track.media_id)
|
||||
.put("status", track.status)
|
||||
.put("score", track.score)
|
||||
.put("num_read_chapters", track.last_chapter_read)
|
||||
|
||||
return body.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
||||
return body.toString()
|
||||
.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
||||
}
|
||||
|
||||
private fun Element.searchTitle() = select("strong").text()!!
|
||||
|
||||
private fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
|
||||
private fun Element.searchTotalChapters() =
|
||||
if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
|
||||
|
||||
private fun Element.searchCoverUrl() = select("img")
|
||||
.attr("data-src")
|
||||
.split("\\?")[0]
|
||||
.replace("/r/50x70/", "/")
|
||||
.attr("data-src")
|
||||
.split("\\?")[0]
|
||||
.replace("/r/50x70/", "/")
|
||||
|
||||
private fun Element.searchMediaId() = select("div.picSurround")
|
||||
.select("a").attr("id")
|
||||
.replace("sarea", "")
|
||||
.toInt()
|
||||
.select("a").attr("id")
|
||||
.replace("sarea", "")
|
||||
.toInt()
|
||||
|
||||
private fun Element.searchSummary() = select("div.pt4")
|
||||
.first()
|
||||
.ownText()!!
|
||||
.first()
|
||||
.ownText()!!
|
||||
|
||||
private fun Element.searchPublishingStatus() = if (select(TD).last().text() == "-") "Publishing" else "Finished"
|
||||
private fun Element.searchPublishingStatus() =
|
||||
if (select(TD).last().text() == "-") "Publishing" else "Finished"
|
||||
|
||||
private fun Element.searchPublishingType() = select(TD)[2].text()!!
|
||||
|
||||
@ -300,6 +277,6 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
||||
"Dropped" -> 4
|
||||
"Plan to Read" -> 6
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -7,74 +7,11 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
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 fun add(track: Track): Observable<Track> {
|
||||
return api.addLibManga(track, getUsername())
|
||||
}
|
||||
|
||||
override fun update(track: Track): Observable<Track> {
|
||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||
track.status = COMPLETED
|
||||
}
|
||||
return api.updateLibManga(track, getUsername())
|
||||
}
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
return api.findLibManga(track, getUsername())
|
||||
.flatMap { remoteTrack ->
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
||||
return api.search(query)
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
return api.findLibManga(track, getUsername())
|
||||
.map { remoteTrack ->
|
||||
if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
}
|
||||
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()
|
||||
@ -103,18 +40,64 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun login(username: String, password: String) = login(password)
|
||||
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 {
|
||||
try {
|
||||
val oauth = api.accessToken(code)
|
||||
|
||||
fun login(code: String): Completable {
|
||||
return api.accessToken(code).map { oauth: OAuth? ->
|
||||
interceptor.newAuth(oauth)
|
||||
if (oauth != null) {
|
||||
val user = api.getCurrentUser()
|
||||
saveCredentials(user.toString(), oauth.access_token)
|
||||
}
|
||||
}.doOnError {
|
||||
val user = api.getCurrentUser()
|
||||
saveCredentials(user.toString(), oauth.access_token)
|
||||
return true
|
||||
} catch (e: java.lang.Exception) {
|
||||
Timber.e(e)
|
||||
logout()
|
||||
}.toCompletable()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun saveToken(oauth: OAuth?) {
|
||||
@ -135,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
|
||||
}
|
||||
}
|
||||
|
@ -14,68 +14,67 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import okhttp3.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) {
|
||||
|
||||
private val gson: Gson by injectLazy()
|
||||
private val parser = JsonParser()
|
||||
private val jsonime = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
fun addLibManga(track: Track, user_id: String): Observable<Track> {
|
||||
val payload = jsonObject(
|
||||
suspend fun updateLibManga(track: Track, user_id: String): Track = addLibManga(track, user_id)
|
||||
|
||||
suspend fun addLibManga(track: Track, user_id: String): Track {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val payload = jsonObject(
|
||||
"user_rate" to jsonObject(
|
||||
"user_id" to user_id,
|
||||
"target_id" to track.media_id,
|
||||
"target_type" to "Manga",
|
||||
"chapters" to track.last_chapter_read,
|
||||
"score" to track.score.toInt(),
|
||||
"status" to track.toShikimoriStatus()
|
||||
"user_id" to user_id,
|
||||
"target_id" to track.media_id,
|
||||
"target_type" to "Manga",
|
||||
"chapters" to track.last_chapter_read,
|
||||
"score" to track.score.toInt(),
|
||||
"status" to track.toShikimoriStatus()
|
||||
)
|
||||
)
|
||||
val body = payload.toString().toRequestBody(jsonime)
|
||||
val request = Request.Builder()
|
||||
)
|
||||
val body = payload.toString().toRequestBody(jsonime)
|
||||
val request = Request.Builder()
|
||||
.url("$apiUrl/v2/user_rates")
|
||||
.post(body)
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
}
|
||||
authClient.newCall(request).execute()
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id)
|
||||
|
||||
fun search(search: String): Observable<List<TrackSearch>> {
|
||||
val url = Uri.parse("$apiUrl/mangas").buildUpon()
|
||||
suspend fun search(search: String): List<TrackSearch> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val url = Uri.parse("$apiUrl/mangas").buildUpon()
|
||||
.appendQueryParameter("order", "popularity")
|
||||
.appendQueryParameter("search", search)
|
||||
.appendQueryParameter("limit", "20")
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
val request = Request.Builder()
|
||||
.url(url.toString())
|
||||
.get()
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = parser.parse(responseBody).array
|
||||
response.map { jsonToSearch(it.obj) }
|
||||
}
|
||||
val netResponse = authClient.newCall(request).execute()
|
||||
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = JsonParser.parseString(responseBody).array
|
||||
|
||||
response.map { jsonToSearch(it.obj) }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||
@ -104,56 +103,55 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
}
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track, user_id: String): Observable<Track?> {
|
||||
val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon()
|
||||
suspend fun findLibManga(track: Track, user_id: String): Track? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon()
|
||||
.appendQueryParameter("user_id", user_id)
|
||||
.appendQueryParameter("target_id", track.media_id.toString())
|
||||
.appendQueryParameter("target_type", "Manga")
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
val request = Request.Builder()
|
||||
.url(url.toString())
|
||||
.get()
|
||||
.build()
|
||||
|
||||
val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon()
|
||||
val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon()
|
||||
.appendPath(track.media_id.toString())
|
||||
.build()
|
||||
val requestMangas = Request.Builder()
|
||||
val requestMangas = Request.Builder()
|
||||
.url(urlMangas.toString())
|
||||
.get()
|
||||
.build()
|
||||
return authClient.newCall(requestMangas)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
parser.parse(responseBody).obj
|
||||
}.flatMap { mangas ->
|
||||
authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = parser.parse(responseBody).array
|
||||
if (response.size() > 1) {
|
||||
throw Exception("Too much mangas in response")
|
||||
}
|
||||
val entry = response.map {
|
||||
jsonToTrack(it.obj, mangas)
|
||||
}
|
||||
entry.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
val requestMangasResponse = authClient.newCall(requestMangas).execute()
|
||||
val requestMangasBody = requestMangasResponse.body?.string().orEmpty()
|
||||
val mangas = JsonParser.parseString(requestMangasBody).obj
|
||||
|
||||
val requestResponse = authClient.newCall(request).execute()
|
||||
val requestResponseBody = requestResponse.body?.string().orEmpty()
|
||||
|
||||
if (requestResponseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = JsonParser.parseString(requestResponseBody).array
|
||||
if (response.size() > 1) {
|
||||
throw Exception("Too much mangas in response")
|
||||
}
|
||||
val entry = response.map {
|
||||
jsonToTrack(it.obj, mangas)
|
||||
}
|
||||
entry.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentUser(): Int {
|
||||
val user = authClient.newCall(GET("$apiUrl/users/whoami")).execute().body?.string()
|
||||
return parser.parse(user).obj["id"].asInt
|
||||
return JsonParser.parseString(user).obj["id"].asInt
|
||||
}
|
||||
|
||||
fun accessToken(code: String): Observable<OAuth> {
|
||||
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
|
||||
suspend fun accessToken(code: String): OAuth {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val netResponse= client.newCall(accessTokenRequest(code)).execute()
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
@ -162,20 +160,22 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
}
|
||||
}
|
||||
|
||||
private fun accessTokenRequest(code: String) = POST(oauthUrl,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "authorization_code")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("code", code)
|
||||
.add("redirect_uri", redirectUrl)
|
||||
.build()
|
||||
private fun accessTokenRequest(code: String) = POST(
|
||||
oauthUrl,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "authorization_code")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("code", code)
|
||||
.add("redirect_uri", redirectUrl)
|
||||
.build()
|
||||
)
|
||||
|
||||
|
||||
companion object {
|
||||
private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
|
||||
private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
|
||||
private const val clientId =
|
||||
"1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
|
||||
private const val clientSecret =
|
||||
"229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
|
||||
|
||||
private const val baseUrl = "https://shikimori.one"
|
||||
private const val apiUrl = "https://shikimori.one/api"
|
||||
@ -190,21 +190,20 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
}
|
||||
|
||||
fun authUrl() =
|
||||
Uri.parse(loginUrl).buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.build()
|
||||
|
||||
|
||||
fun refreshTokenRequest(token: String) = POST(oauthUrl,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("refresh_token", token)
|
||||
.build())
|
||||
Uri.parse(loginUrl).buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.build()
|
||||
|
||||
fun refreshTokenRequest(token: String) = POST(
|
||||
oauthUrl,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("refresh_token", token)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -1,20 +1,13 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.updater.devrepo.DevRepoUpdateChecker
|
||||
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
|
||||
import rx.Observable
|
||||
|
||||
abstract class UpdateChecker {
|
||||
|
||||
companion object {
|
||||
fun getUpdateChecker(): UpdateChecker {
|
||||
return if (BuildConfig.DEBUG) {
|
||||
DevRepoUpdateChecker()
|
||||
} else {
|
||||
GithubUpdateChecker()
|
||||
}
|
||||
}
|
||||
fun getUpdateChecker(): UpdateChecker = GithubUpdateChecker()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,14 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.updater.devrepo
|
||||
|
||||
import eu.kanade.tachiyomi.data.updater.Release
|
||||
|
||||
class DevRepoRelease(override val info: String) : Release {
|
||||
|
||||
override val downloadLink: String
|
||||
get() = LATEST_URL
|
||||
|
||||
companion object {
|
||||
const val LATEST_URL = "https://tachiyomi.kanade.eu/latest"
|
||||
}
|
||||
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.updater.devrepo
|
||||
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateChecker
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.asObservable
|
||||
import okhttp3.OkHttpClient
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class DevRepoUpdateChecker : UpdateChecker() {
|
||||
|
||||
private val client: OkHttpClient by lazy {
|
||||
Injekt.get<NetworkHelper>().client.newBuilder()
|
||||
.followRedirects(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
private val versionRegex: Regex by lazy {
|
||||
Regex("tachiyomi-r(\\d+).apk")
|
||||
}
|
||||
|
||||
override fun checkForUpdate(): Observable<UpdateResult> {
|
||||
return client.newCall(GET(DevRepoRelease.LATEST_URL)).asObservable()
|
||||
.map { response ->
|
||||
// Get latest repo version number from header in format "Location: tachiyomi-r1512.apk"
|
||||
val latestVersionNumber: String = versionRegex.find(response.header("Location")!!)!!.groupValues[1]
|
||||
|
||||
if (latestVersionNumber.toInt() > BuildConfig.COMMIT_COUNT.toInt()) {
|
||||
DevRepoUpdateResult.NewUpdate(DevRepoRelease("v$latestVersionNumber"))
|
||||
} else {
|
||||
DevRepoUpdateResult.NoNewUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.updater.devrepo
|
||||
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||
|
||||
sealed class DevRepoUpdateResult : UpdateResult() {
|
||||
|
||||
class NewUpdate(release: DevRepoRelease): UpdateResult.NewUpdate<DevRepoRelease>(release)
|
||||
class NoNewUpdate: UpdateResult.NoNewUpdate()
|
||||
|
||||
}
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.updater.github
|
||||
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import rx.Observable
|
||||
@ -19,7 +18,6 @@ interface GithubService {
|
||||
val restAdapter = Retrofit.Builder()
|
||||
.baseUrl("https://api.github.com")
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||
.client(Injekt.get<NetworkHelper>().client)
|
||||
.build()
|
||||
|
||||
@ -28,6 +26,6 @@ interface GithubService {
|
||||
}
|
||||
|
||||
@GET("/repos/Jays2Kings/tachiyomiJ2K/releases/latest")
|
||||
fun getLatestVersion(): Observable<GithubRelease>
|
||||
suspend fun getLatestVersion(): GithubRelease
|
||||
|
||||
}
|
||||
|
@ -2,11 +2,15 @@ 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
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.zip.GZIPInputStream
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
@ -94,3 +98,25 @@ 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}")
|
||||
return it.body?.string()
|
||||
}
|
||||
}
|
||||
|
||||
fun Response.consumeXmlBody(): String? {
|
||||
use { res ->
|
||||
if (res.code != 200) throw Exception("Export list error")
|
||||
BufferedReader(InputStreamReader(GZIPInputStream(res.body?.source()?.inputStream()))).use { reader ->
|
||||
val sb = StringBuilder()
|
||||
reader.forEachLine { line ->
|
||||
sb.append(line)
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
@ -927,10 +928,12 @@ class MangaDetailsController : BaseController,
|
||||
}
|
||||
|
||||
fun trackRefreshError(error: Exception) {
|
||||
Timber.e(error)
|
||||
trackingBottomSheet?.onRefreshError(error)
|
||||
}
|
||||
|
||||
fun trackSearchError(error: Exception) {
|
||||
Timber.e(error)
|
||||
trackingBottomSheet?.onSearchResultsError(error)
|
||||
}
|
||||
|
||||
|
@ -691,7 +691,7 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController,
|
||||
val list = trackList.filter { it.track != null }.map { item ->
|
||||
withContext(Dispatchers.IO) {
|
||||
val trackItem = try {
|
||||
item.service.refresh(item.track!!).toBlocking().single()
|
||||
item.service.refresh(item.track!!)
|
||||
} catch (e: Exception) {
|
||||
trackError(e)
|
||||
null
|
||||
@ -710,7 +710,7 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController,
|
||||
|
||||
fun trackSearch(query: String, service: TrackService) {
|
||||
launch(Dispatchers.IO) {
|
||||
val results = try {service.search(query).toBlocking().single() }
|
||||
val results = try {service.search(query) }
|
||||
catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) { controller.trackSearchError(e) }
|
||||
null }
|
||||
@ -725,7 +725,7 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController,
|
||||
item.manga_id = manga.id!!
|
||||
|
||||
launch {
|
||||
val binding = try { service.bind(item).toBlocking().single() }
|
||||
val binding = try { service.bind(item) }
|
||||
catch (e: Exception) {
|
||||
trackError(e)
|
||||
null
|
||||
@ -745,7 +745,7 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController,
|
||||
|
||||
private fun updateRemote(track: Track, service: TrackService) {
|
||||
launch {
|
||||
val binding = try { service.update(track).toBlocking().single() }
|
||||
val binding = try { service.update(track) }
|
||||
catch (e: Exception) {
|
||||
trackError(e)
|
||||
null
|
||||
|
@ -24,6 +24,11 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
@ -40,11 +45,11 @@ import java.util.concurrent.TimeUnit
|
||||
* Presenter used by the activity to perform background operations.
|
||||
*/
|
||||
class ReaderPresenter(
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val coverCache: CoverCache = Injekt.get(),
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val coverCache: CoverCache = Injekt.get(),
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
) : BasePresenter<ReaderActivity>() {
|
||||
|
||||
/**
|
||||
@ -87,19 +92,19 @@ class ReaderPresenter(
|
||||
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
||||
|
||||
val selectedChapter = dbChapters.find { it.id == chapterId }
|
||||
?: error("Requested chapter of id $chapterId not found in chapter list")
|
||||
?: error("Requested chapter of id $chapterId not found in chapter list")
|
||||
|
||||
val chaptersForReader =
|
||||
if (preferences.skipRead()) {
|
||||
val list = dbChapters.filter { !it.read }.toMutableList()
|
||||
val find = list.find { it.id == chapterId }
|
||||
if (find == null) {
|
||||
list.add(selectedChapter)
|
||||
}
|
||||
list
|
||||
} else {
|
||||
dbChapters
|
||||
if (preferences.skipRead()) {
|
||||
val list = dbChapters.filter { !it.read }.toMutableList()
|
||||
val find = list.find { it.id == chapterId }
|
||||
if (find == null) {
|
||||
list.add(selectedChapter)
|
||||
}
|
||||
list
|
||||
} else {
|
||||
dbChapters
|
||||
}
|
||||
|
||||
when (manga.sorting) {
|
||||
Manga.SORTING_SOURCE -> ChapterLoadBySource().get(chaptersForReader)
|
||||
@ -170,12 +175,12 @@ class ReaderPresenter(
|
||||
if (!needsInit()) return
|
||||
|
||||
db.getManga(mangaId).asRxObservable()
|
||||
.first()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { init(it, initialChapterId) }
|
||||
.subscribeFirst({ _, _ ->
|
||||
// Ignore onNext event
|
||||
}, ReaderActivity::setInitialChapterError)
|
||||
.first()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { init(it, initialChapterId) }
|
||||
.subscribeFirst({ _, _ ->
|
||||
// Ignore onNext event
|
||||
}, ReaderActivity::setInitialChapterError)
|
||||
}
|
||||
|
||||
fun init(mangaId: Long, chapterUrl: String) {
|
||||
@ -207,13 +212,13 @@ class ReaderPresenter(
|
||||
// Read chapterList from an io thread because it's retrieved lazily and would block main.
|
||||
activeChapterSubscription?.unsubscribe()
|
||||
activeChapterSubscription = Observable
|
||||
.fromCallable { chapterList.first { chapterId == it.chapter.id } }
|
||||
.flatMap { getLoadObservable(loader!!, it) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ _, _ ->
|
||||
// Ignore onNext event
|
||||
}, ReaderActivity::setInitialChapterError)
|
||||
.fromCallable { chapterList.first { chapterId == it.chapter.id } }
|
||||
.flatMap { getLoadObservable(loader!!, it) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ _, _ ->
|
||||
// Ignore onNext event
|
||||
}, ReaderActivity::setInitialChapterError)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -224,27 +229,29 @@ class ReaderPresenter(
|
||||
* Callers must also handle the onError event.
|
||||
*/
|
||||
private fun getLoadObservable(
|
||||
loader: ChapterLoader,
|
||||
chapter: ReaderChapter
|
||||
loader: ChapterLoader,
|
||||
chapter: ReaderChapter
|
||||
): Observable<ViewerChapters> {
|
||||
return loader.loadChapter(chapter)
|
||||
.andThen(Observable.fromCallable {
|
||||
val chapterPos = chapterList.indexOf(chapter)
|
||||
.andThen(Observable.fromCallable {
|
||||
val chapterPos = chapterList.indexOf(chapter)
|
||||
|
||||
ViewerChapters(chapter,
|
||||
chapterList.getOrNull(chapterPos - 1),
|
||||
chapterList.getOrNull(chapterPos + 1))
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { newChapters ->
|
||||
val oldChapters = viewerChaptersRelay.value
|
||||
ViewerChapters(
|
||||
chapter,
|
||||
chapterList.getOrNull(chapterPos - 1),
|
||||
chapterList.getOrNull(chapterPos + 1)
|
||||
)
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { newChapters ->
|
||||
val oldChapters = viewerChaptersRelay.value
|
||||
|
||||
// Add new references first to avoid unnecessary recycling
|
||||
newChapters.ref()
|
||||
oldChapters?.unref()
|
||||
// Add new references first to avoid unnecessary recycling
|
||||
newChapters.ref()
|
||||
oldChapters?.unref()
|
||||
|
||||
viewerChaptersRelay.call(newChapters)
|
||||
}
|
||||
viewerChaptersRelay.call(newChapters)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -258,10 +265,10 @@ class ReaderPresenter(
|
||||
|
||||
activeChapterSubscription?.unsubscribe()
|
||||
activeChapterSubscription = getLoadObservable(loader, chapter)
|
||||
.toCompletable()
|
||||
.onErrorComplete()
|
||||
.subscribe()
|
||||
.also(::add)
|
||||
.toCompletable()
|
||||
.onErrorComplete()
|
||||
.subscribe()
|
||||
.also(::add)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -276,13 +283,13 @@ class ReaderPresenter(
|
||||
|
||||
activeChapterSubscription?.unsubscribe()
|
||||
activeChapterSubscription = getLoadObservable(loader, chapter)
|
||||
.doOnSubscribe { isLoadingAdjacentChapterRelay.call(true) }
|
||||
.doOnUnsubscribe { isLoadingAdjacentChapterRelay.call(false) }
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.moveToPageIndex(0)
|
||||
}, { _, _ ->
|
||||
// Ignore onError event, viewers handle that state
|
||||
})
|
||||
.doOnSubscribe { isLoadingAdjacentChapterRelay.call(true) }
|
||||
.doOnUnsubscribe { isLoadingAdjacentChapterRelay.call(false) }
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.moveToPageIndex(0)
|
||||
}, { _, _ ->
|
||||
// Ignore onError event, viewers handle that state
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -299,12 +306,12 @@ class ReaderPresenter(
|
||||
val loader = loader ?: return
|
||||
|
||||
loader.loadChapter(chapter)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
// Update current chapters whenever a chapter is preloaded
|
||||
.doOnCompleted { viewerChaptersRelay.value?.let(viewerChaptersRelay::call) }
|
||||
.onErrorComplete()
|
||||
.subscribe()
|
||||
.also(::add)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
// Update current chapters whenever a chapter is preloaded
|
||||
.doOnCompleted { viewerChaptersRelay.value?.let(viewerChaptersRelay::call) }
|
||||
.onErrorComplete()
|
||||
.subscribe()
|
||||
.also(::add)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -348,9 +355,9 @@ class ReaderPresenter(
|
||||
*/
|
||||
private fun saveChapterProgress(chapter: ReaderChapter) {
|
||||
db.updateChapterProgress(chapter.chapter).asRxCompletable()
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -412,18 +419,18 @@ class ReaderPresenter(
|
||||
db.updateMangaViewer(manga).executeAsBlocking()
|
||||
|
||||
Observable.timer(250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ ->
|
||||
val currChapters = viewerChaptersRelay.value
|
||||
if (currChapters != null) {
|
||||
// Save current page
|
||||
val currChapter = currChapters.currChapter
|
||||
currChapter.requestedPage = currChapter.chapter.last_page_read
|
||||
.subscribeFirst({ view, _ ->
|
||||
val currChapters = viewerChaptersRelay.value
|
||||
if (currChapters != null) {
|
||||
// Save current page
|
||||
val currChapter = currChapters.currChapter
|
||||
currChapter.requestedPage = currChapter.chapter.last_page_read
|
||||
|
||||
// Emit manga and chapters to the new viewer
|
||||
view.setManga(manga)
|
||||
view.setChapters(currChapters)
|
||||
}
|
||||
})
|
||||
// Emit manga and chapters to the new viewer
|
||||
view.setManga(manga)
|
||||
view.setChapters(currChapters)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -439,7 +446,7 @@ class ReaderPresenter(
|
||||
|
||||
// Build destination file.
|
||||
val filename = DiskUtil.buildValidFilename(
|
||||
"${manga.currentTitle()} - ${chapter.name}".take(225)
|
||||
"${manga.currentTitle()} - ${chapter.name}".take(225)
|
||||
) + " - ${page.number}.${type.extension}"
|
||||
|
||||
val destFile = File(directory, filename)
|
||||
@ -464,23 +471,25 @@ class ReaderPresenter(
|
||||
notifier.onClear()
|
||||
|
||||
// Pictures directory.
|
||||
val destDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
||||
val destDir = File(
|
||||
Environment.getExternalStorageDirectory().absolutePath +
|
||||
File.separator + Environment.DIRECTORY_PICTURES +
|
||||
File.separator + "Tachiyomi")
|
||||
File.separator + "Tachiyomi"
|
||||
)
|
||||
|
||||
// Copy file in background.
|
||||
Observable.fromCallable { saveImage(page, destDir, manga) }
|
||||
.doOnNext { file ->
|
||||
DiskUtil.scanMedia(context, file)
|
||||
notifier.onComplete(file)
|
||||
}
|
||||
.doOnError { notifier.onError(it.message) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ view, file -> view.onSaveImageResult(SaveImageResult.Success(file)) },
|
||||
{ view, error -> view.onSaveImageResult(SaveImageResult.Error(error)) }
|
||||
)
|
||||
.doOnNext { file ->
|
||||
DiskUtil.scanMedia(context, file)
|
||||
notifier.onComplete(file)
|
||||
}
|
||||
.doOnError { notifier.onError(it.message) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ view, file -> view.onSaveImageResult(SaveImageResult.Success(file)) },
|
||||
{ view, error -> view.onSaveImageResult(SaveImageResult.Error(error)) }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -498,13 +507,13 @@ class ReaderPresenter(
|
||||
val destDir = File(context.cacheDir, "shared_image")
|
||||
|
||||
Observable.fromCallable { destDir.deleteRecursively() } // Keep only the last shared file
|
||||
.map { saveImage(page, destDir, manga) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ view, file -> view.onShareImageResult(file) },
|
||||
{ _, _ -> /* Empty */ }
|
||||
)
|
||||
.map { saveImage(page, destDir, manga) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ view, file -> view.onShareImageResult(file) },
|
||||
{ _, _ -> /* Empty */ }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -516,29 +525,29 @@ class ReaderPresenter(
|
||||
val stream = page.stream ?: return
|
||||
|
||||
Observable
|
||||
.fromCallable {
|
||||
if (manga.source == LocalSource.ID) {
|
||||
val context = Injekt.get<Application>()
|
||||
LocalSource.updateCover(context, manga, stream())
|
||||
R.string.cover_updated
|
||||
.fromCallable {
|
||||
if (manga.source == LocalSource.ID) {
|
||||
val context = Injekt.get<Application>()
|
||||
LocalSource.updateCover(context, manga, stream())
|
||||
R.string.cover_updated
|
||||
SetAsCoverResult.Success
|
||||
} else {
|
||||
val thumbUrl = manga.thumbnail_url ?: throw Exception("Image url not found")
|
||||
if (manga.favorite) {
|
||||
coverCache.copyToCache(thumbUrl, stream())
|
||||
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
|
||||
SetAsCoverResult.Success
|
||||
} else {
|
||||
val thumbUrl = manga.thumbnail_url ?: throw Exception("Image url not found")
|
||||
if (manga.favorite) {
|
||||
coverCache.copyToCache(thumbUrl, stream())
|
||||
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
|
||||
SetAsCoverResult.Success
|
||||
} else {
|
||||
SetAsCoverResult.AddToLibraryFirst
|
||||
}
|
||||
SetAsCoverResult.AddToLibraryFirst
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ view, result -> view.onSetAsCoverResult(result) },
|
||||
{ view, _ -> view.onSetAsCoverResult(SetAsCoverResult.Error) }
|
||||
)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ view, result -> view.onSetAsCoverResult(result) },
|
||||
{ view, _ -> view.onSetAsCoverResult(SetAsCoverResult.Error) }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -568,27 +577,24 @@ class ReaderPresenter(
|
||||
|
||||
val trackManager = Injekt.get<TrackManager>()
|
||||
|
||||
db.getTracks(manga).asRxSingle()
|
||||
.flatMapCompletable { trackList ->
|
||||
Completable.concat(trackList.map { track ->
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service.isLogged && chapterRead > track.last_chapter_read) {
|
||||
// We wan't these to execute even if the presenter is destroyed so launch on GlobalScope
|
||||
GlobalScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val trackList = db.getTracks(manga).executeAsBlocking()
|
||||
trackList.map { track ->
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service.isLogged && chapterRead > track.last_chapter_read) {
|
||||
try {
|
||||
track.last_chapter_read = chapterRead
|
||||
|
||||
// We wan't these to execute even if the presenter is destroyed and leaks
|
||||
// for a while. The view can still be garbage collected.
|
||||
Observable.defer { service.update(track) }
|
||||
.map { db.insertTrack(track).executeAsBlocking() }
|
||||
.toCompletable()
|
||||
.onErrorComplete()
|
||||
} else {
|
||||
Completable.complete()
|
||||
service.update(track)
|
||||
db.insertTrack(track).executeAsBlocking()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -604,19 +610,19 @@ class ReaderPresenter(
|
||||
if (removeAfterReadSlots == -1) return
|
||||
|
||||
Completable
|
||||
.fromCallable {
|
||||
// Position of the read chapter
|
||||
val position = chapterList.indexOf(chapter)
|
||||
.fromCallable {
|
||||
// Position of the read chapter
|
||||
val position = chapterList.indexOf(chapter)
|
||||
|
||||
// Retrieve chapter to delete according to preference
|
||||
val chapterToDelete = chapterList.getOrNull(position - removeAfterReadSlots)
|
||||
if (chapterToDelete != null) {
|
||||
downloadManager.enqueueDeleteChapters(listOf(chapterToDelete.chapter), manga)
|
||||
}
|
||||
// Retrieve chapter to delete according to preference
|
||||
val chapterToDelete = chapterList.getOrNull(position - removeAfterReadSlots)
|
||||
if (chapterToDelete != null) {
|
||||
downloadManager.enqueueDeleteChapters(listOf(chapterToDelete.chapter), manga)
|
||||
}
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -625,9 +631,8 @@ class ReaderPresenter(
|
||||
*/
|
||||
private fun deletePendingChapters() {
|
||||
Completable.fromCallable { downloadManager.deletePendingChapters() }
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,21 +2,26 @@ package eu.kanade.tachiyomi.ui.setting.track
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.view.Gravity.CENTER
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ProgressBar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
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
|
||||
|
||||
class AnilistLoginActivity : AppCompatActivity() {
|
||||
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
@ -26,20 +31,21 @@ class AnilistLoginActivity : AppCompatActivity() {
|
||||
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
|
||||
val matchResult = regex.find(intent.data?.fragment.toString())
|
||||
if (matchResult?.groups?.get(1) != null) {
|
||||
trackManager.aniList.login(matchResult.groups[1]!!.value)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({
|
||||
returnToSettings()
|
||||
}, {
|
||||
returnToSettings()
|
||||
})
|
||||
scope.launch {
|
||||
trackManager.aniList.login(matchResult.groups[1]!!.value)
|
||||
returnToSettings()
|
||||
}
|
||||
} else {
|
||||
trackManager.aniList.logout()
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
private fun returnToSettings() {
|
||||
finish()
|
||||
|
||||
@ -47,5 +53,4 @@ class AnilistLoginActivity : AppCompatActivity() {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,13 +2,18 @@ package eu.kanade.tachiyomi.ui.setting.track
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.view.Gravity.CENTER
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ProgressBar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@ -17,6 +22,8 @@ class BangumiLoginActivity : AppCompatActivity() {
|
||||
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
@ -25,14 +32,10 @@ class BangumiLoginActivity : AppCompatActivity() {
|
||||
|
||||
val code = intent.data?.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
trackManager.bangumi.login(code)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({
|
||||
returnToSettings()
|
||||
}, {
|
||||
returnToSettings()
|
||||
})
|
||||
scope.launch {
|
||||
trackManager.bangumi.login(code)
|
||||
returnToSettings()
|
||||
}
|
||||
} else {
|
||||
trackManager.bangumi.logout()
|
||||
returnToSettings()
|
||||
@ -46,5 +49,4 @@ class BangumiLoginActivity : AppCompatActivity() {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,21 +2,25 @@ package eu.kanade.tachiyomi.ui.setting.track
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.view.Gravity.CENTER
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ProgressBar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class ShikimoriLoginActivity : AppCompatActivity() {
|
||||
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
@ -25,14 +29,10 @@ class ShikimoriLoginActivity : AppCompatActivity() {
|
||||
|
||||
val code = intent.data?.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
trackManager.shikimori.login(code)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({
|
||||
returnToSettings()
|
||||
}, {
|
||||
returnToSettings()
|
||||
})
|
||||
scope.launch {
|
||||
trackManager.shikimori.login(code)
|
||||
returnToSettings()
|
||||
}
|
||||
} else {
|
||||
trackManager.shikimori.logout()
|
||||
returnToSettings()
|
||||
@ -46,5 +46,4 @@ class ShikimoriLoginActivity : AppCompatActivity() {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
}
|
@ -13,21 +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) {
|
||||
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
|
||||
@ -46,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 {
|
||||
@ -76,7 +82,6 @@ abstract class LoginDialogPreference(private val usernameLabel: String? = null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
@ -87,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,13 +7,12 @@ 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 rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) :
|
||||
LoginDialogPreference(usernameLabel, bundle) {
|
||||
LoginDialogPreference(usernameLabel, bundle) {
|
||||
|
||||
private val service = Injekt.get<TrackManager>().getService(args.getInt("key"))!!
|
||||
|
||||
@ -22,7 +21,7 @@ class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) :
|
||||
constructor(service: TrackService) : this(service, null)
|
||||
|
||||
constructor(service: TrackService, usernameLabel: String?) :
|
||||
this(usernameLabel, Bundle().apply { putInt("key", service.id) })
|
||||
this(usernameLabel, Bundle().apply { putInt("key", service.id) })
|
||||
|
||||
override fun setCredentialsOnView(view: View) = with(view) {
|
||||
dialog_title.text = context.getString(R.string.login_title, service.name)
|
||||
@ -31,7 +30,6 @@ class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) :
|
||||
}
|
||||
|
||||
override fun checkLogin() {
|
||||
requestSubscription?.unsubscribe()
|
||||
|
||||
v?.apply {
|
||||
if (username.text.isEmpty() || password.text.isEmpty())
|
||||
@ -41,17 +39,27 @@ class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) :
|
||||
val user = username.text.toString()
|
||||
val pass = password.text.toString()
|
||||
|
||||
requestSubscription = service.login(user, pass)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({
|
||||
scope.launch {
|
||||
try {
|
||||
val result = service.login(user, pass)
|
||||
if (result) {
|
||||
dialog?.dismiss()
|
||||
context.toast(R.string.login_success)
|
||||
}, { error ->
|
||||
login.progress = -1
|
||||
login.setText(R.string.unknown_error)
|
||||
error.message?.let { context.toast(it) }
|
||||
})
|
||||
} else {
|
||||
errorResult(this@apply)
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
errorResult(this@apply)
|
||||
error.message?.let { context.toast(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun errorResult(view: View?) {
|
||||
v?.apply {
|
||||
login.progress = -1
|
||||
login.setText(R.string.unknown_error)
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,5 +78,4 @@ class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) :
|
||||
interface Listener {
|
||||
fun trackDialogClosed(service: TrackService)
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user