diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6e347b7bae..4c9ef098c0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt index 1c8f2399cc..e08c24ce49 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt @@ -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) { + private suspend fun trackingFetch(manga: Manga, tracks: List) { 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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index a4cd28f076..847e942423 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -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() @@ -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, startId: Int) { + private fun launchTarget(target: Target, mangaToAdd: List, 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) { // 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, List>> { 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() - 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() + 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): Observable { + + private suspend fun updateTrackings(mangaToUpdate: List) { // 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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt index 62c34d422f..4cb4a2deb6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt index f4bfcaeff4..8b2165952e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt @@ -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 + abstract suspend fun update(track: Track): Track - abstract fun update(track: Track): Observable + abstract suspend fun bind(track: Track): Track - abstract fun bind(track: Track): Observable + abstract suspend fun search(query: String): List - abstract fun search(query: String): Observable> + abstract suspend fun refresh(track: Track): Track - abstract fun refresh(track: Track): Observable - - abstract fun login(username: String, password: String): Completable + abstract suspend fun login(username: String, password: String): Boolean @CallSuper open fun logout() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt index 5c83029730..7ce286ddd5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt @@ -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 { - 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 { - return api.addLibManga(track) - } - - override fun update(track: Track): Observable { + 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 { - 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> { - 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 { - 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" + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index 2dacccf141..3c9fd3596d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -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 { - 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 { + 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 { + 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 { - 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> { - 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 { - 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 { - 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> { - 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() - } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt index ff416a1c5f..90ca64ffb9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt @@ -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. diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt index 5eba6f373c..ea7f391762 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt deleted file mode 100644 index a53760ba5d..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Avatar.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Avatar.kt deleted file mode 100644 index d058a85f59..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Avatar.kt +++ /dev/null @@ -1,7 +0,0 @@ -package eu.kanade.tachiyomi.data.track.bangumi - -data class Avatar( - val large: String? = "", - val medium: String? = "", - val small: String? = "" -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt index 147dde6de6..31fdc880ba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt @@ -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 { - return api.addLibManga(track) - } - - override fun update(track: Track): Observable { + 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 { - 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> { + override suspend fun search(query: String): List { return api.search(query) } - override fun refresh(track: Track): Observable { - 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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt index 3b356f6533..d24f50b042 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt @@ -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 { + 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 { + 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> { - val url = Uri.parse( - "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}").buildUpon() + suspend fun search(search: String): List { + 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 { - 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 { + 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 { - 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() + ) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt index add168201e..fc2617d335 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt @@ -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) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Collection.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Collection.kt index 03143d19f5..db0d8396c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Collection.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Collection.kt @@ -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? = "" +) + + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/OAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/OAuth.kt deleted file mode 100644 index 811d0fd459..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/OAuth.kt +++ /dev/null @@ -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) - -} - diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Status.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Status.kt deleted file mode 100644 index 3d2ea3c14b..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Status.kt +++ /dev/null @@ -1,7 +0,0 @@ -package eu.kanade.tachiyomi.data.track.bangumi - -data class Status( - val id: Int? = 0, - val name: String? = "", - val type: String? = "" -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/User.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/User.kt deleted file mode 100644 index 9e82f533e3..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/User.kt +++ /dev/null @@ -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? = "" -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt index 4b4f8cbcfe..f1993860e3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt @@ -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 { - return api.addLibManga(track, getUserId()) - } - - override fun update(track: Track): Observable { + 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 { - 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> { + override suspend fun search(query: String): List { return api.search(query) } - override fun refresh(track: Track): Observable { - 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 } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index fa72b6d547..76aad53386 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -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 { + val key = searchRest.getKey()["media"].asJsonObject["key"].string + return algoliaSearch(key, query) + } + + private suspend fun algoliaSearch(key: String, query: String): List { + 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 { - 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 { - 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> { - return searchRest - .getKey().map { json -> - json["media"].asJsonObject["key"].string - }.flatMap { key -> - algoliaSearch(key, query) - } - } - - - private fun algoliaSearch(key: String, query: String): Observable> { - 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 { - 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 { - 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 { - 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 { - 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 + 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 - + 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 + 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 + 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 - + suspend fun getCurrentUser( + @Query("filter[self]", encoded = true) self: Boolean = true + ): JsonObject } private interface SearchKeyRest { @GET("media/") - fun getKey(): Observable + 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 + 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 - + 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() + ) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt index db14187fc0..d159c1607b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt @@ -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 { - return api.addLibManga(track) - } - - override fun update(track: Track): Observable { + 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 { - 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> { + override suspend fun search(query: String): List { return api.search(query) } - override fun refresh(track: Track): Observable { - 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" + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt index 65c16d0e25..4ea961fcb4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -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> { - 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 { + 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 { - return Observable.defer { - authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track))) - .asObservableSuccess() - .map { track } - } + private suspend fun queryUsersList(query: String): List { + val realQuery = query.removePrefix(PREFIX_MY).take(100) + return getList().filter { it.title.contains(realQuery, true) }.toList() } - fun updateLibManga(track: Track): Observable { - 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 { - 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 { - 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> { - 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 { - 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 { - 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 { + 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 - } + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt index 9ef078983b..2c9bb356fb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt @@ -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()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/OAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/OAuth.kt deleted file mode 100644 index 1f6a38b47d..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/OAuth.kt +++ /dev/null @@ -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) -} - diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt index 00f7a517ff..cbcb088e0b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt @@ -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 { - return IntRange(0, 10).map(Int::toString) - } - - override fun displayScore(track: Track): String { - return track.score.toInt().toString() - } - - override fun add(track: Track): Observable { - return api.addLibManga(track, getUsername()) - } - - override fun update(track: Track): Observable { - 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 { - 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> { - return api.search(query) - } - - override fun refresh(track: Track): Observable { - 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 { + 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 + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt index 74702fcca2..e5ff4ca81d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt @@ -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 { - 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 = addLibManga(track, user_id) - - fun search(search: String): Observable> { - val url = Uri.parse("$apiUrl/mangas").buildUpon() + suspend fun search(search: String): List { + 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 { - 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 { - 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() + ) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt index 91e556bdd8..4ff0943c0e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt @@ -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) +} + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateChecker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateChecker.kt index 4d2a1de66c..fd056bbc08 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateChecker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateChecker.kt @@ -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() } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoRelease.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoRelease.kt deleted file mode 100644 index ea8a79a182..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoRelease.kt +++ /dev/null @@ -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" - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoUpdateChecker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoUpdateChecker.kt deleted file mode 100644 index a24036830d..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoUpdateChecker.kt +++ /dev/null @@ -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().client.newBuilder() - .followRedirects(false) - .build() - } - - private val versionRegex: Regex by lazy { - Regex("tachiyomi-r(\\d+).apk") - } - - override fun checkForUpdate(): Observable { - 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() - } - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoUpdateResult.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoUpdateResult.kt deleted file mode 100644 index 1bda48b9c3..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoUpdateResult.kt +++ /dev/null @@ -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(release) - class NoNewUpdate: UpdateResult.NoNewUpdate() - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubService.kt index 052684bd03..beaf42ea74 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubService.kt @@ -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().client) .build() @@ -28,6 +26,6 @@ interface GithubService { } @GET("/repos/Jays2Kings/tachiyomiJ2K/releases/latest") - fun getLatestVersion(): Observable + suspend fun getLatestVersion(): GithubRelease } diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt index dc76ff0e7a..8de9f9e25d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt @@ -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() + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt index 21817c09ab..a8756d78c5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt @@ -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) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt index b46d7262e8..03d3a664db 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index 2708ccb7d5..674702dd29 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -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() { /** @@ -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 { 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() - LocalSource.updateCover(context, manga, stream()) - R.string.cover_updated + .fromCallable { + if (manga.source == LocalSource.ID) { + val context = Injekt.get() + 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() - 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() } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/AnilistLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/AnilistLoginActivity.kt index 0418fdaa63..cdee2846a0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/AnilistLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/AnilistLoginActivity.kt @@ -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) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BangumiLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BangumiLoginActivity.kt index bac59b4f6a..da60c68d1d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BangumiLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BangumiLoginActivity.kt @@ -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) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/ShikomoriLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/ShikimoriLoginActivity.kt similarity index 74% rename from app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/ShikomoriLoginActivity.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/ShikimoriLoginActivity.kt index 682f998606..25fed5e2d8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/ShikomoriLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/ShikimoriLoginActivity.kt @@ -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) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt index 296a7a5c12..62a4ac569a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt @@ -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) - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt index 0ba66ba40a..397b6c52d8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt @@ -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().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) } - }