From 7bc12c04c44811c75bd113174c990b40467e822d Mon Sep 17 00:00:00 2001 From: Carlos Date: Sat, 14 Mar 2020 20:00:16 -0400 Subject: [PATCH] More tracker clean up --- .../data/backup/BackupRestoreService.kt | 16 +- .../data/library/LibraryUpdateService.kt | 261 ++++++------ .../tachiyomi/data/track/TrackService.kt | 2 - .../tachiyomi/data/track/anilist/Anilist.kt | 94 ++--- .../data/track/anilist/AnilistApi.kt | 384 +++++++++--------- .../data/track/anilist/AnilistInterceptor.kt | 2 +- .../data/track/anilist/AnilistModels.kt | 11 +- .../tachiyomi/data/track/anilist/OAuth.kt | 10 - .../tachiyomi/data/track/bangumi/Bangumi.kt | 6 +- .../tachiyomi/data/track/kitsu/Kitsu.kt | 6 +- .../data/track/myanimelist/MyAnimeList.kt | 59 ++- .../data/track/myanimelist/MyAnimeListApi.kt | 134 +++--- .../myanimelist/MyAnimeListInterceptor.kt | 27 +- .../tachiyomi/data/track/shikimori/OAuth.kt | 13 - .../data/track/shikimori/Shikimori.kt | 90 ++-- .../data/track/shikimori/ShikimoriModels.kt | 12 + .../tachiyomi/network/OkHttpExtensions.kt | 3 + .../ui/manga/MangaDetailsController.kt | 3 + .../ui/setting/track/AnilistLoginActivity.kt | 6 + .../preference/LoginDialogPreference.kt | 20 +- .../widget/preference/TrackLoginDialog.kt | 27 +- 21 files changed, 610 insertions(+), 576 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/OAuth.kt 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() - - Observable.from(tracks) - .concatMap { track -> - val service = trackManager.getService(track.sync_id) - if (service != null && service in loggedServices) { - service.refresh(track) - .doOnNext { db.insertTrack(it).executeAsBlocking() } - .onErrorReturn { track } - } else { - Observable.empty() - } - } - .map { manga } - } - .doOnCompleted { - cancelProgressNotification() + mangaToUpdate.forEach { manga -> + showProgressNotification(manga, count++, mangaToUpdate.size) + + val tracks = db.getTracks(manga).executeAsBlocking() + + tracks.forEach { track -> + val service = trackManager.getService(track.sync_id) + if (service != null && service in loggedServices) { + try { + service.refresh(track) + db.insertTrack(track).executeAsBlocking() + } catch (e: Exception) { + Timber.e(e) + } } + } + } + cancelProgressNotification() } /** @@ -515,10 +520,12 @@ class LibraryUpdateService( * @param total the total progress. */ private fun showProgressNotification(manga: Manga, current: Int, total: Int) { - notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification + notificationManager.notify( + Notifications.ID_LIBRARY_PROGRESS, progressNotification .setContentTitle(manga.currentTitle()) .setProgress(total, current, false) - .build()) + .build() + ) } /** @@ -539,15 +546,17 @@ class LibraryUpdateService( .asBitmap().load(manga).dontTransform().centerCrop().circleCrop() .override(256, 256).submit().get() setLargeIcon(icon) + } catch (e: Exception) { } - catch (e: Exception) { } setGroupAlertBehavior(GROUP_ALERT_SUMMARY) setContentTitle(manga.currentTitle()) color = ContextCompat.getColor(this@LibraryUpdateService, R.color.colorAccent) val chaptersNames = if (chapterNames.size > 5) { "${chapterNames.take(4).joinToString(", ")}, " + - resources.getQuantityString(R.plurals.notification_and_n_more, - (chapterNames.size - 4), (chapterNames.size - 4)) + resources.getQuantityString( + R.plurals.notification_and_n_more, + (chapterNames.size - 4), (chapterNames.size - 4) + ) } else chapterNames.joinToString(", ") setContentText(chaptersNames) setStyle(NotificationCompat.BigTextStyle().bigText(chaptersNames)) @@ -558,41 +567,57 @@ class LibraryUpdateService( this@LibraryUpdateService, manga, chapters.first() ) ) - addAction(R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read), - NotificationReceiver.markAsReadPendingBroadcast(this@LibraryUpdateService, - manga, chapters, Notifications.ID_NEW_CHAPTERS)) - addAction(R.drawable.ic_book_white_24dp, getString(R.string.action_view_chapters), - NotificationReceiver.openChapterPendingActivity(this@LibraryUpdateService, - manga, Notifications.ID_NEW_CHAPTERS)) + addAction( + R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read), + NotificationReceiver.markAsReadPendingBroadcast( + this@LibraryUpdateService, + manga, chapters, Notifications.ID_NEW_CHAPTERS + ) + ) + addAction( + R.drawable.ic_book_white_24dp, getString(R.string.action_view_chapters), + NotificationReceiver.openChapterPendingActivity( + this@LibraryUpdateService, + manga, Notifications.ID_NEW_CHAPTERS + ) + ) setAutoCancel(true) }, manga.id.hashCode())) } NotificationManagerCompat.from(this).apply { - notify(Notifications.ID_NEW_CHAPTERS, notification(Notifications.CHANNEL_NEW_CHAPTERS) { - setSmallIcon(R.drawable.ic_tachi) - setLargeIcon(notificationBitmap) - setContentTitle(getString(R.string.notification_new_chapters)) - color = ContextCompat.getColor(applicationContext, R.color.colorAccent) - if (updates.size > 1) { - setContentText(resources.getQuantityString(R.plurals - .notification_new_chapters_text, - updates.size, updates.size)) - setStyle(NotificationCompat.BigTextStyle().bigText(updates.keys.joinToString("\n") { - it.currentTitle().chop(45) - })) - } - else { - setContentText(updates.keys.first().currentTitle().chop(45)) - } - priority = NotificationCompat.PRIORITY_HIGH - setGroup(Notifications.GROUP_NEW_CHAPTERS) - setGroupAlertBehavior(GROUP_ALERT_SUMMARY) - setGroupSummary(true) - setContentIntent(getNotificationIntent()) - setAutoCancel(true) - }) + notify( + Notifications.ID_NEW_CHAPTERS, + notification(Notifications.CHANNEL_NEW_CHAPTERS) { + setSmallIcon(R.drawable.ic_tachi) + setLargeIcon(notificationBitmap) + setContentTitle(getString(R.string.notification_new_chapters)) + color = ContextCompat.getColor(applicationContext, R.color.colorAccent) + if (updates.size > 1) { + setContentText( + resources.getQuantityString( + R.plurals + .notification_new_chapters_text, + updates.size, updates.size + ) + ) + setStyle( + NotificationCompat.BigTextStyle() + .bigText(updates.keys.joinToString("\n") { + it.currentTitle().chop(45) + }) + ) + } else { + setContentText(updates.keys.first().currentTitle().chop(45)) + } + priority = NotificationCompat.PRIORITY_HIGH + setGroup(Notifications.GROUP_NEW_CHAPTERS) + setGroupAlertBehavior(GROUP_ALERT_SUMMARY) + setGroupSummary(true) + setContentIntent(getNotificationIntent()) + setAutoCancel(true) + }) notifications.forEach { notify(it.second, it.first) 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 b668efaebd..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 @@ -37,8 +37,6 @@ abstract class TrackService(val id: Int) { abstract fun displayScore(track: Track): String - abstract suspend fun add(track: Track): Track - abstract suspend fun update(track: Track): Track abstract suspend fun bind(track: Track): Track 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 d3b5d78968..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,28 +8,11 @@ import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.model.TrackSearch +import timber.log.Timber import uy.kohesive.injekt.injectLazy class Anilist(private val context: Context, id: Int) : TrackService(id) { - companion object { - const val READING = 1 - const val COMPLETED = 2 - const val PAUSED = 3 - const val DROPPED = 4 - const val PLANNING = 5 - const val REPEATING = 6 - - const val DEFAULT_STATUS = READING - const val DEFAULT_SCORE = 0 - - const val POINT_100 = "POINT_100" - const val POINT_10 = "POINT_10" - const val POINT_10_DECIMAL = "POINT_10_DECIMAL" - const val POINT_5 = "POINT_5" - const val POINT_3 = "POINT_3" - } - override val name = "AniList" private val gson: Gson by injectLazy() @@ -54,9 +37,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { override fun getLogoColor() = Color.rgb(18, 25, 35) - override fun getStatusList(): List { - return listOf(READING, PLANNING, COMPLETED, REPEATING, PAUSED, DROPPED) - } + override fun getStatusList() = listOf(READING, PLANNING, COMPLETED, REPEATING, PAUSED, DROPPED) override fun getStatus(status: Int): String = with(context) { when (status) { @@ -93,13 +74,13 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { // 100 point POINT_100 -> index.toFloat() // 5 stars - POINT_5 -> when { - index == 0 -> 0f + POINT_5 -> when (index) { + 0 -> 0f else -> index * 20f - 10f } // Smiley - POINT_3 -> when { - index == 0 -> 0f + POINT_3 -> when (index) { + 0 -> 0f else -> index * 25f + 10f } // 10 point decimal @@ -112,8 +93,8 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { val score = track.score return when (scorePreference.getOrDefault()) { - POINT_5 -> when { - score == 0f -> "0 ★" + POINT_5 -> when (score) { + 0f -> "0 ★" else -> "${((score + 10) / 20).toInt()} ★" } POINT_3 -> when { @@ -126,10 +107,6 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { } } - override suspend fun add(track: Track): Track { - return api.addLibManga(track) - } - override suspend fun update(track: Track): Track { if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { track.status = COMPLETED @@ -137,34 +114,30 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { // If user was using API v1 fetch library_id if (track.library_id == null || track.library_id!! == 0L) { val libManga = api.findLibManga(track, getUsername().toInt()) + ?: throw Exception("$track not found on user library") - if (libManga == null) { - throw Exception("$track not found on user library") - } track.library_id = libManga.library_id } - return api.updateLibManga(track) + return api.updateLibraryManga(track) } override suspend fun bind(track: Track): Track { val remoteTrack = api.findLibManga(track, getUsername().toInt()) - if (remoteTrack != null) { + return if (remoteTrack != null) { track.copyPersonalFrom(remoteTrack) track.library_id = remoteTrack.library_id - return update(track) + update(track) } else { // Set default fields if it's not found in the list track.score = DEFAULT_SCORE.toFloat() track.status = DEFAULT_STATUS - return add(track) + api.addLibManga(track) } } - override suspend fun search(query: String): List { - return api.search(query) - } + override suspend fun search(query: String) = api.search(query) override suspend fun refresh(track: Track): Track { val remoteTrack = api.getLibManga(track, getUsername().toInt()) @@ -180,15 +153,16 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { val oauth = api.createOAuth(token) interceptor.setAuth(oauth) - try { - val currentUser = api.getCurrentUser() - scorePreference.set(currentUser.second) - saveCredentials(currentUser.first.toString(), oauth.access_token) - return true - } catch (e: Exception) { - logout() - return false - } + return try { + val currentUser = api.getCurrentUser() + scorePreference.set(currentUser.second) + saveCredentials(currentUser.first.toString(), oauth.access_token) + true + } catch (e: Exception) { + Timber.e(e) + logout() + false + } } override fun logout() { @@ -205,9 +179,29 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { return try { gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java) } catch (e: Exception) { + Timber.e(e) null } } + + companion object { + const val READING = 1 + const val COMPLETED = 2 + const val PAUSED = 3 + const val DROPPED = 4 + const val PLANNING = 5 + const val REPEATING = 6 + + const val DEFAULT_STATUS = READING + const val DEFAULT_SCORE = 0 + + const val POINT_100 = "POINT_100" + const val POINT_10 = "POINT_10" + const val POINT_10_DECIMAL = "POINT_10_DECIMAL" + const val POINT_5 = "POINT_5" + const val POINT_3 = "POINT_3" + } + } 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 6422556652..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,56 +11,218 @@ import com.google.gson.JsonObject import com.google.gson.JsonParser import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.model.TrackSearch -import eu.kanade.tachiyomi.network.await -import okhttp3.MediaType.Companion.toMediaTypeOrNull +import eu.kanade.tachiyomi.network.jsonType +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.MediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response import java.util.Calendar +import java.util.concurrent.TimeUnit class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { - private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull() private val authClient = client.newBuilder().addInterceptor(interceptor).build() suspend fun addLibManga(track: Track): Track { - val query = """ - |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { - |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { - | id - | status - |} - |} - |""".trimMargin() - val variables = jsonObject( - "mangaId" to track.media_id, - "progress" to track.last_chapter_read, - "status" to track.toAnilistStatus() - ) - val payload = jsonObject( - "query" to query, - "variables" to variables + return withContext(Dispatchers.IO) { + + val variables = jsonObject( + "mangaId" to track.media_id, + "progress" to track.last_chapter_read, + "status" to track.toAnilistStatus() + ) + val payload = jsonObject( + "query" to addToLibraryQuery(), + "variables" to variables + ) + val body = payload.toString().toRequestBody(MediaType.jsonType()) + val request = Request.Builder().url(apiUrl).post(body).build() + + val netResponse = authClient.newCall(request).execute() + + val responseBody = netResponse.body?.string().orEmpty() + netResponse.close() + if (responseBody.isEmpty()) { + throw Exception("Null Response") + } + val response = JsonParser.parseString(responseBody).obj + track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong + track + } + } + + suspend fun updateLibraryManga(track: Track): Track { + return withContext(Dispatchers.IO) { + val variables = jsonObject( + "listId" to track.library_id, + "progress" to track.last_chapter_read, + "status" to track.toAnilistStatus(), + "score" to track.score.toInt() + ) + val payload = jsonObject( + "query" to updateInLibraryQuery(), + "variables" to variables + ) + val body = payload.toString().toRequestBody(MediaType.jsonType()) + val request = Request.Builder().url(apiUrl).post(body).build() + val response = authClient.newCall(request).execute() + + track + } + } + + suspend fun search(search: String): List { + return withContext(Dispatchers.IO) { + val variables = jsonObject( + "query" to search + ) + val payload = jsonObject( + "query" to searchQuery(), + "variables" to variables + ) + val body = payload.toString().toRequestBody(MediaType.jsonType()) + val request = Request.Builder().url(apiUrl).post(body).build() + val netResponse = authClient.newCall(request).execute() + val response = responseToJson(netResponse) + + val media = response["data"]!!.obj["Page"].obj["mediaList"].array + val entries = media.map { jsonToALManga(it.obj) } + entries.map { it.toTrack() } + } + } + + suspend fun findLibManga(track: Track, userid: Int): Track? { + + return withContext(Dispatchers.IO) { + val variables = jsonObject( + "id" to userid, + "manga_id" to track.media_id + ) + val payload = jsonObject( + "query" to findLibraryMangaQuery(), + "variables" to variables + ) + val body = payload.toString().toRequestBody(MediaType.jsonType()) + val request = Request.Builder().url(apiUrl).post(body).build() + val result = authClient.newCall(request).execute() + + result.let { resp -> + val response = responseToJson(resp) + val media = response["data"]!!.obj["Page"].obj["mediaList"].array + val entries = media.map { jsonToALUserManga(it.obj) } + + entries.firstOrNull()?.toTrack() + } + } + } + + suspend fun getLibManga(track: Track, userid: Int): Track { + val remoteTrack = findLibManga(track, userid) + if (remoteTrack == null) { + throw Exception("Could not find manga") + } else { + return remoteTrack + } + } + + fun createOAuth(token: String): OAuth { + return OAuth( + token, + "Bearer", + System.currentTimeMillis() + TimeUnit.DAYS.toMillis(365), + TimeUnit.DAYS.toMillis(365) ) - val body = payload.toString().toRequestBody(jsonMime) - val request = Request.Builder() - .url(apiUrl) - .post(body) - .build() - val netResponse = authClient.newCall(request).await() + } + suspend fun getCurrentUser(): Pair { + return withContext(Dispatchers.IO) { + val payload = jsonObject( + "query" to currentUserQuery() + ) + val body = payload.toString().toRequestBody(MediaType.jsonType()) + val request = Request.Builder().url(apiUrl).post(body).build() + val netResponse = authClient.newCall(request).execute() + + val response = responseToJson(netResponse) + val viewer = response["data"]!!.obj["Viewer"].obj + + Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString) + } + } + + private fun responseToJson(netResponse: Response): JsonObject { val responseBody = netResponse.body?.string().orEmpty() - netResponse.close() + if (responseBody.isEmpty()) { throw Exception("Null Response") } - val response = JsonParser().parse(responseBody).obj - track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong - return track + return JsonParser.parseString(responseBody).obj + } + + private fun jsonToALManga(struct: JsonObject): ALManga { + val date = try { + val date = Calendar.getInstance() + date.set( + struct["startDate"]["year"].nullInt ?: 0, + (struct["startDate"]["month"].nullInt ?: 0) - 1, + struct["startDate"]["day"].nullInt ?: 0 + ) + date.timeInMillis + } catch (_: Exception) { + 0L + } + + return ALManga( + struct["id"].asInt, + struct["title"]["romaji"].asString, + struct["coverImage"]["large"].asString, + struct["description"].nullString.orEmpty(), + struct["type"].asString, + struct["status"].asString, + date, + struct["chapters"].nullInt ?: 0 + ) + } + + private fun jsonToALUserManga(struct: JsonObject): ALUserManga { + return ALUserManga( + struct["id"].asLong, + struct["status"].asString, + struct["scoreRaw"].asInt, + struct["progress"].asInt, + jsonToALManga(struct["media"].obj) + ) } - suspend fun updateLibManga(track: Track): Track { - val query = """ + companion object { + private const val clientId = "385" + private const val apiUrl = "https://graphql.anilist.co/" + private const val baseUrl = "https://anilist.co/api/v2/" + private const val baseMangaUrl = "https://anilist.co/manga/" + + fun mangaUrl(mediaId: Int): String { + return baseMangaUrl + mediaId + } + + fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon() + .appendQueryParameter("client_id", clientId) + .appendQueryParameter("response_type", "token") + .build()!! + + fun addToLibraryQuery() = """ + |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { + |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { + | id + | status + |} + |} + |""".trimMargin() + + fun updateInLibraryQuery() = """ |mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) { |SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) { |id @@ -69,27 +231,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { |} |} |""".trimMargin() - val variables = jsonObject( - "listId" to track.library_id, - "progress" to track.last_chapter_read, - "status" to track.toAnilistStatus(), - "score" to track.score.toInt() - ) - val payload = jsonObject( - "query" to query, - "variables" to variables - ) - val body = payload.toString().toRequestBody(jsonMime) - val request = Request.Builder() - .url(apiUrl) - .post(body) - .build() - authClient.newCall(request).execute() - return track - } - suspend fun search(search: String): List { - val query = """ + fun searchQuery() = """ |query Search(${'$'}query: String) { |Page (perPage: 50) { |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { @@ -113,33 +256,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { |} |} |""".trimMargin() - val variables = jsonObject( - "query" to search - ) - val payload = jsonObject( - "query" to query, - "variables" to variables - ) - val body = payload.toString().toRequestBody(jsonMime) - val request = Request.Builder() - .url(apiUrl) - .post(body) - .build() - val netResponse = authClient.newCall(request).await() - val responseBody = netResponse.body?.string().orEmpty() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - val response = JsonParser().parse(responseBody).obj - val data = response["data"]!!.obj - val page = data["Page"].obj - val media = page["media"].array - val entries = media.map { jsonToALManga(it.obj) } - return entries.map { it.toTrack() } - } - suspend fun findLibManga(track: Track, userid: Int): Track? { - val query = """ + fun findLibraryMangaQuery() = """ |query (${'$'}id: Int!, ${'$'}manga_id: Int!) { |Page { |mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) { @@ -169,49 +287,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { |} |} |""".trimMargin() - val variables = jsonObject( - "id" to userid, - "manga_id" to track.media_id - ) - val payload = jsonObject( - "query" to query, - "variables" to variables - ) - val body = payload.toString().toRequestBody(jsonMime) - val request = Request.Builder() - .url(apiUrl) - .post(body) - .build() - val result = authClient.newCall(request).await() - return result.let { resp -> - val responseBody = resp.body?.string().orEmpty() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - val response = JsonParser().parse(responseBody).obj - val data = response["data"]!!.obj - val page = data["Page"].obj - val media = page["mediaList"].array - val entries = media.map { jsonToALUserManga(it.obj) } - entries.firstOrNull()?.toTrack() - } - } - suspend fun getLibManga(track: Track, userid: Int): Track { - val track = findLibManga(track, userid) - if (track == null) { - throw Exception("Could not find manga") - } else { - return track - } - } - - fun createOAuth(token: String): OAuth { - return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000) - } - - suspend fun getCurrentUser(): Pair { - val query = """ + fun currentUserQuery() = """ |query User { |Viewer { |id @@ -221,74 +298,5 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { |} |} |""".trimMargin() - val payload = jsonObject( - "query" to query - ) - val body = payload.toString().toRequestBody(jsonMime) - val request = Request.Builder() - .url(apiUrl) - .post(body) - .build() - val netResponse = authClient.newCall(request).await() - - val responseBody = netResponse.body?.string().orEmpty() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - val response = JsonParser().parse(responseBody).obj - val data = response["data"]!!.obj - val viewer = data["Viewer"].obj - return Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString) - } - - private fun jsonToALManga(struct: JsonObject): ALManga { - val date = try { - val date = Calendar.getInstance() - date.set( - struct["startDate"]["year"].nullInt ?: 0, - (struct["startDate"]["month"].nullInt ?: 0) - 1, - struct["startDate"]["day"].nullInt ?: 0 - ) - date.timeInMillis - } catch (_: Exception) { - 0L - } - - return ALManga( - struct["id"].asInt, - struct["title"]["romaji"].asString, - struct["coverImage"]["large"].asString, - struct["description"].nullString.orEmpty(), - struct["type"].asString, - struct["status"].asString, - date, - struct["chapters"].nullInt ?: 0 - ) - } - - private fun jsonToALUserManga(struct: JsonObject): ALUserManga { - return ALUserManga( - struct["id"].asLong, - struct["status"].asString, - struct["scoreRaw"].asInt, - struct["progress"].asInt, - jsonToALManga(struct["media"].obj) - ) - } - - companion object { - private const val clientId = "385" - private const val apiUrl = "https://graphql.anilist.co/" - private const val baseUrl = "https://anilist.co/api/v2/" - private const val baseMangaUrl = "https://anilist.co/manga/" - - fun mangaUrl(mediaId: Int): String { - return baseMangaUrl + mediaId - } - - fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon() - .appendQueryParameter("client_id", clientId) - .appendQueryParameter("response_type", "token") - .build() } } 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/Bangumi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt index 35bbff47e5..2af8d3dfda 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 @@ -28,10 +28,6 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) { return track.score.toInt().toString() } - override suspend fun add(track: Track): Track { - return api.addLibManga(track) - } - override suspend fun update(track: Track): Track { if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { track.status = COMPLETED @@ -51,7 +47,7 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) { } else { track.score = DEFAULT_SCORE.toFloat() track.status = DEFAULT_STATUS - add(track) + api.addLibManga(track) update(track) } return track 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 40a431e7d1..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 @@ -69,10 +69,6 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { return df.format(track.score) } - override suspend fun add(track: Track): Track { - return api.addLibManga(track, getUserId()) - } - override suspend fun update(track: Track): Track { if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { track.status = COMPLETED @@ -90,7 +86,7 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { } else { track.score = DEFAULT_SCORE track.status = DEFAULT_STATUS - return add(track) + return api.addLibManga(track, getUserId()) } } 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 3c3efe5e37..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 @@ -8,29 +8,14 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.model.TrackSearch import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import timber.log.Timber -class Myanimelist(private val context: Context, id: Int) : TrackService(id) { - - companion object { - const val READING = 1 - const val COMPLETED = 2 - const val ON_HOLD = 3 - const val DROPPED = 4 - const val PLAN_TO_READ = 6 - - const val DEFAULT_STATUS = READING - const val DEFAULT_SCORE = 0 - - const val BASE_URL = "https://myanimelist.net" - const val USER_SESSION_COOKIE = "MALSESSIONID" - const val LOGGED_IN_COOKIE = "is_logged_in" - } +class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { private val interceptor by lazy { MyAnimeListInterceptor(this) } private val api by lazy { MyAnimeListApi(client, interceptor) } - override val name: String - get() = "MyAnimeList" + override val name = "MyAnimeList" override fun getLogo() = R.drawable.tracker_mal @@ -59,10 +44,6 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) { return track.score.toInt().toString() } - override suspend fun add(track: Track): Track { - return api.addLibManga(track) - } - override suspend fun update(track: Track): Track { if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { track.status = COMPLETED @@ -80,7 +61,7 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) { // Set default fields if it's not found in the list track.score = DEFAULT_SCORE.toFloat() track.status = DEFAULT_STATUS - add(track) + return api.addLibManga(track) } return track } @@ -98,18 +79,19 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) { override suspend fun login(username: String, password: String): Boolean { logout() - try { + return try { val csrf = api.login(username, password) saveCSRF(csrf) saveCredentials(username, password) - return true + true } catch (e: Exception) { + Timber.e(e) logout() - return false + false } } - fun refreshLogin() { + private suspend fun refreshLogin() { val username = getUsername() val password = getPassword() logout() @@ -119,13 +101,14 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) { saveCSRF(csrf) saveCredentials(username, password) } catch (e: Exception) { + Timber.e(e) logout() throw e } } // Attempt to login again if cookies have been cleared but credentials are still filled - fun ensureLoggedIn() { + suspend fun ensureLoggedIn() { if (isAuthorized) return if (!isLogged) throw Exception("MAL Login Credentials not found") @@ -138,10 +121,7 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) { networkService.cookieManager.remove(BASE_URL.toHttpUrlOrNull()!!) } - val isAuthorized: Boolean - get() = super.isLogged && - getCSRF().isNotEmpty() && - checkCookies() + private val isAuthorized = super.isLogged && getCSRF().isNotEmpty() && checkCookies() fun getCSRF(): String = preferences.trackToken(this).getOrDefault() @@ -157,4 +137,19 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) { return ckCount == 2 } + + companion object { + const val READING = 1 + const val COMPLETED = 2 + const val ON_HOLD = 3 + const val DROPPED = 4 + const val PLAN_TO_READ = 6 + + const val DEFAULT_STATUS = READING + const val DEFAULT_SCORE = 0 + + const val BASE_URL = "https://myanimelist.net" + const val USER_SESSION_COOKIE = "MALSESSIONID" + const val LOGGED_IN_COOKIE = "is_logged_in" + } } 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 672bce8a11..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 @@ -11,6 +11,8 @@ import eu.kanade.tachiyomi.network.consumeBody import eu.kanade.tachiyomi.network.consumeXmlBody import eu.kanade.tachiyomi.util.selectInt import eu.kanade.tachiyomi.util.selectText +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import okhttp3.FormBody import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient @@ -27,35 +29,41 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI private val authClient = client.newBuilder().addInterceptor(interceptor).build() suspend fun search(query: String): List { - if (query.startsWith(PREFIX_MY)) { - val realQuery = query.removePrefix(PREFIX_MY) - return getList().filter { it.title.contains(realQuery, true) }.toList() - } else { - val realQuery = query.take(100) - val response = client.newCall(GET(searchUrl(realQuery))).await() - val matches = Jsoup.parse(response.consumeBody()) - .select("div.js-categories-seasonal.js-block-list.list") - .select("table").select("tbody") - .select("tr").drop(1) - - return matches.filter { row -> row.select(TD)[2].text() != "Novel" } - .map { row -> - TrackSearch.create(TrackManager.MYANIMELIST).apply { - title = row.searchTitle() - media_id = row.searchMediaId() - total_chapters = row.searchTotalChapters() - summary = row.searchSummary() - cover_url = row.searchCoverUrl() - tracking_url = mangaUrl(media_id) - publishing_status = row.searchPublishingStatus() - publishing_type = row.searchPublishingType() - start_date = row.searchStartDate() + return withContext(Dispatchers.IO) { + if (query.startsWith(PREFIX_MY)) { + queryUsersList(query) + } else { + val realQuery = query.take(100) + val response = client.newCall(GET(searchUrl(realQuery))).await() + val matches = Jsoup.parse(response.consumeBody()) + .select("div.js-categories-seasonal.js-block-list.list") + .select("table").select("tbody") + .select("tr").drop(1) + + matches.filter { row -> row.select(TD)[2].text() != "Novel" } + .map { row -> + TrackSearch.create(TrackManager.MYANIMELIST).apply { + title = row.searchTitle() + media_id = row.searchMediaId() + total_chapters = row.searchTotalChapters() + summary = row.searchSummary() + cover_url = row.searchCoverUrl() + tracking_url = mangaUrl(media_id) + publishing_status = row.searchPublishingStatus() + publishing_type = row.searchPublishingType() + start_date = row.searchStartDate() + } } - } - .toList() + .toList() + } } } + private suspend fun queryUsersList(query: String): List { + val realQuery = query.removePrefix(PREFIX_MY).take(100) + return getList().filter { it.title.contains(realQuery, true) }.toList() + } + suspend fun addLibManga(track: Track): Track { authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track))).await() return track @@ -67,24 +75,26 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI } suspend fun findLibManga(track: Track): Track? { - val response = authClient.newCall(GET(url = listEntryUrl(track.media_id))).await() - var libTrack: Track? = null - response.use { - if (it.priorResponse?.isRedirect != true) { - val trackForm = Jsoup.parse(it.consumeBody()) - - libTrack = Track.create(TrackManager.MYANIMELIST).apply { - last_chapter_read = - trackForm.select("#add_manga_num_read_chapters").`val`().toInt() - total_chapters = trackForm.select("#totalChap").text().toInt() - status = - trackForm.select("#add_manga_status > option[selected]").`val`().toInt() - score = trackForm.select("#add_manga_score > option[selected]").`val`() - .toFloatOrNull() ?: 0f + return withContext(Dispatchers.IO) { + val response = authClient.newCall(GET(url = listEntryUrl(track.media_id))).await() + var remoteTrack: Track? = null + response.use { + if (it.priorResponse?.isRedirect != true) { + val trackForm = Jsoup.parse(it.consumeBody()) + + remoteTrack = Track.create(TrackManager.MYANIMELIST).apply { + last_chapter_read = + trackForm.select("#add_manga_num_read_chapters").`val`().toInt() + total_chapters = trackForm.select("#totalChap").text().toInt() + status = + trackForm.select("#add_manga_status > option[selected]").`val`().toInt() + score = trackForm.select("#add_manga_score > option[selected]").`val`() + .toFloatOrNull() ?: 0f + } } } + remoteTrack } - return libTrack } suspend fun getLibManga(track: Track): Track { @@ -96,15 +106,15 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI } } - fun login(username: String, password: String): String { - val csrf = getSessionInfo() - - login(username, password, csrf) - - return csrf + suspend fun login(username: String, password: String): String { + return withContext(Dispatchers.IO) { + val csrf = getSessionInfo() + login(username, password, csrf) + csrf + } } - private fun getSessionInfo(): String { + private suspend fun getSessionInfo(): String { val response = client.newCall(GET(loginUrl())).execute() return Jsoup.parse(response.consumeBody()) @@ -112,13 +122,15 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .attr("content") } - private fun login(username: String, password: String, csrf: String) { - val response = - client.newCall(POST(url = loginUrl(), body = loginPostBody(username, password, csrf))) - .execute() + private suspend fun login(username: String, password: String, csrf: String) { + withContext(Dispatchers.IO) { + val response = + client.newCall(POST(loginUrl(), body = loginPostBody(username, password, csrf))) + .execute() - response.use { - if (response.priorResponse?.code != 302) throw Exception("Authentication error") + response.use { + if (response.priorResponse?.code != 302) throw Exception("Authentication error") + } } } @@ -140,13 +152,15 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI } private suspend fun getListUrl(): String { - val response = - authClient.newCall(POST(url = exportListUrl(), body = exportPostBody())).await() - - return baseUrl + Jsoup.parse(response.consumeBody()) - .select("div.goodresult") - .select("a") - .attr("href") + return withContext(Dispatchers.IO) { + val response = + authClient.newCall(POST(url = exportListUrl(), body = exportPostBody())).execute() + + baseUrl + Jsoup.parse(response.consumeBody()) + .select("div.goodresult") + .select("a") + .attr("href") + } } private suspend fun getListXml(url: String): Document { 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 { - override fun intercept(chain: Interceptor.Chain): Response { - myanimelist.ensureLoggedIn() - - val request = chain.request() - var response = chain.proceed(updateRequest(request)) + val scope = CoroutineScope(Job() + Dispatchers.Main) - if (response.code == 400) { - myanimelist.refreshLogin() - response = chain.proceed(updateRequest(request)) + override fun intercept(chain: Interceptor.Chain): Response { + scope.launch { + myanimelist.ensureLoggedIn() } + val request = chain.request() + return chain.proceed(updateRequest(request)) - return response } private fun updateRequest(request: Request): Request { @@ -46,13 +47,15 @@ class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor private fun updateFormBody(requestBody: RequestBody): RequestBody { val formString = bodyToString(requestBody) - return "$formString${if (formString.isNotEmpty()) "&" else ""}${MyAnimeListApi.CSRF}=${myanimelist.getCSRF()}".toRequestBody(requestBody.contentType()) + return "$formString${if (formString.isNotEmpty()) "&" else ""}${MyAnimeListApi.CSRF}=${myanimelist.getCSRF()}".toRequestBody( + requestBody.contentType() + ) } private fun updateJsonBody(requestBody: RequestBody): RequestBody { val jsonString = bodyToString(requestBody) val newBody = JSONObject(jsonString) - .put(MyAnimeListApi.CSRF, myanimelist.getCSRF()) + .put(MyAnimeListApi.CSRF, myanimelist.getCSRF()) return newBody.toString().toRequestBody(requestBody.contentType()) } 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 33df133ca0..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 @@ -12,6 +12,34 @@ import uy.kohesive.injekt.injectLazy class Shikimori(private val context: Context, id: Int) : TrackService(id) { + override val name = "Shikimori" + + private val gson: Gson by injectLazy() + + private val interceptor by lazy { ShikimoriInterceptor(this, gson) } + + private val api by lazy { ShikimoriApi(client, interceptor) } + + override fun getLogo() = R.drawable.tracker_shikimori + + override fun getLogoColor() = Color.rgb(40, 40, 40) + + override fun getStatusList(): List { + return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING) + } + + override fun getStatus(status: Int): String = with(context) { + when (status) { + READING -> getString(R.string.reading) + COMPLETED -> getString(R.string.completed) + ON_HOLD -> getString(R.string.on_hold) + DROPPED -> getString(R.string.dropped) + PLANNING -> getString(R.string.plan_to_read) + REPEATING -> getString(R.string.repeating) + else -> "" + } + } + override fun getScoreList(): List { return IntRange(0, 10).map(Int::toString) } @@ -20,10 +48,6 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) { return track.score.toInt().toString() } - override suspend fun add(track: Track): Track { - return api.addLibManga(track, getUsername()) - } - override suspend fun update(track: Track): Track { if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { track.status = COMPLETED @@ -42,14 +66,12 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) { // Set default fields if it's not found in the list track.score = DEFAULT_SCORE.toFloat() track.status = DEFAULT_STATUS - add(track) + return api.addLibManga(track, getUsername()) } return track } - override suspend fun search(query: String): List { - return api.search(query) - } + override suspend fun search(query: String) = api.search(query) override suspend fun refresh(track: Track): Track { val remoteTrack = api.findLibManga(track, getUsername()) @@ -61,46 +83,6 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) { return track } - companion object { - const val READING = 1 - const val COMPLETED = 2 - const val ON_HOLD = 3 - const val DROPPED = 4 - const val PLANNING = 5 - const val REPEATING = 6 - - const val DEFAULT_STATUS = READING - const val DEFAULT_SCORE = 0 - } - - override val name = "Shikimori" - - private val gson: Gson by injectLazy() - - private val interceptor by lazy { ShikimoriInterceptor(this, gson) } - - private val api by lazy { ShikimoriApi(client, interceptor) } - - override fun getLogo() = R.drawable.tracker_shikimori - - override fun getLogoColor() = Color.rgb(40, 40, 40) - - override fun getStatusList(): List { - return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING) - } - - override fun getStatus(status: Int): String = with(context) { - when (status) { - READING -> getString(R.string.reading) - COMPLETED -> getString(R.string.completed) - ON_HOLD -> getString(R.string.on_hold) - DROPPED -> getString(R.string.dropped) - PLANNING -> getString(R.string.plan_to_read) - REPEATING -> getString(R.string.repeating) - else -> "" - } - } - override suspend fun login(username: String, password: String) = login(password) suspend fun login(code: String): Boolean { @@ -136,4 +118,16 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) { preferences.trackToken(this).set(null) interceptor.newAuth(null) } + + companion object { + const val READING = 1 + const val COMPLETED = 2 + const val ON_HOLD = 3 + const val DROPPED = 4 + const val PLANNING = 5 + const val REPEATING = 6 + + const val DEFAULT_STATUS = READING + const val DEFAULT_SCORE = 0 + } } 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/network/OkHttpExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt index 7bb1a0d849..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,6 +2,7 @@ package eu.kanade.tachiyomi.network import kotlinx.coroutines.suspendCancellableCoroutine import okhttp3.* +import okhttp3.MediaType.Companion.toMediaTypeOrNull import rx.Observable import rx.Producer import rx.Subscription @@ -98,6 +99,8 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene return progressClient.newCall(request) } +fun MediaType.Companion.jsonType() : MediaType = "application/json; charset=utf-8".toMediaTypeOrNull()!! + fun Response.consumeBody(): String? { use { if (it.code != 200) throw Exception("HTTP error ${it.code}") 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 a2dade8ec1..6b2868ffe9 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 @@ -929,10 +930,12 @@ open class MangaDetailsController : BaseController, } fun trackRefreshError(error: Exception) { + Timber.e(error) trackingBottomSheet?.onRefreshError(error) } fun trackSearchError(error: Exception) { + Timber.e(error) trackingBottomSheet?.onSearchResultsError(error) } 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 1854f467b4..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 @@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.ui.main.MainActivity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import uy.kohesive.injekt.injectLazy @@ -40,6 +41,11 @@ class AnilistLoginActivity : AppCompatActivity() { } } + override fun onDestroy() { + super.onDestroy() + scope.cancel() + } + private fun returnToSettings() { finish() 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 f5e0f9644b..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,24 +13,27 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.widget.SimpleTextWatcher -import kotlinx.android.synthetic.main.pref_account_login.view.login -import kotlinx.android.synthetic.main.pref_account_login.view.password -import kotlinx.android.synthetic.main.pref_account_login.view.show_password -import kotlinx.android.synthetic.main.pref_account_login.view.username_label +import kotlinx.android.synthetic.main.pref_account_login.view.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import rx.Subscription import uy.kohesive.injekt.injectLazy -abstract class LoginDialogPreference(private val usernameLabel: String? = null, bundle: Bundle? = null) : - DialogController(bundle), CoroutineScope { +abstract class LoginDialogPreference( + private val usernameLabel: String? = null, + bundle: Bundle? = null +) : + DialogController(bundle) { var v: View? = null private set val preferences: PreferencesHelper by injectLazy() + val scope = CoroutineScope(Job() + Dispatchers.Main) + var requestSubscription: Subscription? = null open var canLogout = false @@ -49,7 +52,7 @@ abstract class LoginDialogPreference(private val usernameLabel: String? = null, return dialog } - open fun logout() { } + open fun logout() {} fun onViewCreated(view: View) { v = view.apply { @@ -79,7 +82,6 @@ abstract class LoginDialogPreference(private val usernameLabel: String? = null, } }) } - } override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { @@ -90,11 +92,11 @@ abstract class LoginDialogPreference(private val usernameLabel: String? = null, } open fun onDialogClosed() { + scope.cancel() requestSubscription?.unsubscribe() } protected abstract fun checkLogin() protected abstract fun setCredentialsOnView(view: View) - } 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 0b3d45bdd6..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,12 +7,9 @@ import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.util.system.toast import kotlinx.android.synthetic.main.pref_account_login.view.* -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import kotlin.coroutines.CoroutineContext class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) : LoginDialogPreference(usernameLabel, bundle) { @@ -32,11 +29,7 @@ class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) : password.setText(service.getPassword()) } - override val coroutineContext: CoroutineContext - get() = TODO("Not yet implemented") - override fun checkLogin() { - requestSubscription?.unsubscribe() v?.apply { if (username.text.isEmpty() || password.text.isEmpty()) @@ -46,24 +39,30 @@ class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) : val user = username.text.toString() val pass = password.text.toString() - launch { + scope.launch { try { - withContext(Dispatchers.IO) { - service.login(user, pass) - } - withContext(Dispatchers.Main) { + val result = service.login(user, pass) + if (result) { dialog?.dismiss() context.toast(R.string.login_success) + } else { + errorResult(this@apply) } } catch (error: Exception) { - login.progress = -1 - login.setText(R.string.unknown_error) + errorResult(this@apply) error.message?.let { context.toast(it) } } } } } + fun errorResult(view: View?) { + v?.apply { + login.progress = -1 + login.setText(R.string.unknown_error) + } + } + override fun logout() { if (service.isLogged) { service.logout()