mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Linting fixes
This commit is contained in:
		| @@ -13,7 +13,7 @@ import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|         Worker(context, workerParams) { | ||||
|     Worker(context, workerParams) { | ||||
|  | ||||
|     override fun doWork(): Result { | ||||
|         val preferences = Injekt.get<PreferencesHelper>() | ||||
| @@ -32,10 +32,11 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet | ||||
|             val interval = prefInterval ?: preferences.backupInterval().get() | ||||
|             if (interval > 0) { | ||||
|                 val request = PeriodicWorkRequestBuilder<BackupCreatorJob>( | ||||
|                         interval.toLong(), TimeUnit.HOURS, | ||||
|                         10, TimeUnit.MINUTES) | ||||
|                         .addTag(TAG) | ||||
|                         .build() | ||||
|                     interval.toLong(), TimeUnit.HOURS, | ||||
|                     10, TimeUnit.MINUTES | ||||
|                 ) | ||||
|                     .addTag(TAG) | ||||
|                     .build() | ||||
|  | ||||
|                 WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request) | ||||
|             } else { | ||||
|   | ||||
| @@ -85,7 +85,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|  | ||||
|     private fun initParser(): Gson = when (version) { | ||||
|         1 -> GsonBuilder().create() | ||||
|         2 -> GsonBuilder() | ||||
|         2 -> | ||||
|             GsonBuilder() | ||||
|                 .registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build()) | ||||
|                 .registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build()) | ||||
|                 .registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build()) | ||||
| @@ -142,21 +143,21 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|                 val numberOfBackups = numberOfBackups() | ||||
|                 val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""") | ||||
|                 dir.listFiles { _, filename -> backupRegex.matches(filename) } | ||||
|                         .orEmpty() | ||||
|                         .sortedByDescending { it.name } | ||||
|                         .drop(numberOfBackups - 1) | ||||
|                         .forEach { it.delete() } | ||||
|                     .orEmpty() | ||||
|                     .sortedByDescending { it.name } | ||||
|                     .drop(numberOfBackups - 1) | ||||
|                     .forEach { it.delete() } | ||||
|  | ||||
|                 // Create new file to place backup | ||||
|                 val newFile = dir.createFile(Backup.getDefaultFilename()) | ||||
|                         ?: throw Exception("Couldn't create backup file") | ||||
|                     ?: throw Exception("Couldn't create backup file") | ||||
|  | ||||
|                 newFile.openOutputStream().bufferedWriter().use { | ||||
|                     parser.toJson(root, it) | ||||
|                 } | ||||
|             } else { | ||||
|                 val file = UniFile.fromUri(context, uri) | ||||
|                         ?: throw Exception("Couldn't create backup file") | ||||
|                     ?: throw Exception("Couldn't create backup file") | ||||
|                 file.openOutputStream().bufferedWriter().use { | ||||
|                     parser.toJson(root, it) | ||||
|                 } | ||||
| @@ -268,13 +269,13 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      */ | ||||
|     fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> { | ||||
|         return source.fetchMangaDetails(manga) | ||||
|                 .map { networkManga -> | ||||
|                     manga.copyFrom(networkManga) | ||||
|                     manga.favorite = true | ||||
|                     manga.initialized = true | ||||
|                     manga.id = insertManga(manga) | ||||
|                     manga | ||||
|                 } | ||||
|             .map { networkManga -> | ||||
|                 manga.copyFrom(networkManga) | ||||
|                 manga.favorite = true | ||||
|                 manga.initialized = true | ||||
|                 manga.id = insertManga(manga) | ||||
|                 manga | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -286,13 +287,13 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      */ | ||||
|     fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> { | ||||
|         return source.fetchChapterList(manga) | ||||
|                 .map { syncChaptersWithSource(databaseHelper, it, manga, source) } | ||||
|                 .doOnNext { pair -> | ||||
|                     if (pair.first.isNotEmpty()) { | ||||
|                         chapters.forEach { it.manga_id = manga.id } | ||||
|                         insertChapters(chapters) | ||||
|                     } | ||||
|             .map { syncChaptersWithSource(databaseHelper, it, manga, source) } | ||||
|             .doOnNext { pair -> | ||||
|                 if (pair.first.isNotEmpty()) { | ||||
|                     chapters.forEach { it.manga_id = manga.id } | ||||
|                     insertChapters(chapters) | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -442,8 +443,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|         val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking() | ||||
|  | ||||
|         // Return if fetch is needed | ||||
|         if (dbChapters.isEmpty() || dbChapters.size < chapters.size) | ||||
|         if (dbChapters.isEmpty() || dbChapters.size < chapters.size) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         for (chapter in chapters) { | ||||
|             val pos = dbChapters.indexOf(chapter) | ||||
| @@ -468,7 +470,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      * @return [Manga], null if not found | ||||
|      */ | ||||
|     internal fun getMangaFromDatabase(manga: Manga): Manga? = | ||||
|             databaseHelper.getManga(manga.url, manga.source).executeAsBlocking() | ||||
|         databaseHelper.getManga(manga.url, manga.source).executeAsBlocking() | ||||
|  | ||||
|     /** | ||||
|      * Returns list containing manga from library | ||||
| @@ -476,7 +478,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      * @return [Manga] from library | ||||
|      */ | ||||
|     internal fun getFavoriteManga(): List<Manga> = | ||||
|             databaseHelper.getFavoriteMangas().executeAsBlocking() | ||||
|         databaseHelper.getFavoriteMangas().executeAsBlocking() | ||||
|  | ||||
|     /** | ||||
|      * Inserts manga and returns id | ||||
| @@ -484,7 +486,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      * @return id of [Manga], null if not found | ||||
|      */ | ||||
|     internal fun insertManga(manga: Manga): Long? = | ||||
|             databaseHelper.insertManga(manga).executeAsBlocking().insertedId() | ||||
|         databaseHelper.insertManga(manga).executeAsBlocking().insertedId() | ||||
|  | ||||
|     /** | ||||
|      * Inserts list of chapters | ||||
|   | ||||
| @@ -60,7 +60,7 @@ class BackupRestoreService : Service() { | ||||
|          * @return true if the service is running, false otherwise. | ||||
|          */ | ||||
|         private fun isRunning(context: Context): Boolean = | ||||
|                 context.isServiceRunning(BackupRestoreService::class.java) | ||||
|             context.isServiceRunning(BackupRestoreService::class.java) | ||||
|  | ||||
|         /** | ||||
|          * Starts a service to restore a backup from Json | ||||
| @@ -143,7 +143,8 @@ class BackupRestoreService : Service() { | ||||
|         startForeground(Notifications.ID_RESTORE, notifier.showRestoreProgress().build()) | ||||
|  | ||||
|         wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( | ||||
|                 PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock") | ||||
|             PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock" | ||||
|         ) | ||||
|         wakeLock.acquire() | ||||
|     } | ||||
|  | ||||
| @@ -182,12 +183,13 @@ class BackupRestoreService : Service() { | ||||
|         subscription?.unsubscribe() | ||||
|  | ||||
|         subscription = Observable.using( | ||||
|                 { db.lowLevel().beginTransaction() }, | ||||
|                 { getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } }, | ||||
|                 { executor.execute { db.lowLevel().endTransaction() } }) | ||||
|                 .doAfterTerminate { stopSelf(startId) } | ||||
|                 .subscribeOn(Schedulers.from(executor)) | ||||
|                 .subscribe() | ||||
|             { db.lowLevel().beginTransaction() }, | ||||
|             { getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } }, | ||||
|             { executor.execute { db.lowLevel().endTransaction() } } | ||||
|         ) | ||||
|             .doAfterTerminate { stopSelf(startId) } | ||||
|             .subscribeOn(Schedulers.from(executor)) | ||||
|             .subscribe() | ||||
|  | ||||
|         return START_NOT_STICKY | ||||
|     } | ||||
| @@ -202,79 +204,87 @@ class BackupRestoreService : Service() { | ||||
|         val startTime = System.currentTimeMillis() | ||||
|  | ||||
|         return Observable.just(Unit) | ||||
|                 .map { | ||||
|                     val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader()) | ||||
|                     val json = JsonParser.parseReader(reader).asJsonObject | ||||
|             .map { | ||||
|                 val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader()) | ||||
|                 val json = JsonParser.parseReader(reader).asJsonObject | ||||
|  | ||||
|                     // Get parser version | ||||
|                     val version = json.get(VERSION)?.asInt ?: 1 | ||||
|                 // Get parser version | ||||
|                 val version = json.get(VERSION)?.asInt ?: 1 | ||||
|  | ||||
|                     // Initialize manager | ||||
|                     backupManager = BackupManager(this, version) | ||||
|                 // Initialize manager | ||||
|                 backupManager = BackupManager(this, version) | ||||
|  | ||||
|                     val mangasJson = json.get(MANGAS).asJsonArray | ||||
|                 val mangasJson = json.get(MANGAS).asJsonArray | ||||
|  | ||||
|                     restoreAmount = mangasJson.size() + 1 // +1 for categories | ||||
|                     restoreProgress = 0 | ||||
|                     errors.clear() | ||||
|                 restoreAmount = mangasJson.size() + 1 // +1 for categories | ||||
|                 restoreProgress = 0 | ||||
|                 errors.clear() | ||||
|  | ||||
|                     // Restore categories | ||||
|                     json.get(CATEGORIES)?.let { | ||||
|                         backupManager.restoreCategories(it.asJsonArray) | ||||
|                         restoreProgress += 1 | ||||
|                         showRestoreProgress(restoreProgress, restoreAmount, "Categories added") | ||||
|                     } | ||||
|  | ||||
|                     mangasJson | ||||
|                 // Restore categories | ||||
|                 json.get(CATEGORIES)?.let { | ||||
|                     backupManager.restoreCategories(it.asJsonArray) | ||||
|                     restoreProgress += 1 | ||||
|                     showRestoreProgress(restoreProgress, restoreAmount, "Categories added") | ||||
|                 } | ||||
|                 .flatMap { Observable.from(it) } | ||||
|                 .concatMap { | ||||
|                     val obj = it.asJsonObject | ||||
|                     val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA)) | ||||
|                     val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(obj.get(CHAPTERS) | ||||
|                             ?: JsonArray()) | ||||
|                     val categories = backupManager.parser.fromJson<List<String>>(obj.get(CATEGORIES) | ||||
|                             ?: JsonArray()) | ||||
|                     val history = backupManager.parser.fromJson<List<DHistory>>(obj.get(HISTORY) | ||||
|                             ?: JsonArray()) | ||||
|                     val tracks = backupManager.parser.fromJson<List<TrackImpl>>(obj.get(TRACK) | ||||
|                             ?: JsonArray()) | ||||
|  | ||||
|                     val observable = getMangaRestoreObservable(manga, chapters, categories, history, tracks) | ||||
|                     if (observable != null) { | ||||
|                         observable | ||||
|                     } else { | ||||
|                         errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}") | ||||
|                         restoreProgress += 1 | ||||
|                         val content = getString(R.string.dialog_restoring_source_not_found, manga.title.chop(15)) | ||||
|                         showRestoreProgress(restoreProgress, restoreAmount, manga.title, content) | ||||
|                         Observable.just(manga) | ||||
|                     } | ||||
|                 mangasJson | ||||
|             } | ||||
|             .flatMap { Observable.from(it) } | ||||
|             .concatMap { | ||||
|                 val obj = it.asJsonObject | ||||
|                 val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA)) | ||||
|                 val chapters = backupManager.parser.fromJson<List<ChapterImpl>>( | ||||
|                     obj.get(CHAPTERS) | ||||
|                         ?: JsonArray() | ||||
|                 ) | ||||
|                 val categories = backupManager.parser.fromJson<List<String>>( | ||||
|                     obj.get(CATEGORIES) | ||||
|                         ?: JsonArray() | ||||
|                 ) | ||||
|                 val history = backupManager.parser.fromJson<List<DHistory>>( | ||||
|                     obj.get(HISTORY) | ||||
|                         ?: JsonArray() | ||||
|                 ) | ||||
|                 val tracks = backupManager.parser.fromJson<List<TrackImpl>>( | ||||
|                     obj.get(TRACK) | ||||
|                         ?: JsonArray() | ||||
|                 ) | ||||
|  | ||||
|                 val observable = getMangaRestoreObservable(manga, chapters, categories, history, tracks) | ||||
|                 if (observable != null) { | ||||
|                     observable | ||||
|                 } else { | ||||
|                     errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}") | ||||
|                     restoreProgress += 1 | ||||
|                     val content = getString(R.string.dialog_restoring_source_not_found, manga.title.chop(15)) | ||||
|                     showRestoreProgress(restoreProgress, restoreAmount, manga.title, content) | ||||
|                     Observable.just(manga) | ||||
|                 } | ||||
|                 .toList() | ||||
|                 .doOnNext { | ||||
|                     val endTime = System.currentTimeMillis() | ||||
|                     val time = endTime - startTime | ||||
|                     val logFile = writeErrorLog() | ||||
|                     val completeIntent = Intent(BackupConst.INTENT_FILTER).apply { | ||||
|                         putExtra(BackupConst.EXTRA_TIME, time) | ||||
|                         putExtra(BackupConst.EXTRA_ERRORS, errors.size) | ||||
|                         putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent) | ||||
|                         putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name) | ||||
|                         putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED) | ||||
|                     } | ||||
|                     sendLocalBroadcast(completeIntent) | ||||
|             } | ||||
|             .toList() | ||||
|             .doOnNext { | ||||
|                 val endTime = System.currentTimeMillis() | ||||
|                 val time = endTime - startTime | ||||
|                 val logFile = writeErrorLog() | ||||
|                 val completeIntent = Intent(BackupConst.INTENT_FILTER).apply { | ||||
|                     putExtra(BackupConst.EXTRA_TIME, time) | ||||
|                     putExtra(BackupConst.EXTRA_ERRORS, errors.size) | ||||
|                     putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent) | ||||
|                     putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name) | ||||
|                     putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED) | ||||
|                 } | ||||
|                 .doOnError { error -> | ||||
|                     Timber.e(error) | ||||
|                     writeErrorLog() | ||||
|                     val errorIntent = Intent(BackupConst.INTENT_FILTER).apply { | ||||
|                         putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_ERROR) | ||||
|                         putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message) | ||||
|                     } | ||||
|                     sendLocalBroadcast(errorIntent) | ||||
|                 sendLocalBroadcast(completeIntent) | ||||
|             } | ||||
|             .doOnError { error -> | ||||
|                 Timber.e(error) | ||||
|                 writeErrorLog() | ||||
|                 val errorIntent = Intent(BackupConst.INTENT_FILTER).apply { | ||||
|                     putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_ERROR) | ||||
|                     putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message) | ||||
|                 } | ||||
|                 .onErrorReturn { emptyList() } | ||||
|                 sendLocalBroadcast(errorIntent) | ||||
|             } | ||||
|             .onErrorReturn { emptyList() } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -347,28 +357,28 @@ class BackupRestoreService : Service() { | ||||
|         tracks: List<Track> | ||||
|     ): Observable<Manga> { | ||||
|         return backupManager.restoreMangaFetchObservable(source, manga) | ||||
|                 .onErrorReturn { | ||||
|                     errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                     manga | ||||
|                 } | ||||
|                 .filter { it.id != null } | ||||
|                 .flatMap { | ||||
|                     chapterFetchObservable(source, it, chapters) | ||||
|                             // Convert to the manga that contains new chapters. | ||||
|                             .map { manga } | ||||
|                 } | ||||
|                 .doOnNext { | ||||
|                     restoreExtraForManga(it, categories, history, tracks) | ||||
|                 } | ||||
|                 .flatMap { | ||||
|                     trackingFetchObservable(it, tracks) | ||||
|                             // Convert to the manga that contains new chapters. | ||||
|                             .map { manga } | ||||
|                 } | ||||
|                 .doOnCompleted { | ||||
|                     restoreProgress += 1 | ||||
|                     showRestoreProgress(restoreProgress, restoreAmount, manga.title) | ||||
|                 } | ||||
|             .onErrorReturn { | ||||
|                 errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                 manga | ||||
|             } | ||||
|             .filter { it.id != null } | ||||
|             .flatMap { | ||||
|                 chapterFetchObservable(source, it, chapters) | ||||
|                     // Convert to the manga that contains new chapters. | ||||
|                     .map { manga } | ||||
|             } | ||||
|             .doOnNext { | ||||
|                 restoreExtraForManga(it, categories, history, tracks) | ||||
|             } | ||||
|             .flatMap { | ||||
|                 trackingFetchObservable(it, tracks) | ||||
|                     // Convert to the manga that contains new chapters. | ||||
|                     .map { manga } | ||||
|             } | ||||
|             .doOnCompleted { | ||||
|                 restoreProgress += 1 | ||||
|                 showRestoreProgress(restoreProgress, restoreAmount, manga.title) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private fun mangaNoFetchObservable( | ||||
| @@ -379,28 +389,27 @@ class BackupRestoreService : Service() { | ||||
|         history: List<DHistory>, | ||||
|         tracks: List<Track> | ||||
|     ): Observable<Manga> { | ||||
|  | ||||
|         return Observable.just(backupManga) | ||||
|                 .flatMap { manga -> | ||||
|                     if (!backupManager.restoreChaptersForManga(manga, chapters)) { | ||||
|                         chapterFetchObservable(source, manga, chapters) | ||||
|                                 .map { manga } | ||||
|                     } else { | ||||
|                         Observable.just(manga) | ||||
|                     } | ||||
|                 } | ||||
|                 .doOnNext { | ||||
|                     restoreExtraForManga(it, categories, history, tracks) | ||||
|                 } | ||||
|                 .flatMap { manga -> | ||||
|                     trackingFetchObservable(manga, tracks) | ||||
|                             // Convert to the manga that contains new chapters. | ||||
|                             .map { manga } | ||||
|                 } | ||||
|                 .doOnCompleted { | ||||
|                     restoreProgress += 1 | ||||
|                     showRestoreProgress(restoreProgress, restoreAmount, backupManga.title) | ||||
|             .flatMap { manga -> | ||||
|                 if (!backupManager.restoreChaptersForManga(manga, chapters)) { | ||||
|                     chapterFetchObservable(source, manga, chapters) | ||||
|                         .map { manga } | ||||
|                 } else { | ||||
|                     Observable.just(manga) | ||||
|                 } | ||||
|             } | ||||
|             .doOnNext { | ||||
|                 restoreExtraForManga(it, categories, history, tracks) | ||||
|             } | ||||
|             .flatMap { manga -> | ||||
|                 trackingFetchObservable(manga, tracks) | ||||
|                     // Convert to the manga that contains new chapters. | ||||
|                     .map { manga } | ||||
|             } | ||||
|             .doOnCompleted { | ||||
|                 restoreProgress += 1 | ||||
|                 showRestoreProgress(restoreProgress, restoreAmount, backupManga.title) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) { | ||||
| @@ -423,11 +432,11 @@ class BackupRestoreService : Service() { | ||||
|      */ | ||||
|     private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> { | ||||
|         return backupManager.restoreChapterFetchObservable(source, manga, chapters) | ||||
|                 // If there's any error, return empty update and continue. | ||||
|                 .onErrorReturn { | ||||
|                     errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                     Pair(emptyList(), emptyList()) | ||||
|                 } | ||||
|             // If there's any error, return empty update and continue. | ||||
|             .onErrorReturn { | ||||
|                 errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                 Pair(emptyList(), emptyList()) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -438,20 +447,20 @@ class BackupRestoreService : Service() { | ||||
|      */ | ||||
|     private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> { | ||||
|         return Observable.from(tracks) | ||||
|                 .concatMap { track -> | ||||
|                     val service = trackManager.getService(track.sync_id) | ||||
|                     if (service != null && service.isLogged) { | ||||
|                         service.refresh(track) | ||||
|                                 .doOnNext { db.insertTrack(it).executeAsBlocking() } | ||||
|                                 .onErrorReturn { | ||||
|                                     errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                                     track | ||||
|                                 } | ||||
|                     } else { | ||||
|                         errors.add(Date() to "${manga.title} - ${service?.name} not logged in") | ||||
|                         Observable.empty() | ||||
|                     } | ||||
|             .concatMap { track -> | ||||
|                 val service = trackManager.getService(track.sync_id) | ||||
|                 if (service != null && service.isLogged) { | ||||
|                     service.refresh(track) | ||||
|                         .doOnNext { db.insertTrack(it).executeAsBlocking() } | ||||
|                         .onErrorReturn { | ||||
|                             errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                             track | ||||
|                         } | ||||
|                 } else { | ||||
|                     errors.add(Date() to "${manga.title} - ${service?.name} not logged in") | ||||
|                     Observable.empty() | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -46,10 +46,12 @@ class ChapterCache(private val context: Context) { | ||||
|     private val gson: Gson by injectLazy() | ||||
|  | ||||
|     /** Cache class used for cache management.  */ | ||||
|     private val diskCache = DiskLruCache.open(File(context.cacheDir, PARAMETER_CACHE_DIRECTORY), | ||||
|             PARAMETER_APP_VERSION, | ||||
|             PARAMETER_VALUE_COUNT, | ||||
|             PARAMETER_CACHE_SIZE) | ||||
|     private val diskCache = DiskLruCache.open( | ||||
|         File(context.cacheDir, PARAMETER_CACHE_DIRECTORY), | ||||
|         PARAMETER_APP_VERSION, | ||||
|         PARAMETER_VALUE_COUNT, | ||||
|         PARAMETER_CACHE_SIZE | ||||
|     ) | ||||
|  | ||||
|     /** | ||||
|      * Returns directory of cache. | ||||
| @@ -77,8 +79,9 @@ class ChapterCache(private val context: Context) { | ||||
|      */ | ||||
|     fun removeFileFromCache(file: String): Boolean { | ||||
|         // Make sure we don't delete the journal file (keeps track of cache). | ||||
|         if (file == "journal" || file.startsWith("journal.")) | ||||
|         if (file == "journal" || file.startsWith("journal.")) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         return try { | ||||
|             // Remove the extension from the file to get the key of the cache | ||||
|   | ||||
| @@ -21,7 +21,7 @@ class CoverCache(private val context: Context) { | ||||
|      * Cache directory used for cache management. | ||||
|      */ | ||||
|     private val cacheDir = context.getExternalFilesDir("covers") | ||||
|             ?: File(context.filesDir, "covers").also { it.mkdirs() } | ||||
|         ?: File(context.filesDir, "covers").also { it.mkdirs() } | ||||
|  | ||||
|     /** | ||||
|      * Returns the cover from cache. | ||||
| @@ -56,8 +56,9 @@ class CoverCache(private val context: Context) { | ||||
|      */ | ||||
|     fun deleteFromCache(thumbnailUrl: String?): Boolean { | ||||
|         // Check if url is empty. | ||||
|         if (thumbnailUrl.isNullOrEmpty()) | ||||
|         if (thumbnailUrl.isNullOrEmpty()) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         // Remove file. | ||||
|         val file = getCoverFile(thumbnailUrl) | ||||
|   | ||||
| @@ -30,19 +30,19 @@ open class DatabaseHelper(context: Context) : | ||||
|     MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries { | ||||
|  | ||||
|     private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) | ||||
|             .name(DbOpenCallback.DATABASE_NAME) | ||||
|             .callback(DbOpenCallback()) | ||||
|             .build() | ||||
|         .name(DbOpenCallback.DATABASE_NAME) | ||||
|         .callback(DbOpenCallback()) | ||||
|         .build() | ||||
|  | ||||
|     override val db = DefaultStorIOSQLite.builder() | ||||
|             .sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration)) | ||||
|             .addTypeMapping(Manga::class.java, MangaTypeMapping()) | ||||
|             .addTypeMapping(Chapter::class.java, ChapterTypeMapping()) | ||||
|             .addTypeMapping(Track::class.java, TrackTypeMapping()) | ||||
|             .addTypeMapping(Category::class.java, CategoryTypeMapping()) | ||||
|             .addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping()) | ||||
|             .addTypeMapping(History::class.java, HistoryTypeMapping()) | ||||
|             .build() | ||||
|         .sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration)) | ||||
|         .addTypeMapping(Manga::class.java, MangaTypeMapping()) | ||||
|         .addTypeMapping(Chapter::class.java, ChapterTypeMapping()) | ||||
|         .addTypeMapping(Track::class.java, TrackTypeMapping()) | ||||
|         .addTypeMapping(Category::class.java, CategoryTypeMapping()) | ||||
|         .addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping()) | ||||
|         .addTypeMapping(History::class.java, HistoryTypeMapping()) | ||||
|         .build() | ||||
|  | ||||
|     inline fun inTransaction(block: () -> Unit) = db.inTransaction(block) | ||||
|  | ||||
|   | ||||
| @@ -44,8 +44,10 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { | ||||
|             db.execSQL(ChapterTable.sourceOrderUpdateQuery) | ||||
|  | ||||
|             // Fix kissmanga covers after supporting cloudflare | ||||
|             db.execSQL("""UPDATE mangas SET thumbnail_url = | ||||
|                     REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""") | ||||
|             db.execSQL( | ||||
|                 """UPDATE mangas SET thumbnail_url = | ||||
|                     REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""" | ||||
|             ) | ||||
|         } | ||||
|         if (oldVersion < 3) { | ||||
|             // Initialize history tables | ||||
|   | ||||
| @@ -18,22 +18,22 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ORDER | ||||
| import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE | ||||
|  | ||||
| class CategoryTypeMapping : SQLiteTypeMapping<Category>( | ||||
|         CategoryPutResolver(), | ||||
|         CategoryGetResolver(), | ||||
|         CategoryDeleteResolver() | ||||
|     CategoryPutResolver(), | ||||
|     CategoryGetResolver(), | ||||
|     CategoryDeleteResolver() | ||||
| ) | ||||
|  | ||||
| class CategoryPutResolver : DefaultPutResolver<Category>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: Category) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: Category) = ContentValues(4).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -56,8 +56,8 @@ class CategoryGetResolver : DefaultGetResolver<Category>() { | ||||
| class CategoryDeleteResolver : DefaultDeleteResolver<Category>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -26,22 +26,22 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL | ||||
| import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE | ||||
|  | ||||
| class ChapterTypeMapping : SQLiteTypeMapping<Chapter>( | ||||
|         ChapterPutResolver(), | ||||
|         ChapterGetResolver(), | ||||
|         ChapterDeleteResolver() | ||||
|     ChapterPutResolver(), | ||||
|     ChapterGetResolver(), | ||||
|     ChapterDeleteResolver() | ||||
| ) | ||||
|  | ||||
| class ChapterPutResolver : DefaultPutResolver<Chapter>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -80,8 +80,8 @@ class ChapterGetResolver : DefaultGetResolver<Chapter>() { | ||||
| class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -18,22 +18,22 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_TIME_READ | ||||
| import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE | ||||
|  | ||||
| class HistoryTypeMapping : SQLiteTypeMapping<History>( | ||||
|         HistoryPutResolver(), | ||||
|         HistoryGetResolver(), | ||||
|         HistoryDeleteResolver() | ||||
|     HistoryPutResolver(), | ||||
|     HistoryGetResolver(), | ||||
|     HistoryDeleteResolver() | ||||
| ) | ||||
|  | ||||
| open class HistoryPutResolver : DefaultPutResolver<History>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: History) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: History) = ContentValues(4).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -56,8 +56,8 @@ class HistoryGetResolver : DefaultGetResolver<History>() { | ||||
| class HistoryDeleteResolver : DefaultDeleteResolver<History>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -16,22 +16,22 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_MANGA_ID | ||||
| import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE | ||||
|  | ||||
| class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>( | ||||
|         MangaCategoryPutResolver(), | ||||
|         MangaCategoryGetResolver(), | ||||
|         MangaCategoryDeleteResolver() | ||||
|     MangaCategoryPutResolver(), | ||||
|     MangaCategoryGetResolver(), | ||||
|     MangaCategoryDeleteResolver() | ||||
| ) | ||||
|  | ||||
| class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -52,8 +52,8 @@ class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() { | ||||
| class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -29,22 +29,22 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_VIEWER | ||||
| import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE | ||||
|  | ||||
| class MangaTypeMapping : SQLiteTypeMapping<Manga>( | ||||
|         MangaPutResolver(), | ||||
|         MangaGetResolver(), | ||||
|         MangaDeleteResolver() | ||||
|     MangaPutResolver(), | ||||
|     MangaGetResolver(), | ||||
|     MangaDeleteResolver() | ||||
| ) | ||||
|  | ||||
| class MangaPutResolver : DefaultPutResolver<Manga>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: Manga) = ContentValues(15).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -95,8 +95,8 @@ open class MangaGetResolver : DefaultGetResolver<Manga>(), BaseMangaGetResolver | ||||
| class MangaDeleteResolver : DefaultDeleteResolver<Manga>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -27,22 +27,22 @@ import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TRACKING_URL | ||||
| import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE | ||||
|  | ||||
| class TrackTypeMapping : SQLiteTypeMapping<Track>( | ||||
|         TrackPutResolver(), | ||||
|         TrackGetResolver(), | ||||
|         TrackDeleteResolver() | ||||
|     TrackPutResolver(), | ||||
|     TrackGetResolver(), | ||||
|     TrackDeleteResolver() | ||||
| ) | ||||
|  | ||||
| class TrackPutResolver : DefaultPutResolver<Track>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: Track) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: Track) = ContentValues(10).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -83,8 +83,8 @@ class TrackGetResolver : DefaultGetResolver<Track>() { | ||||
| class TrackDeleteResolver : DefaultDeleteResolver<Track>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -10,20 +10,24 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable | ||||
| interface CategoryQueries : DbProvider { | ||||
|  | ||||
|     fun getCategories() = db.get() | ||||
|             .listOfObjects(Category::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(CategoryTable.TABLE) | ||||
|                     .orderBy(CategoryTable.COL_ORDER) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Category::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(CategoryTable.TABLE) | ||||
|                 .orderBy(CategoryTable.COL_ORDER) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getCategoriesForManga(manga: Manga) = db.get() | ||||
|             .listOfObjects(Category::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getCategoriesForMangaQuery()) | ||||
|                     .args(manga.id) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Category::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getCategoriesForMangaQuery()) | ||||
|                 .args(manga.id) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun insertCategory(category: Category) = db.put().`object`(category).prepare() | ||||
|  | ||||
|   | ||||
| @@ -16,50 +16,60 @@ import java.util.Date | ||||
| interface ChapterQueries : DbProvider { | ||||
|  | ||||
|     fun getChapters(manga: Manga) = db.get() | ||||
|             .listOfObjects(Chapter::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(ChapterTable.TABLE) | ||||
|                     .where("${ChapterTable.COL_MANGA_ID} = ?") | ||||
|                     .whereArgs(manga.id) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Chapter::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(ChapterTable.TABLE) | ||||
|                 .where("${ChapterTable.COL_MANGA_ID} = ?") | ||||
|                 .whereArgs(manga.id) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getRecentChapters(date: Date) = db.get() | ||||
|             .listOfObjects(MangaChapter::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getRecentsQuery()) | ||||
|                     .args(date.time) | ||||
|                     .observesTables(ChapterTable.TABLE) | ||||
|                     .build()) | ||||
|             .withGetResolver(MangaChapterGetResolver.INSTANCE) | ||||
|             .prepare() | ||||
|         .listOfObjects(MangaChapter::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getRecentsQuery()) | ||||
|                 .args(date.time) | ||||
|                 .observesTables(ChapterTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .withGetResolver(MangaChapterGetResolver.INSTANCE) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getChapter(id: Long) = db.get() | ||||
|             .`object`(Chapter::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(ChapterTable.TABLE) | ||||
|                     .where("${ChapterTable.COL_ID} = ?") | ||||
|                     .whereArgs(id) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .`object`(Chapter::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(ChapterTable.TABLE) | ||||
|                 .where("${ChapterTable.COL_ID} = ?") | ||||
|                 .whereArgs(id) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getChapter(url: String) = db.get() | ||||
|             .`object`(Chapter::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(ChapterTable.TABLE) | ||||
|                     .where("${ChapterTable.COL_URL} = ?") | ||||
|                     .whereArgs(url) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .`object`(Chapter::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(ChapterTable.TABLE) | ||||
|                 .where("${ChapterTable.COL_URL} = ?") | ||||
|                 .whereArgs(url) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getChapter(url: String, mangaId: Long) = db.get() | ||||
|             .`object`(Chapter::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(ChapterTable.TABLE) | ||||
|                     .where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?") | ||||
|                     .whereArgs(url, mangaId) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .`object`(Chapter::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(ChapterTable.TABLE) | ||||
|                 .where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?") | ||||
|                 .whereArgs(url, mangaId) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare() | ||||
|  | ||||
| @@ -70,22 +80,22 @@ interface ChapterQueries : DbProvider { | ||||
|     fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare() | ||||
|  | ||||
|     fun updateChaptersBackup(chapters: List<Chapter>) = db.put() | ||||
|             .objects(chapters) | ||||
|             .withPutResolver(ChapterBackupPutResolver()) | ||||
|             .prepare() | ||||
|         .objects(chapters) | ||||
|         .withPutResolver(ChapterBackupPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateChapterProgress(chapter: Chapter) = db.put() | ||||
|             .`object`(chapter) | ||||
|             .withPutResolver(ChapterProgressPutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(chapter) | ||||
|         .withPutResolver(ChapterProgressPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateChaptersProgress(chapters: List<Chapter>) = db.put() | ||||
|             .objects(chapters) | ||||
|             .withPutResolver(ChapterProgressPutResolver()) | ||||
|             .prepare() | ||||
|         .objects(chapters) | ||||
|         .withPutResolver(ChapterProgressPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun fixChaptersSourceOrder(chapters: List<Chapter>) = db.put() | ||||
|             .objects(chapters) | ||||
|             .withPutResolver(ChapterSourceOrderPutResolver()) | ||||
|             .prepare() | ||||
|         .objects(chapters) | ||||
|         .withPutResolver(ChapterSourceOrderPutResolver()) | ||||
|         .prepare() | ||||
| } | ||||
|   | ||||
| @@ -23,32 +23,38 @@ interface HistoryQueries : DbProvider { | ||||
|      * @param date recent date range | ||||
|      */ | ||||
|     fun getRecentManga(date: Date) = db.get() | ||||
|             .listOfObjects(MangaChapterHistory::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getRecentMangasQuery()) | ||||
|                     .args(date.time) | ||||
|                     .observesTables(HistoryTable.TABLE) | ||||
|                     .build()) | ||||
|             .withGetResolver(MangaChapterHistoryGetResolver.INSTANCE) | ||||
|             .prepare() | ||||
|         .listOfObjects(MangaChapterHistory::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getRecentMangasQuery()) | ||||
|                 .args(date.time) | ||||
|                 .observesTables(HistoryTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .withGetResolver(MangaChapterHistoryGetResolver.INSTANCE) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getHistoryByMangaId(mangaId: Long) = db.get() | ||||
|             .listOfObjects(History::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getHistoryByMangaId()) | ||||
|                     .args(mangaId) | ||||
|                     .observesTables(HistoryTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(History::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getHistoryByMangaId()) | ||||
|                 .args(mangaId) | ||||
|                 .observesTables(HistoryTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getHistoryByChapterUrl(chapterUrl: String) = db.get() | ||||
|             .`object`(History::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getHistoryByChapterUrl()) | ||||
|                     .args(chapterUrl) | ||||
|                     .observesTables(HistoryTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .`object`(History::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getHistoryByChapterUrl()) | ||||
|                 .args(chapterUrl) | ||||
|                 .observesTables(HistoryTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     /** | ||||
|      * Updates the history last read. | ||||
| @@ -56,9 +62,9 @@ interface HistoryQueries : DbProvider { | ||||
|      * @param history history object | ||||
|      */ | ||||
|     fun updateHistoryLastRead(history: History) = db.put() | ||||
|             .`object`(history) | ||||
|             .withPutResolver(HistoryLastReadPutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(history) | ||||
|         .withPutResolver(HistoryLastReadPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     /** | ||||
|      * Updates the history last read. | ||||
| @@ -66,21 +72,25 @@ interface HistoryQueries : DbProvider { | ||||
|      * @param historyList history object list | ||||
|      */ | ||||
|     fun updateHistoryLastRead(historyList: List<History>) = db.put() | ||||
|             .objects(historyList) | ||||
|             .withPutResolver(HistoryLastReadPutResolver()) | ||||
|             .prepare() | ||||
|         .objects(historyList) | ||||
|         .withPutResolver(HistoryLastReadPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun deleteHistory() = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(HistoryTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(HistoryTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun deleteHistoryNoLastRead() = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(HistoryTable.TABLE) | ||||
|                     .where("${HistoryTable.COL_LAST_READ} = ?") | ||||
|                     .whereArgs(0) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(HistoryTable.TABLE) | ||||
|                 .where("${HistoryTable.COL_LAST_READ} = ?") | ||||
|                 .whereArgs(0) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
| } | ||||
|   | ||||
| @@ -15,12 +15,14 @@ interface MangaCategoryQueries : DbProvider { | ||||
|     fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare() | ||||
|  | ||||
|     fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(MangaCategoryTable.TABLE) | ||||
|                     .where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})") | ||||
|                     .whereArgs(*mangas.map { it.id }.toTypedArray()) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(MangaCategoryTable.TABLE) | ||||
|                 .where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})") | ||||
|                 .whereArgs(*mangas.map { it.id }.toTypedArray()) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) { | ||||
|         db.inTransaction { | ||||
|   | ||||
| @@ -20,117 +20,137 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable | ||||
| interface MangaQueries : DbProvider { | ||||
|  | ||||
|     fun getMangas() = db.get() | ||||
|             .listOfObjects(Manga::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Manga::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getLibraryMangas() = db.get() | ||||
|             .listOfObjects(LibraryManga::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(libraryQuery) | ||||
|                     .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE) | ||||
|                     .build()) | ||||
|             .withGetResolver(LibraryMangaGetResolver.INSTANCE) | ||||
|             .prepare() | ||||
|         .listOfObjects(LibraryManga::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(libraryQuery) | ||||
|                 .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .withGetResolver(LibraryMangaGetResolver.INSTANCE) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getFavoriteMangas() = db.get() | ||||
|             .listOfObjects(Manga::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .where("${MangaTable.COL_FAVORITE} = ?") | ||||
|                     .whereArgs(1) | ||||
|                     .orderBy(MangaTable.COL_TITLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Manga::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .where("${MangaTable.COL_FAVORITE} = ?") | ||||
|                 .whereArgs(1) | ||||
|                 .orderBy(MangaTable.COL_TITLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getManga(url: String, sourceId: Long) = db.get() | ||||
|             .`object`(Manga::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?") | ||||
|                     .whereArgs(url, sourceId) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .`object`(Manga::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?") | ||||
|                 .whereArgs(url, sourceId) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getManga(id: Long) = db.get() | ||||
|             .`object`(Manga::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .where("${MangaTable.COL_ID} = ?") | ||||
|                     .whereArgs(id) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .`object`(Manga::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .where("${MangaTable.COL_ID} = ?") | ||||
|                 .whereArgs(id) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun insertManga(manga: Manga) = db.put().`object`(manga).prepare() | ||||
|  | ||||
|     fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare() | ||||
|  | ||||
|     fun updateFlags(manga: Manga) = db.put() | ||||
|             .`object`(manga) | ||||
|             .withPutResolver(MangaFlagsPutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(manga) | ||||
|         .withPutResolver(MangaFlagsPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateLastUpdated(manga: Manga) = db.put() | ||||
|             .`object`(manga) | ||||
|             .withPutResolver(MangaLastUpdatedPutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(manga) | ||||
|         .withPutResolver(MangaLastUpdatedPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateMangaFavorite(manga: Manga) = db.put() | ||||
|             .`object`(manga) | ||||
|             .withPutResolver(MangaFavoritePutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(manga) | ||||
|         .withPutResolver(MangaFavoritePutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateMangaViewer(manga: Manga) = db.put() | ||||
|             .`object`(manga) | ||||
|             .withPutResolver(MangaViewerPutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(manga) | ||||
|         .withPutResolver(MangaViewerPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateMangaTitle(manga: Manga) = db.put() | ||||
|             .`object`(manga) | ||||
|             .withPutResolver(MangaTitlePutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(manga) | ||||
|         .withPutResolver(MangaTitlePutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() | ||||
|  | ||||
|     fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare() | ||||
|  | ||||
|     fun deleteMangasNotInLibrary() = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .where("${MangaTable.COL_FAVORITE} = ?") | ||||
|                     .whereArgs(0) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .where("${MangaTable.COL_FAVORITE} = ?") | ||||
|                 .whereArgs(0) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun deleteMangas() = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getLastReadManga() = db.get() | ||||
|             .listOfObjects(Manga::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getLastReadMangaQuery()) | ||||
|                     .observesTables(MangaTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Manga::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getLastReadMangaQuery()) | ||||
|                 .observesTables(MangaTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getTotalChapterManga() = db.get() | ||||
|             .listOfObjects(Manga::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getTotalChapterMangaQuery()) | ||||
|                     .observesTables(MangaTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Manga::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getTotalChapterMangaQuery()) | ||||
|                 .observesTables(MangaTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getLatestChapterManga() = db.get() | ||||
|             .listOfObjects(Manga::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getLatestChapterMangaQuery()) | ||||
|                     .observesTables(MangaTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Manga::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getLatestChapterMangaQuery()) | ||||
|                 .observesTables(MangaTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
| } | ||||
|   | ||||
| @@ -9,7 +9,8 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga | ||||
| /** | ||||
|  * Query to get the manga from the library, with their categories and unread count. | ||||
|  */ | ||||
| val libraryQuery = """ | ||||
| val libraryQuery = | ||||
|     """ | ||||
|     SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY} | ||||
|     FROM ( | ||||
|         SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD} | ||||
| @@ -33,7 +34,8 @@ val libraryQuery = """ | ||||
| /** | ||||
|  * Query to get the recent chapters of manga from the library up to a date. | ||||
|  */ | ||||
| fun getRecentsQuery() = """ | ||||
| fun getRecentsQuery() = | ||||
|     """ | ||||
|     SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE} | ||||
|     ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} | ||||
|     WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ? | ||||
| @@ -47,7 +49,8 @@ fun getRecentsQuery() = """ | ||||
|  * and are read after the given time period | ||||
|  * @return return limit is 25 | ||||
|  */ | ||||
| fun getRecentMangasQuery() = """ | ||||
| fun getRecentMangasQuery() = | ||||
|     """ | ||||
|     SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.* | ||||
|     FROM ${Manga.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
| @@ -65,7 +68,8 @@ fun getRecentMangasQuery() = """ | ||||
|     LIMIT 25 | ||||
| """ | ||||
|  | ||||
| fun getHistoryByMangaId() = """ | ||||
| fun getHistoryByMangaId() = | ||||
|     """ | ||||
|     SELECT ${History.TABLE}.* | ||||
|     FROM ${History.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
| @@ -73,7 +77,8 @@ fun getHistoryByMangaId() = """ | ||||
|     WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID} | ||||
| """ | ||||
|  | ||||
| fun getHistoryByChapterUrl() = """ | ||||
| fun getHistoryByChapterUrl() = | ||||
|     """ | ||||
|     SELECT ${History.TABLE}.* | ||||
|     FROM ${History.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
| @@ -81,7 +86,8 @@ fun getHistoryByChapterUrl() = """ | ||||
|     WHERE ${Chapter.TABLE}.${Chapter.COL_URL} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID} | ||||
| """ | ||||
|  | ||||
| fun getLastReadMangaQuery() = """ | ||||
| fun getLastReadMangaQuery() = | ||||
|     """ | ||||
|     SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max | ||||
|     FROM ${Manga.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
| @@ -93,7 +99,8 @@ fun getLastReadMangaQuery() = """ | ||||
|     ORDER BY max DESC | ||||
| """ | ||||
|  | ||||
| fun getTotalChapterMangaQuery() = """ | ||||
| fun getTotalChapterMangaQuery() = | ||||
|     """ | ||||
|     SELECT ${Manga.TABLE}.* | ||||
|     FROM ${Manga.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
| @@ -102,7 +109,8 @@ fun getTotalChapterMangaQuery() = """ | ||||
|     ORDER by COUNT(*) | ||||
| """ | ||||
|  | ||||
| fun getLatestChapterMangaQuery() = """ | ||||
| fun getLatestChapterMangaQuery() = | ||||
|     """ | ||||
|     SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_UPLOAD}) AS max | ||||
|     FROM ${Manga.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
| @@ -114,7 +122,8 @@ fun getLatestChapterMangaQuery() = """ | ||||
| /** | ||||
|  * Query to get the categories for a manga. | ||||
|  */ | ||||
| fun getCategoriesForMangaQuery() = """ | ||||
| fun getCategoriesForMangaQuery() = | ||||
|     """ | ||||
|     SELECT ${Category.TABLE}.* FROM ${Category.TABLE} | ||||
|     JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} = | ||||
|     ${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID} | ||||
|   | ||||
| @@ -11,23 +11,27 @@ import eu.kanade.tachiyomi.data.track.TrackService | ||||
| interface TrackQueries : DbProvider { | ||||
|  | ||||
|     fun getTracks(manga: Manga) = db.get() | ||||
|             .listOfObjects(Track::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(TrackTable.TABLE) | ||||
|                     .where("${TrackTable.COL_MANGA_ID} = ?") | ||||
|                     .whereArgs(manga.id) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Track::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(TrackTable.TABLE) | ||||
|                 .where("${TrackTable.COL_MANGA_ID} = ?") | ||||
|                 .whereArgs(manga.id) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun insertTrack(track: Track) = db.put().`object`(track).prepare() | ||||
|  | ||||
|     fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare() | ||||
|  | ||||
|     fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(TrackTable.TABLE) | ||||
|                     .where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?") | ||||
|                     .whereArgs(manga.id, sync.id) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(TrackTable.TABLE) | ||||
|                 .where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?") | ||||
|                 .whereArgs(manga.id, sync.id) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
| } | ||||
|   | ||||
| @@ -20,10 +20,10 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder() | ||||
|             .table(ChapterTable.TABLE) | ||||
|             .where("${ChapterTable.COL_URL} = ?") | ||||
|             .whereArgs(chapter.url) | ||||
|             .build() | ||||
|         .table(ChapterTable.TABLE) | ||||
|         .where("${ChapterTable.COL_URL} = ?") | ||||
|         .whereArgs(chapter.url) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply { | ||||
|         put(ChapterTable.COL_READ, chapter.read) | ||||
|   | ||||
| @@ -20,10 +20,10 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder() | ||||
|             .table(ChapterTable.TABLE) | ||||
|             .where("${ChapterTable.COL_ID} = ?") | ||||
|             .whereArgs(chapter.id) | ||||
|             .build() | ||||
|         .table(ChapterTable.TABLE) | ||||
|         .where("${ChapterTable.COL_ID} = ?") | ||||
|         .whereArgs(chapter.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply { | ||||
|         put(ChapterTable.COL_READ, chapter.read) | ||||
|   | ||||
| @@ -20,10 +20,10 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder() | ||||
|             .table(ChapterTable.TABLE) | ||||
|             .where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?") | ||||
|             .whereArgs(chapter.url, chapter.manga_id) | ||||
|             .build() | ||||
|         .table(ChapterTable.TABLE) | ||||
|         .where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?") | ||||
|         .whereArgs(chapter.url, chapter.manga_id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply { | ||||
|         put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order) | ||||
|   | ||||
| @@ -19,11 +19,13 @@ class HistoryLastReadPutResolver : HistoryPutResolver() { | ||||
|     override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: History): PutResult = db.inTransactionReturn { | ||||
|         val updateQuery = mapToUpdateQuery(history) | ||||
|  | ||||
|         val cursor = db.lowLevel().query(Query.builder() | ||||
|         val cursor = db.lowLevel().query( | ||||
|             Query.builder() | ||||
|                 .table(updateQuery.table()) | ||||
|                 .where(updateQuery.where()) | ||||
|                 .whereArgs(updateQuery.whereArgs()) | ||||
|                 .build()) | ||||
|                 .build() | ||||
|         ) | ||||
|  | ||||
|         val putResult: PutResult | ||||
|  | ||||
| @@ -46,10 +48,10 @@ class HistoryLastReadPutResolver : HistoryPutResolver() { | ||||
|      * @param obj history object | ||||
|      */ | ||||
|     override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder() | ||||
|             .table(HistoryTable.TABLE) | ||||
|             .where("${HistoryTable.COL_CHAPTER_ID} = ?") | ||||
|             .whereArgs(obj.chapter_id) | ||||
|             .build() | ||||
|         .table(HistoryTable.TABLE) | ||||
|         .where("${HistoryTable.COL_CHAPTER_ID} = ?") | ||||
|         .whereArgs(obj.chapter_id) | ||||
|         .build() | ||||
|  | ||||
|     /** | ||||
|      * Create content query | ||||
|   | ||||
| @@ -20,10 +20,10 @@ class MangaFavoritePutResolver : PutResolver<Manga>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() | ||||
|             .table(MangaTable.TABLE) | ||||
|             .where("${MangaTable.COL_ID} = ?") | ||||
|             .whereArgs(manga.id) | ||||
|             .build() | ||||
|         .table(MangaTable.TABLE) | ||||
|         .where("${MangaTable.COL_ID} = ?") | ||||
|         .whereArgs(manga.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(manga: Manga) = ContentValues(1).apply { | ||||
|         put(MangaTable.COL_FAVORITE, manga.favorite) | ||||
|   | ||||
| @@ -20,10 +20,10 @@ class MangaFlagsPutResolver : PutResolver<Manga>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() | ||||
|             .table(MangaTable.TABLE) | ||||
|             .where("${MangaTable.COL_ID} = ?") | ||||
|             .whereArgs(manga.id) | ||||
|             .build() | ||||
|         .table(MangaTable.TABLE) | ||||
|         .where("${MangaTable.COL_ID} = ?") | ||||
|         .whereArgs(manga.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(manga: Manga) = ContentValues(1).apply { | ||||
|         put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags) | ||||
|   | ||||
| @@ -20,10 +20,10 @@ class MangaLastUpdatedPutResolver : PutResolver<Manga>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() | ||||
|             .table(MangaTable.TABLE) | ||||
|             .where("${MangaTable.COL_ID} = ?") | ||||
|             .whereArgs(manga.id) | ||||
|             .build() | ||||
|         .table(MangaTable.TABLE) | ||||
|         .where("${MangaTable.COL_ID} = ?") | ||||
|         .whereArgs(manga.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(manga: Manga) = ContentValues(1).apply { | ||||
|         put(MangaTable.COL_LAST_UPDATE, manga.last_update) | ||||
|   | ||||
| @@ -20,10 +20,10 @@ class MangaTitlePutResolver : PutResolver<Manga>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() | ||||
|             .table(MangaTable.TABLE) | ||||
|             .where("${MangaTable.COL_ID} = ?") | ||||
|             .whereArgs(manga.id) | ||||
|             .build() | ||||
|         .table(MangaTable.TABLE) | ||||
|         .where("${MangaTable.COL_ID} = ?") | ||||
|         .whereArgs(manga.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(manga: Manga) = ContentValues(1).apply { | ||||
|         put(MangaTable.COL_TITLE, manga.title) | ||||
|   | ||||
| @@ -20,10 +20,10 @@ class MangaViewerPutResolver : PutResolver<Manga>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() | ||||
|             .table(MangaTable.TABLE) | ||||
|             .where("${MangaTable.COL_ID} = ?") | ||||
|             .whereArgs(manga.id) | ||||
|             .build() | ||||
|         .table(MangaTable.TABLE) | ||||
|         .where("${MangaTable.COL_ID} = ?") | ||||
|         .whereArgs(manga.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(manga: Manga) = ContentValues(1).apply { | ||||
|         put(MangaTable.COL_VIEWER, manga.viewer) | ||||
|   | ||||
| @@ -13,7 +13,8 @@ object CategoryTable { | ||||
|     const val COL_FLAGS = "flags" | ||||
|  | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_NAME TEXT NOT NULL, | ||||
|             $COL_ORDER INTEGER NOT NULL, | ||||
|   | ||||
| @@ -29,7 +29,8 @@ object ChapterTable { | ||||
|     const val COL_SOURCE_ORDER = "source_order" | ||||
|  | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_MANGA_ID INTEGER NOT NULL, | ||||
|             $COL_URL TEXT NOT NULL, | ||||
| @@ -51,7 +52,7 @@ object ChapterTable { | ||||
|  | ||||
|     val createUnreadChaptersIndexQuery: String | ||||
|         get() = "CREATE INDEX ${TABLE}_unread_by_manga_index ON $TABLE($COL_MANGA_ID, $COL_READ) " + | ||||
|                 "WHERE $COL_READ = 0" | ||||
|             "WHERE $COL_READ = 0" | ||||
|  | ||||
|     val sourceOrderUpdateQuery: String | ||||
|         get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0" | ||||
|   | ||||
| @@ -31,7 +31,8 @@ object HistoryTable { | ||||
|      * query to create history table | ||||
|      */ | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_CHAPTER_ID INTEGER NOT NULL UNIQUE, | ||||
|             $COL_LAST_READ LONG, | ||||
|   | ||||
| @@ -11,7 +11,8 @@ object MangaCategoryTable { | ||||
|     const val COL_CATEGORY_ID = "category_id" | ||||
|  | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_MANGA_ID INTEGER NOT NULL, | ||||
|             $COL_CATEGORY_ID INTEGER NOT NULL, | ||||
|   | ||||
| @@ -39,7 +39,8 @@ object MangaTable { | ||||
|     const val COL_CATEGORY = "category" | ||||
|  | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_SOURCE INTEGER NOT NULL, | ||||
|             $COL_URL TEXT NOT NULL, | ||||
| @@ -62,5 +63,5 @@ object MangaTable { | ||||
|  | ||||
|     val createLibraryIndexQuery: String | ||||
|         get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " + | ||||
|                 "WHERE $COL_FAVORITE = 1" | ||||
|             "WHERE $COL_FAVORITE = 1" | ||||
| } | ||||
|   | ||||
| @@ -31,7 +31,8 @@ object TrackTable { | ||||
|     const val COL_FINISH_DATE = "finish_date" | ||||
|  | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_MANGA_ID INTEGER NOT NULL, | ||||
|             $COL_SYNC_ID INTEGER NOT NULL, | ||||
|   | ||||
| @@ -100,8 +100,8 @@ class DownloadCache( | ||||
|             val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] | ||||
|             if (mangaDir != null) { | ||||
|                 return mangaDir.files | ||||
|                         .filter { !it.endsWith(Downloader.TMP_DIR_SUFFIX) } | ||||
|                         .size | ||||
|                     .filter { !it.endsWith(Downloader.TMP_DIR_SUFFIX) } | ||||
|                     .size | ||||
|             } | ||||
|         } | ||||
|         return 0 | ||||
| @@ -125,26 +125,26 @@ class DownloadCache( | ||||
|         val onlineSources = sourceManager.getOnlineSources() | ||||
|  | ||||
|         val sourceDirs = rootDir.dir.listFiles() | ||||
|                 .orEmpty() | ||||
|                 .associate { it.name to SourceDirectory(it) } | ||||
|                 .mapNotNullKeys { entry -> | ||||
|                     onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id | ||||
|                 } | ||||
|             .orEmpty() | ||||
|             .associate { it.name to SourceDirectory(it) } | ||||
|             .mapNotNullKeys { entry -> | ||||
|                 onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id | ||||
|             } | ||||
|  | ||||
|         rootDir.files = sourceDirs | ||||
|  | ||||
|         sourceDirs.values.forEach { sourceDir -> | ||||
|             val mangaDirs = sourceDir.dir.listFiles() | ||||
|                     .orEmpty() | ||||
|                     .associateNotNullKeys { it.name to MangaDirectory(it) } | ||||
|                 .orEmpty() | ||||
|                 .associateNotNullKeys { it.name to MangaDirectory(it) } | ||||
|  | ||||
|             sourceDir.files = mangaDirs | ||||
|  | ||||
|             mangaDirs.values.forEach { mangaDir -> | ||||
|                 val chapterDirs = mangaDir.dir.listFiles() | ||||
|                         .orEmpty() | ||||
|                         .mapNotNull { it.name } | ||||
|                         .toHashSet() | ||||
|                     .orEmpty() | ||||
|                     .mapNotNull { it.name } | ||||
|                     .toHashSet() | ||||
|  | ||||
|                 mangaDir.files = chapterDirs | ||||
|             } | ||||
|   | ||||
| @@ -148,16 +148,16 @@ class DownloadManager(private val context: Context) { | ||||
|     private fun buildPageList(chapterDir: UniFile?): Observable<List<Page>> { | ||||
|         return Observable.fromCallable { | ||||
|             val files = chapterDir?.listFiles().orEmpty() | ||||
|                     .filter { "image" in it.type.orEmpty() } | ||||
|                 .filter { "image" in it.type.orEmpty() } | ||||
|  | ||||
|             if (files.isEmpty()) { | ||||
|                 throw Exception("Page list is empty") | ||||
|             } | ||||
|  | ||||
|             files.sortedBy { it.name } | ||||
|                     .mapIndexed { i, file -> | ||||
|                         Page(i, uri = file.uri).apply { status = Page.READY } | ||||
|                     } | ||||
|                 .mapIndexed { i, file -> | ||||
|                     Page(i, uri = file.uri).apply { status = Page.READY } | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -87,13 +87,15 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|                 setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) | ||||
|                 isDownloading = true | ||||
|                 // Pause action | ||||
|                 addAction(R.drawable.ic_pause_24dp, | ||||
|                         context.getString(R.string.action_pause), | ||||
|                         NotificationReceiver.pauseDownloadsPendingBroadcast(context)) | ||||
|                 addAction( | ||||
|                     R.drawable.ic_pause_24dp, | ||||
|                     context.getString(R.string.action_pause), | ||||
|                     NotificationReceiver.pauseDownloadsPendingBroadcast(context) | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|             val downloadingProgressText = context.getString(R.string.chapter_downloading_progress) | ||||
|                     .format(download.downloadedImages, download.pages!!.size) | ||||
|                 .format(download.downloadedImages, download.pages!!.size) | ||||
|  | ||||
|             if (preferences.hideNotificationContent()) { | ||||
|                 setContentTitle(downloadingProgressText) | ||||
| @@ -126,13 +128,17 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|             // Open download manager when clicked | ||||
|             setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) | ||||
|             // Resume action | ||||
|             addAction(R.drawable.ic_play_arrow_24dp, | ||||
|                     context.getString(R.string.action_resume), | ||||
|                     NotificationReceiver.resumeDownloadsPendingBroadcast(context)) | ||||
|             addAction( | ||||
|                 R.drawable.ic_play_arrow_24dp, | ||||
|                 context.getString(R.string.action_resume), | ||||
|                 NotificationReceiver.resumeDownloadsPendingBroadcast(context) | ||||
|             ) | ||||
|             // Clear action | ||||
|             addAction(R.drawable.ic_close_24dp, | ||||
|                     context.getString(R.string.action_cancel_all), | ||||
|                     NotificationReceiver.clearDownloadsPendingBroadcast(context)) | ||||
|             addAction( | ||||
|                 R.drawable.ic_close_24dp, | ||||
|                 context.getString(R.string.action_cancel_all), | ||||
|                 NotificationReceiver.clearDownloadsPendingBroadcast(context) | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         // Show notification. | ||||
| @@ -173,8 +179,10 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|     fun onError(error: String? = null, chapter: String? = null) { | ||||
|         // Create notification | ||||
|         with(notificationBuilder) { | ||||
|             setContentTitle(chapter | ||||
|                     ?: context.getString(R.string.download_notifier_downloader_title)) | ||||
|             setContentTitle( | ||||
|                 chapter | ||||
|                     ?: context.getString(R.string.download_notifier_downloader_title) | ||||
|             ) | ||||
|             setContentText(error ?: context.getString(R.string.download_notifier_unknown_error)) | ||||
|             setSmallIcon(android.R.drawable.stat_sys_warning) | ||||
|             clearActions() | ||||
|   | ||||
| @@ -52,8 +52,8 @@ class DownloadProvider(private val context: Context) { | ||||
|     internal fun getMangaDir(manga: Manga, source: Source): UniFile { | ||||
|         try { | ||||
|             return downloadsDir | ||||
|                     .createDirectory(getSourceDirName(source)) | ||||
|                     .createDirectory(getMangaDirName(manga)) | ||||
|                 .createDirectory(getSourceDirName(source)) | ||||
|                 .createDirectory(getMangaDirName(manga)) | ||||
|         } catch (e: NullPointerException) { | ||||
|             throw Exception(context.getString(R.string.invalid_download_dir)) | ||||
|         } | ||||
|   | ||||
| @@ -123,14 +123,17 @@ class DownloadService : Service() { | ||||
|      */ | ||||
|     private fun listenNetworkChanges() { | ||||
|         subscriptions += ReactiveNetwork.observeNetworkConnectivity(applicationContext) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe({ state -> | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe( | ||||
|                 { state -> | ||||
|                     onNetworkStateChanged(state) | ||||
|                 }, { | ||||
|                 }, | ||||
|                 { | ||||
|                     toast(R.string.download_queue_error) | ||||
|                     stopSelf() | ||||
|                 }) | ||||
|                 } | ||||
|             ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -162,10 +165,11 @@ class DownloadService : Service() { | ||||
|      */ | ||||
|     private fun listenDownloaderState() { | ||||
|         subscriptions += downloadManager.runningRelay.subscribe { running -> | ||||
|             if (running) | ||||
|             if (running) { | ||||
|                 wakeLock.acquireIfNeeded() | ||||
|             else | ||||
|             } else { | ||||
|                 wakeLock.releaseIfNeeded() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -77,9 +77,9 @@ class DownloadStore( | ||||
|      */ | ||||
|     fun restore(): List<Download> { | ||||
|         val objs = preferences.all | ||||
|                 .mapNotNull { it.value as? String } | ||||
|                 .mapNotNull { deserialize(it) } | ||||
|                 .sortedBy { it.order } | ||||
|             .mapNotNull { it.value as? String } | ||||
|             .mapNotNull { deserialize(it) } | ||||
|             .sortedBy { it.order } | ||||
|  | ||||
|         val downloads = mutableListOf<Download>() | ||||
|         if (objs.isNotEmpty()) { | ||||
|   | ||||
| @@ -100,11 +100,13 @@ class Downloader( | ||||
|      * @return true if the downloader is started, false otherwise. | ||||
|      */ | ||||
|     fun start(): Boolean { | ||||
|         if (isRunning || queue.isEmpty()) | ||||
|         if (isRunning || queue.isEmpty()) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         if (!subscriptions.hasSubscriptions()) | ||||
|         if (!subscriptions.hasSubscriptions()) { | ||||
|             initializeSubscriptions() | ||||
|         } | ||||
|  | ||||
|         val pending = queue.filter { it.status != Download.DOWNLOADED } | ||||
|         pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE } | ||||
| @@ -119,8 +121,8 @@ class Downloader( | ||||
|     fun stop(reason: String? = null) { | ||||
|         destroySubscriptions() | ||||
|         queue | ||||
|                 .filter { it.status == Download.DOWNLOADING } | ||||
|                 .forEach { it.status = Download.ERROR } | ||||
|             .filter { it.status == Download.DOWNLOADING } | ||||
|             .forEach { it.status = Download.ERROR } | ||||
|  | ||||
|         if (reason != null) { | ||||
|             notifier.onWarning(reason) | ||||
| @@ -140,8 +142,8 @@ class Downloader( | ||||
|     fun pause() { | ||||
|         destroySubscriptions() | ||||
|         queue | ||||
|                 .filter { it.status == Download.DOWNLOADING } | ||||
|                 .forEach { it.status = Download.QUEUE } | ||||
|             .filter { it.status == Download.DOWNLOADING } | ||||
|             .forEach { it.status = Download.QUEUE } | ||||
|         notifier.paused = true | ||||
|     } | ||||
|  | ||||
| @@ -156,8 +158,8 @@ class Downloader( | ||||
|         // Needed to update the chapter view | ||||
|         if (isNotification) { | ||||
|             queue | ||||
|                     .filter { it.status == Download.QUEUE } | ||||
|                     .forEach { it.status = Download.NOT_DOWNLOADED } | ||||
|                 .filter { it.status == Download.QUEUE } | ||||
|                 .forEach { it.status = Download.NOT_DOWNLOADED } | ||||
|         } | ||||
|         queue.clear() | ||||
|         notifier.dismiss() | ||||
| @@ -174,16 +176,19 @@ class Downloader( | ||||
|         subscriptions.clear() | ||||
|  | ||||
|         subscriptions += downloadsRelay.concatMapIterable { it } | ||||
|                 .concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) } | ||||
|                 .onBackpressureBuffer() | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe({ | ||||
|             .concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) } | ||||
|             .onBackpressureBuffer() | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe( | ||||
|                 { | ||||
|                     completeDownload(it) | ||||
|                 }, { error -> | ||||
|                 }, | ||||
|                 { error -> | ||||
|                     DownloadService.stop(context) | ||||
|                     Timber.e(error) | ||||
|                     notifier.onError(error.message) | ||||
|                 }) | ||||
|                 } | ||||
|             ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -212,20 +217,20 @@ class Downloader( | ||||
|             val mangaDir = provider.findMangaDir(manga, source) | ||||
|  | ||||
|             chapters | ||||
|                     // Avoid downloading chapters with the same name. | ||||
|                     .distinctBy { it.name } | ||||
|                     // Filter out those already downloaded. | ||||
|                     .filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null } | ||||
|                     // Add chapters to queue from the start. | ||||
|                     .sortedByDescending { it.source_order } | ||||
|                 // Avoid downloading chapters with the same name. | ||||
|                 .distinctBy { it.name } | ||||
|                 // Filter out those already downloaded. | ||||
|                 .filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null } | ||||
|                 // Add chapters to queue from the start. | ||||
|                 .sortedByDescending { it.source_order } | ||||
|         } | ||||
|  | ||||
|         // Runs in main thread (synchronization needed). | ||||
|         val chaptersToQueue = chaptersWithoutDir.await() | ||||
|                 // Filter out those already enqueued. | ||||
|                 .filter { chapter -> queue.none { it.chapter.id == chapter.id } } | ||||
|                 // Create a download for each one. | ||||
|                 .map { Download(source, manga, it) } | ||||
|             // Filter out those already enqueued. | ||||
|             .filter { chapter -> queue.none { it.chapter.id == chapter.id } } | ||||
|             // Create a download for each one. | ||||
|             .map { Download(source, manga, it) } | ||||
|  | ||||
|         if (chaptersToQueue.isNotEmpty()) { | ||||
|             queue.addAll(chaptersToQueue) | ||||
| @@ -255,43 +260,43 @@ class Downloader( | ||||
|         val pageListObservable = if (download.pages == null) { | ||||
|             // Pull page list from network and add them to download object | ||||
|             download.source.fetchPageList(download.chapter) | ||||
|                     .doOnNext { pages -> | ||||
|                         if (pages.isEmpty()) { | ||||
|                             throw Exception("Page list is empty") | ||||
|                         } | ||||
|                         download.pages = pages | ||||
|                 .doOnNext { pages -> | ||||
|                     if (pages.isEmpty()) { | ||||
|                         throw Exception("Page list is empty") | ||||
|                     } | ||||
|                     download.pages = pages | ||||
|                 } | ||||
|         } else { | ||||
|             // Or if the page list already exists, start from the file | ||||
|             Observable.just(download.pages!!) | ||||
|         } | ||||
|  | ||||
|         pageListObservable | ||||
|                 .doOnNext { _ -> | ||||
|                     // Delete all temporary (unfinished) files | ||||
|                     tmpDir.listFiles() | ||||
|                             ?.filter { it.name!!.endsWith(".tmp") } | ||||
|                             ?.forEach { it.delete() } | ||||
|             .doOnNext { _ -> | ||||
|                 // Delete all temporary (unfinished) files | ||||
|                 tmpDir.listFiles() | ||||
|                     ?.filter { it.name!!.endsWith(".tmp") } | ||||
|                     ?.forEach { it.delete() } | ||||
|  | ||||
|                     download.downloadedImages = 0 | ||||
|                     download.status = Download.DOWNLOADING | ||||
|                 } | ||||
|                 // Get all the URLs to the source images, fetch pages if necessary | ||||
|                 .flatMap { download.source.fetchAllImageUrlsFromPageList(it) } | ||||
|                 // Start downloading images, consider we can have downloaded images already | ||||
|                 .concatMap { page -> getOrDownloadImage(page, download, tmpDir) } | ||||
|                 // Do when page is downloaded. | ||||
|                 .doOnNext { notifier.onProgressChange(download) } | ||||
|                 .toList() | ||||
|                 .map { download } | ||||
|                 // Do after download completes | ||||
|                 .doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) } | ||||
|                 // If the page list threw, it will resume here | ||||
|                 .onErrorReturn { error -> | ||||
|                     download.status = Download.ERROR | ||||
|                     notifier.onError(error.message, download.chapter.name) | ||||
|                     download | ||||
|                 } | ||||
|                 download.downloadedImages = 0 | ||||
|                 download.status = Download.DOWNLOADING | ||||
|             } | ||||
|             // Get all the URLs to the source images, fetch pages if necessary | ||||
|             .flatMap { download.source.fetchAllImageUrlsFromPageList(it) } | ||||
|             // Start downloading images, consider we can have downloaded images already | ||||
|             .concatMap { page -> getOrDownloadImage(page, download, tmpDir) } | ||||
|             // Do when page is downloaded. | ||||
|             .doOnNext { notifier.onProgressChange(download) } | ||||
|             .toList() | ||||
|             .map { download } | ||||
|             // Do after download completes | ||||
|             .doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) } | ||||
|             // If the page list threw, it will resume here | ||||
|             .onErrorReturn { error -> | ||||
|                 download.status = Download.ERROR | ||||
|                 notifier.onError(error.message, download.chapter.name) | ||||
|                 download | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -304,8 +309,9 @@ class Downloader( | ||||
|      */ | ||||
|     private fun getOrDownloadImage(page: Page, download: Download, tmpDir: UniFile): Observable<Page> { | ||||
|         // If the image URL is empty, do nothing | ||||
|         if (page.imageUrl == null) | ||||
|         if (page.imageUrl == null) { | ||||
|             return Observable.just(page) | ||||
|         } | ||||
|  | ||||
|         val filename = String.format("%03d", page.number) | ||||
|         val tmpFile = tmpDir.findFile("$filename.tmp") | ||||
| @@ -317,26 +323,27 @@ class Downloader( | ||||
|         val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") } | ||||
|  | ||||
|         // If the image is already downloaded, do nothing. Otherwise download from network | ||||
|         val pageObservable = if (imageFile != null) | ||||
|         val pageObservable = if (imageFile != null) { | ||||
|             Observable.just(imageFile) | ||||
|         else | ||||
|         } else { | ||||
|             downloadImage(page, download.source, tmpDir, filename) | ||||
|         } | ||||
|  | ||||
|         return pageObservable | ||||
|                 // When the image is ready, set image path, progress (just in case) and status | ||||
|                 .doOnNext { file -> | ||||
|                     page.uri = file.uri | ||||
|                     page.progress = 100 | ||||
|                     download.downloadedImages++ | ||||
|                     page.status = Page.READY | ||||
|                 } | ||||
|                 .map { page } | ||||
|                 // Mark this page as error and allow to download the remaining | ||||
|                 .onErrorReturn { | ||||
|                     page.progress = 0 | ||||
|                     page.status = Page.ERROR | ||||
|                     page | ||||
|                 } | ||||
|             // When the image is ready, set image path, progress (just in case) and status | ||||
|             .doOnNext { file -> | ||||
|                 page.uri = file.uri | ||||
|                 page.progress = 100 | ||||
|                 download.downloadedImages++ | ||||
|                 page.status = Page.READY | ||||
|             } | ||||
|             .map { page } | ||||
|             // Mark this page as error and allow to download the remaining | ||||
|             .onErrorReturn { | ||||
|                 page.progress = 0 | ||||
|                 page.status = Page.ERROR | ||||
|                 page | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -351,21 +358,21 @@ class Downloader( | ||||
|         page.status = Page.DOWNLOAD_IMAGE | ||||
|         page.progress = 0 | ||||
|         return source.fetchImage(page) | ||||
|                 .map { response -> | ||||
|                     val file = tmpDir.createFile("$filename.tmp") | ||||
|                     try { | ||||
|                         response.body!!.source().saveTo(file.openOutputStream()) | ||||
|                         val extension = getImageExtension(response, file) | ||||
|                         file.renameTo("$filename.$extension") | ||||
|                     } catch (e: Exception) { | ||||
|                         response.close() | ||||
|                         file.delete() | ||||
|                         throw e | ||||
|                     } | ||||
|                     file | ||||
|             .map { response -> | ||||
|                 val file = tmpDir.createFile("$filename.tmp") | ||||
|                 try { | ||||
|                     response.body!!.source().saveTo(file.openOutputStream()) | ||||
|                     val extension = getImageExtension(response, file) | ||||
|                     file.renameTo("$filename.$extension") | ||||
|                 } catch (e: Exception) { | ||||
|                     response.close() | ||||
|                     file.delete() | ||||
|                     throw e | ||||
|                 } | ||||
|                 // Retry 3 times, waiting 2, 4 and 8 seconds between attempts. | ||||
|                 .retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline())) | ||||
|                 file | ||||
|             } | ||||
|             // Retry 3 times, waiting 2, 4 and 8 seconds between attempts. | ||||
|             .retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline())) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -378,10 +385,10 @@ class Downloader( | ||||
|     private fun getImageExtension(response: Response, file: UniFile): String { | ||||
|         // Read content type if available. | ||||
|         val mime = response.body?.contentType()?.let { ct -> "${ct.type}/${ct.subtype}" } | ||||
|                 // Else guess from the uri. | ||||
|                 ?: context.contentResolver.getType(file.uri) | ||||
|                 // Else read magic numbers. | ||||
|                 ?: ImageUtil.findImageType { file.openInputStream() }?.mime | ||||
|             // Else guess from the uri. | ||||
|             ?: context.contentResolver.getType(file.uri) | ||||
|             // Else read magic numbers. | ||||
|             ?: ImageUtil.findImageType { file.openInputStream() }?.mime | ||||
|  | ||||
|         return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg" | ||||
|     } | ||||
| @@ -400,7 +407,6 @@ class Downloader( | ||||
|         tmpDir: UniFile, | ||||
|         dirname: String | ||||
|     ) { | ||||
|  | ||||
|         // Ensure that the chapter folder has all the images. | ||||
|         val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") } | ||||
|  | ||||
|   | ||||
| @@ -70,13 +70,13 @@ class DownloadQueue( | ||||
|     } | ||||
|  | ||||
|     fun getActiveDownloads(): Observable<Download> = | ||||
|             Observable.from(this).filter { download -> download.status == Download.DOWNLOADING } | ||||
|         Observable.from(this).filter { download -> download.status == Download.DOWNLOADING } | ||||
|  | ||||
|     fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer() | ||||
|  | ||||
|     fun getUpdatedObservable(): Observable<List<Download>> = updatedRelay.onBackpressureBuffer() | ||||
|             .startWith(Unit) | ||||
|             .map { this } | ||||
|         .startWith(Unit) | ||||
|         .map { this } | ||||
|  | ||||
|     private fun setPagesFor(download: Download) { | ||||
|         if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) { | ||||
| @@ -86,21 +86,21 @@ class DownloadQueue( | ||||
|  | ||||
|     fun getProgressObservable(): Observable<Download> { | ||||
|         return statusSubject.onBackpressureBuffer() | ||||
|                 .startWith(getActiveDownloads()) | ||||
|                 .flatMap { download -> | ||||
|                     if (download.status == Download.DOWNLOADING) { | ||||
|                         val pageStatusSubject = PublishSubject.create<Int>() | ||||
|                         setPagesSubject(download.pages, pageStatusSubject) | ||||
|                         return@flatMap pageStatusSubject | ||||
|                                 .onBackpressureBuffer() | ||||
|                                 .filter { it == Page.READY } | ||||
|                                 .map { download } | ||||
|                     } else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) { | ||||
|                         setPagesSubject(download.pages, null) | ||||
|                     } | ||||
|                     Observable.just(download) | ||||
|             .startWith(getActiveDownloads()) | ||||
|             .flatMap { download -> | ||||
|                 if (download.status == Download.DOWNLOADING) { | ||||
|                     val pageStatusSubject = PublishSubject.create<Int>() | ||||
|                     setPagesSubject(download.pages, pageStatusSubject) | ||||
|                     return@flatMap pageStatusSubject | ||||
|                         .onBackpressureBuffer() | ||||
|                         .filter { it == Page.READY } | ||||
|                         .map { download } | ||||
|                 } else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) { | ||||
|                     setPagesSubject(download.pages, null) | ||||
|                 } | ||||
|                 .filter { it.status == Download.DOWNLOADING } | ||||
|                 Observable.just(download) | ||||
|             } | ||||
|             .filter { it.status == Download.DOWNLOADING } | ||||
|     } | ||||
|  | ||||
|     private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) { | ||||
|   | ||||
| @@ -25,36 +25,39 @@ class LibraryMangaUrlFetcher( | ||||
|  | ||||
|     override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) { | ||||
|         if (!file.exists()) { | ||||
|             networkFetcher.loadData(priority, object : DataFetcher.DataCallback<InputStream> { | ||||
|                 override fun onDataReady(data: InputStream?) { | ||||
|                     if (data != null) { | ||||
|                         val tmpFile = File(file.path + ".tmp") | ||||
|                         try { | ||||
|                             // Retrieve destination stream, create parent folders if needed. | ||||
|                             val output = try { | ||||
|                                 tmpFile.outputStream() | ||||
|                             } catch (e: FileNotFoundException) { | ||||
|                                 tmpFile.parentFile.mkdirs() | ||||
|                                 tmpFile.outputStream() | ||||
|                             } | ||||
|             networkFetcher.loadData( | ||||
|                 priority, | ||||
|                 object : DataFetcher.DataCallback<InputStream> { | ||||
|                     override fun onDataReady(data: InputStream?) { | ||||
|                         if (data != null) { | ||||
|                             val tmpFile = File(file.path + ".tmp") | ||||
|                             try { | ||||
|                                 // Retrieve destination stream, create parent folders if needed. | ||||
|                                 val output = try { | ||||
|                                     tmpFile.outputStream() | ||||
|                                 } catch (e: FileNotFoundException) { | ||||
|                                     tmpFile.parentFile.mkdirs() | ||||
|                                     tmpFile.outputStream() | ||||
|                                 } | ||||
|  | ||||
|                             // Copy the file and rename to the original. | ||||
|                             data.use { output.use { data.copyTo(output) } } | ||||
|                             tmpFile.renameTo(file) | ||||
|                             loadFromFile(callback) | ||||
|                         } catch (e: Exception) { | ||||
|                             tmpFile.delete() | ||||
|                             callback.onLoadFailed(e) | ||||
|                                 // Copy the file and rename to the original. | ||||
|                                 data.use { output.use { data.copyTo(output) } } | ||||
|                                 tmpFile.renameTo(file) | ||||
|                                 loadFromFile(callback) | ||||
|                             } catch (e: Exception) { | ||||
|                                 tmpFile.delete() | ||||
|                                 callback.onLoadFailed(e) | ||||
|                             } | ||||
|                         } else { | ||||
|                             callback.onLoadFailed(Exception("Null data")) | ||||
|                         } | ||||
|                     } else { | ||||
|                         callback.onLoadFailed(Exception("Null data")) | ||||
|                     } | ||||
|  | ||||
|                     override fun onLoadFailed(e: Exception) { | ||||
|                         callback.onLoadFailed(e) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 override fun onLoadFailed(e: Exception) { | ||||
|                     callback.onLoadFailed(e) | ||||
|                 } | ||||
|             }) | ||||
|             ) | ||||
|         } else { | ||||
|             loadFromFile(callback) | ||||
|         } | ||||
|   | ||||
| @@ -27,8 +27,10 @@ class TachiGlideModule : AppGlideModule() { | ||||
|     override fun applyOptions(context: Context, builder: GlideBuilder) { | ||||
|         builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024)) | ||||
|         builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565)) | ||||
|         builder.setDefaultTransitionOptions(Drawable::class.java, | ||||
|                 DrawableTransitionOptions.withCrossFade()) | ||||
|         builder.setDefaultTransitionOptions( | ||||
|             Drawable::class.java, | ||||
|             DrawableTransitionOptions.withCrossFade() | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     override fun registerComponents(context: Context, glide: Glide, registry: Registry) { | ||||
| @@ -36,7 +38,10 @@ class TachiGlideModule : AppGlideModule() { | ||||
|  | ||||
|         registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory) | ||||
|         registry.append(MangaThumbnail::class.java, InputStream::class.java, MangaThumbnailModelLoader.Factory()) | ||||
|         registry.append(InputStream::class.java, InputStream::class.java, PassthroughModelLoader | ||||
|                 .Factory()) | ||||
|         registry.append( | ||||
|             InputStream::class.java, InputStream::class.java, | ||||
|             PassthroughModelLoader | ||||
|                 .Factory() | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -14,7 +14,7 @@ import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|         Worker(context, workerParams) { | ||||
|     Worker(context, workerParams) { | ||||
|  | ||||
|     override fun doWork(): Result { | ||||
|         LibraryUpdateService.start(context) | ||||
| @@ -30,22 +30,24 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet | ||||
|             if (interval > 0) { | ||||
|                 val restrictions = preferences.libraryUpdateRestriction()!! | ||||
|                 val acRestriction = "ac" in restrictions | ||||
|                 val wifiRestriction = if ("wifi" in restrictions) | ||||
|                 val wifiRestriction = if ("wifi" in restrictions) { | ||||
|                     NetworkType.UNMETERED | ||||
|                 else | ||||
|                 } else { | ||||
|                     NetworkType.CONNECTED | ||||
|                 } | ||||
|  | ||||
|                 val constraints = Constraints.Builder() | ||||
|                         .setRequiredNetworkType(wifiRestriction) | ||||
|                         .setRequiresCharging(acRestriction) | ||||
|                         .build() | ||||
|                     .setRequiredNetworkType(wifiRestriction) | ||||
|                     .setRequiresCharging(acRestriction) | ||||
|                     .build() | ||||
|  | ||||
|                 val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>( | ||||
|                         interval.toLong(), TimeUnit.HOURS, | ||||
|                         10, TimeUnit.MINUTES) | ||||
|                         .addTag(TAG) | ||||
|                         .setConstraints(constraints) | ||||
|                         .build() | ||||
|                     interval.toLong(), TimeUnit.HOURS, | ||||
|                     10, TimeUnit.MINUTES | ||||
|                 ) | ||||
|                     .addTag(TAG) | ||||
|                     .setConstraints(constraints) | ||||
|                     .build() | ||||
|  | ||||
|                 WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request) | ||||
|             } else { | ||||
|   | ||||
| @@ -8,8 +8,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| object LibraryUpdateRanker { | ||||
|  | ||||
|     val rankingScheme = listOf( | ||||
|             (this::lexicographicRanking)(), | ||||
|             (this::latestFirstRanking)()) | ||||
|         (this::lexicographicRanking)(), | ||||
|         (this::latestFirstRanking)() | ||||
|     ) | ||||
|  | ||||
|     /** | ||||
|      * Provides a total ordering over all the Mangas. | ||||
| @@ -22,7 +23,7 @@ object LibraryUpdateRanker { | ||||
|      */ | ||||
|     fun latestFirstRanking(): Comparator<Manga> { | ||||
|         return Comparator { mangaFirst: Manga, | ||||
|                             mangaSecond: Manga -> | ||||
|             mangaSecond: Manga -> | ||||
|             compareValues(mangaSecond.last_update, mangaFirst.last_update) | ||||
|         } | ||||
|     } | ||||
| @@ -35,7 +36,7 @@ object LibraryUpdateRanker { | ||||
|      */ | ||||
|     fun lexicographicRanking(): Comparator<Manga> { | ||||
|         return Comparator { mangaFirst: Manga, | ||||
|                             mangaSecond: Manga -> | ||||
|             mangaSecond: Manga -> | ||||
|             compareValues(mangaFirst.title, mangaSecond.title) | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -184,7 +184,8 @@ class LibraryUpdateService( | ||||
|         super.onCreate() | ||||
|         startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotificationBuilder.build()) | ||||
|         wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( | ||||
|                 PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock") | ||||
|             PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock" | ||||
|         ) | ||||
|         wakeLock.acquire() | ||||
|     } | ||||
|  | ||||
| @@ -218,33 +219,37 @@ 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() | ||||
|  | ||||
|         // Update favorite manga. Destroy service when completed or in case of an error. | ||||
|         subscription = Observable | ||||
|                 .defer { | ||||
|                     val selectedScheme = preferences.libraryUpdatePrioritization().get() | ||||
|                     val mangaList = getMangaToUpdate(intent, target) | ||||
|                             .sortedWith(rankingScheme[selectedScheme]) | ||||
|             .defer { | ||||
|                 val selectedScheme = preferences.libraryUpdatePrioritization().get() | ||||
|                 val mangaList = getMangaToUpdate(intent, target) | ||||
|                     .sortedWith(rankingScheme[selectedScheme]) | ||||
|  | ||||
|                     // Update either chapter list or manga details. | ||||
|                     when (target) { | ||||
|                         Target.CHAPTERS -> updateChapterList(mangaList) | ||||
|                         Target.DETAILS -> updateDetails(mangaList) | ||||
|                         Target.TRACKING -> updateTrackings(mangaList) | ||||
|                     } | ||||
|                 // Update either chapter list or manga details. | ||||
|                 when (target) { | ||||
|                     Target.CHAPTERS -> updateChapterList(mangaList) | ||||
|                     Target.DETAILS -> updateDetails(mangaList) | ||||
|                     Target.TRACKING -> updateTrackings(mangaList) | ||||
|                 } | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .subscribe({ | ||||
|                 }, { | ||||
|             } | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .subscribe( | ||||
|                 { | ||||
|                 }, | ||||
|                 { | ||||
|                     Timber.e(it) | ||||
|                     stopSelf(startId) | ||||
|                 }, { | ||||
|                 }, | ||||
|                 { | ||||
|                     stopSelf(startId) | ||||
|                 }) | ||||
|                 } | ||||
|             ) | ||||
|  | ||||
|         return START_REDELIVER_INTENT | ||||
|     } | ||||
| @@ -259,16 +264,17 @@ class LibraryUpdateService( | ||||
|     fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> { | ||||
|         val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) | ||||
|  | ||||
|         var listToUpdate = if (categoryId != -1) | ||||
|         var listToUpdate = if (categoryId != -1) { | ||||
|             db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId } | ||||
|         else { | ||||
|         } else { | ||||
|             val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt) | ||||
|             if (categoriesToUpdate.isNotEmpty()) | ||||
|             if (categoriesToUpdate.isNotEmpty()) { | ||||
|                 db.getLibraryMangas().executeAsBlocking() | ||||
|                         .filter { it.category in categoriesToUpdate } | ||||
|                         .distinctBy { it.id } | ||||
|             else | ||||
|                     .filter { it.category in categoriesToUpdate } | ||||
|                     .distinctBy { it.id } | ||||
|             } else { | ||||
|                 db.getLibraryMangas().executeAsBlocking().distinctBy { it.id } | ||||
|             } | ||||
|         } | ||||
|         if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) { | ||||
|             listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED } | ||||
| @@ -302,55 +308,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 chapters of the manga. | ||||
|                 .concatMap { manga -> | ||||
|                     updateManga(manga) | ||||
|                             // If there's any error, return empty update and continue. | ||||
|                             .onErrorReturn { | ||||
|                                 failedUpdates.add(manga) | ||||
|                                 Pair(emptyList(), emptyList()) | ||||
|                             } | ||||
|                             // Filter out mangas without new chapters (or failed). | ||||
|                             .filter { pair -> pair.first.isNotEmpty() } | ||||
|                             .doOnNext { | ||||
|                                 if (downloadNew && (categoriesToDownload.isEmpty() || | ||||
|                                                 manga.category in categoriesToDownload)) { | ||||
|  | ||||
|                                     downloadChapters(manga, it.first) | ||||
|                                     hasDownloads = true | ||||
|                                 } | ||||
|                             } | ||||
|                             // Convert to the manga that contains new chapters. | ||||
|                             .map { | ||||
|                                 Pair( | ||||
|                                         manga, | ||||
|                                         (it.first.sortedByDescending { ch -> ch.source_order }.toTypedArray()) | ||||
|                                 ) | ||||
|                             } | ||||
|                 } | ||||
|                 // Add manga with new chapters to the list. | ||||
|                 .doOnNext { manga -> | ||||
|                     // Add to the list | ||||
|                     newUpdates.add(manga) | ||||
|                 } | ||||
|                 // Notify result of the overall update. | ||||
|                 .doOnCompleted { | ||||
|                     if (newUpdates.isNotEmpty()) { | ||||
|                         showUpdateNotifications(newUpdates) | ||||
|                         if (downloadNew && hasDownloads) { | ||||
|                             DownloadService.start(this) | ||||
|             // Notify manga that will update. | ||||
|             .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } | ||||
|             // Update the chapters of the manga. | ||||
|             .concatMap { manga -> | ||||
|                 updateManga(manga) | ||||
|                     // If there's any error, return empty update and continue. | ||||
|                     .onErrorReturn { | ||||
|                         failedUpdates.add(manga) | ||||
|                         Pair(emptyList(), emptyList()) | ||||
|                     } | ||||
|                     // Filter out mangas without new chapters (or failed). | ||||
|                     .filter { pair -> pair.first.isNotEmpty() } | ||||
|                     .doOnNext { | ||||
|                         if (downloadNew && ( | ||||
|                             categoriesToDownload.isEmpty() || | ||||
|                                 manga.category in categoriesToDownload | ||||
|                             ) | ||||
|                         ) { | ||||
|                             downloadChapters(manga, it.first) | ||||
|                             hasDownloads = true | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     if (failedUpdates.isNotEmpty()) { | ||||
|                         Timber.e("Failed updating: ${failedUpdates.map { it.title }}") | ||||
|                     // Convert to the manga that contains new chapters. | ||||
|                     .map { | ||||
|                         Pair( | ||||
|                             manga, | ||||
|                             (it.first.sortedByDescending { ch -> ch.source_order }.toTypedArray()) | ||||
|                         ) | ||||
|                     } | ||||
|             } | ||||
|             // Add manga with new chapters to the list. | ||||
|             .doOnNext { manga -> | ||||
|                 // Add to the list | ||||
|                 newUpdates.add(manga) | ||||
|             } | ||||
|             // Notify result of the overall update. | ||||
|             .doOnCompleted { | ||||
|                 if (newUpdates.isNotEmpty()) { | ||||
|                     showUpdateNotifications(newUpdates) | ||||
|                     if (downloadNew && hasDownloads) { | ||||
|                         DownloadService.start(this) | ||||
|                     } | ||||
|  | ||||
|                     cancelProgressNotification() | ||||
|                 } | ||||
|                 .map { manga -> manga.first } | ||||
|  | ||||
|                 if (failedUpdates.isNotEmpty()) { | ||||
|                     Timber.e("Failed updating: ${failedUpdates.map { it.title }}") | ||||
|                 } | ||||
|  | ||||
|                 cancelProgressNotification() | ||||
|             } | ||||
|             .map { manga -> manga.first } | ||||
|     } | ||||
|  | ||||
|     fun downloadChapters(manga: Manga, chapters: List<Chapter>) { | ||||
| @@ -373,7 +381,7 @@ class LibraryUpdateService( | ||||
|     fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> { | ||||
|         val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty() | ||||
|         return source.fetchChapterList(manga) | ||||
|                 .map { syncChaptersWithSource(db, it, manga, source) } | ||||
|             .map { syncChaptersWithSource(db, it, manga, source) } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -389,24 +397,24 @@ class LibraryUpdateService( | ||||
|  | ||||
|         // Emit each manga and update it sequentially. | ||||
|         return Observable.from(mangaToUpdate) | ||||
|                 // Notify manga that will update. | ||||
|                 .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } | ||||
|                 // Update the details of the manga. | ||||
|                 .concatMap { manga -> | ||||
|                     val source = sourceManager.get(manga.source) as? HttpSource | ||||
|                             ?: return@concatMap Observable.empty<LibraryManga>() | ||||
|             // Notify manga that will update. | ||||
|             .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } | ||||
|             // Update the details of the manga. | ||||
|             .concatMap { manga -> | ||||
|                 val source = sourceManager.get(manga.source) as? HttpSource | ||||
|                     ?: return@concatMap Observable.empty<LibraryManga>() | ||||
|  | ||||
|                     source.fetchMangaDetails(manga) | ||||
|                             .map { networkManga -> | ||||
|                                 manga.copyFrom(networkManga) | ||||
|                                 db.insertManga(manga).executeAsBlocking() | ||||
|                                 manga | ||||
|                             } | ||||
|                             .onErrorReturn { manga } | ||||
|                 } | ||||
|                 .doOnCompleted { | ||||
|                     cancelProgressNotification() | ||||
|                 } | ||||
|                 source.fetchMangaDetails(manga) | ||||
|                     .map { networkManga -> | ||||
|                         manga.copyFrom(networkManga) | ||||
|                         db.insertManga(manga).executeAsBlocking() | ||||
|                         manga | ||||
|                     } | ||||
|                     .onErrorReturn { manga } | ||||
|             } | ||||
|             .doOnCompleted { | ||||
|                 cancelProgressNotification() | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -421,28 +429,28 @@ class LibraryUpdateService( | ||||
|  | ||||
|         // 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() | ||||
|             // 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() | ||||
|                 } | ||||
|                 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() | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -453,15 +461,19 @@ class LibraryUpdateService( | ||||
|      * @param total the total progress. | ||||
|      */ | ||||
|     private fun showProgressNotification(manga: Manga, current: Int, total: Int) { | ||||
|         val title = if (preferences.hideNotificationContent()) | ||||
|         val title = if (preferences.hideNotificationContent()) { | ||||
|             getString(R.string.notification_check_updates) | ||||
|         else | ||||
|         } else { | ||||
|             manga.title | ||||
|         } | ||||
|  | ||||
|         notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotificationBuilder | ||||
|         notificationManager.notify( | ||||
|             Notifications.ID_LIBRARY_PROGRESS, | ||||
|             progressNotificationBuilder | ||||
|                 .setContentTitle(title) | ||||
|                 .setProgress(total, current, false) | ||||
|                 .build()) | ||||
|                 .build() | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -476,31 +488,38 @@ class LibraryUpdateService( | ||||
|  | ||||
|         NotificationManagerCompat.from(this).apply { | ||||
|             // Parent group notification | ||||
|             notify(Notifications.ID_NEW_CHAPTERS, notification(Notifications.CHANNEL_NEW_CHAPTERS) { | ||||
|                 setContentTitle(getString(R.string.notification_new_chapters)) | ||||
|                 if (updates.size == 1 && !preferences.hideNotificationContent()) { | ||||
|                     setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN)) | ||||
|                 } else { | ||||
|                     setContentText(resources.getQuantityString(R.plurals.notification_new_chapters_summary, updates.size, updates.size)) | ||||
|             notify( | ||||
|                 Notifications.ID_NEW_CHAPTERS, | ||||
|                 notification(Notifications.CHANNEL_NEW_CHAPTERS) { | ||||
|                     setContentTitle(getString(R.string.notification_new_chapters)) | ||||
|                     if (updates.size == 1 && !preferences.hideNotificationContent()) { | ||||
|                         setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN)) | ||||
|                     } else { | ||||
|                         setContentText(resources.getQuantityString(R.plurals.notification_new_chapters_summary, updates.size, updates.size)) | ||||
|  | ||||
|                     if (!preferences.hideNotificationContent()) { | ||||
|                         setStyle(NotificationCompat.BigTextStyle().bigText(updates.joinToString("\n") { | ||||
|                             it.first.title.chop(NOTIF_TITLE_MAX_LEN) | ||||
|                         })) | ||||
|                         if (!preferences.hideNotificationContent()) { | ||||
|                             setStyle( | ||||
|                                 NotificationCompat.BigTextStyle().bigText( | ||||
|                                     updates.joinToString("\n") { | ||||
|                                         it.first.title.chop(NOTIF_TITLE_MAX_LEN) | ||||
|                                     } | ||||
|                                 ) | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     setSmallIcon(R.drawable.ic_tachi) | ||||
|                     setLargeIcon(notificationBitmap) | ||||
|  | ||||
|                     setGroup(Notifications.GROUP_NEW_CHAPTERS) | ||||
|                     setGroupAlertBehavior(GROUP_ALERT_SUMMARY) | ||||
|                     setGroupSummary(true) | ||||
|                     priority = NotificationCompat.PRIORITY_HIGH | ||||
|  | ||||
|                     setContentIntent(getNotificationIntent()) | ||||
|                     setAutoCancel(true) | ||||
|                 } | ||||
|  | ||||
|                 setSmallIcon(R.drawable.ic_tachi) | ||||
|                 setLargeIcon(notificationBitmap) | ||||
|  | ||||
|                 setGroup(Notifications.GROUP_NEW_CHAPTERS) | ||||
|                 setGroupAlertBehavior(GROUP_ALERT_SUMMARY) | ||||
|                 setGroupSummary(true) | ||||
|                 priority = NotificationCompat.PRIORITY_HIGH | ||||
|  | ||||
|                 setContentIntent(getNotificationIntent()) | ||||
|                 setAutoCancel(true) | ||||
|             }) | ||||
|             ) | ||||
|  | ||||
|             // Per-manga notification | ||||
|             if (!preferences.hideNotificationContent()) { | ||||
| @@ -536,13 +555,21 @@ class LibraryUpdateService( | ||||
|             setAutoCancel(true) | ||||
|  | ||||
|             // Mark chapters as read action | ||||
|             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_glasses_black_24dp, getString(R.string.action_mark_as_read), | ||||
|                 NotificationReceiver.markAsReadPendingBroadcast( | ||||
|                     this@LibraryUpdateService, | ||||
|                     manga, chapters, Notifications.ID_NEW_CHAPTERS | ||||
|                 ) | ||||
|             ) | ||||
|             // View chapters action | ||||
|             addAction(R.drawable.ic_book_24dp, getString(R.string.action_view_chapters), | ||||
|                     NotificationReceiver.openChapterPendingActivity(this@LibraryUpdateService, | ||||
|                             manga, Notifications.ID_NEW_CHAPTERS)) | ||||
|             addAction( | ||||
|                 R.drawable.ic_book_24dp, getString(R.string.action_view_chapters), | ||||
|                 NotificationReceiver.openChapterPendingActivity( | ||||
|                     this@LibraryUpdateService, | ||||
|                     manga, Notifications.ID_NEW_CHAPTERS | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -556,28 +583,31 @@ class LibraryUpdateService( | ||||
|     private fun getMangaIcon(manga: Manga): Bitmap? { | ||||
|         return try { | ||||
|             Glide.with(this) | ||||
|                     .asBitmap() | ||||
|                     .load(manga.toMangaThumbnail()) | ||||
|                     .dontTransform() | ||||
|                     .centerCrop() | ||||
|                     .circleCrop() | ||||
|                     .override(NOTIF_ICON_SIZE, NOTIF_ICON_SIZE) | ||||
|                     .submit() | ||||
|                     .get() | ||||
|                 .asBitmap() | ||||
|                 .load(manga.toMangaThumbnail()) | ||||
|                 .dontTransform() | ||||
|                 .centerCrop() | ||||
|                 .circleCrop() | ||||
|                 .override(NOTIF_ICON_SIZE, NOTIF_ICON_SIZE) | ||||
|                 .submit() | ||||
|                 .get() | ||||
|         } catch (e: Exception) { | ||||
|             null | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun getNewChaptersDescription(chapters: Array<Chapter>): String { | ||||
|         val formatter = DecimalFormat("#.###", DecimalFormatSymbols() | ||||
|                 .apply { decimalSeparator = '.' }) | ||||
|         val formatter = DecimalFormat( | ||||
|             "#.###", | ||||
|             DecimalFormatSymbols() | ||||
|                 .apply { decimalSeparator = '.' } | ||||
|         ) | ||||
|  | ||||
|         val displayableChapterNumbers = chapters | ||||
|                 .filter { it.isRecognizedNumber } | ||||
|                 .sortedBy { it.chapter_number } | ||||
|                 .map { formatter.format(it.chapter_number) } | ||||
|                 .toSet() | ||||
|             .filter { it.isRecognizedNumber } | ||||
|             .sortedBy { it.chapter_number } | ||||
|             .map { formatter.format(it.chapter_number) } | ||||
|             .toSet() | ||||
|  | ||||
|         return when (displayableChapterNumbers.size) { | ||||
|             // No sensible chapter numbers to show (i.e. no chapters have parsed chapter number) | ||||
|   | ||||
| @@ -54,22 +54,35 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|             // Clear the download queue | ||||
|             ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true) | ||||
|             // Launch share activity and dismiss notification | ||||
|             ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION), | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) | ||||
|             ACTION_SHARE_IMAGE -> | ||||
|                 shareImage( | ||||
|                     context, intent.getStringExtra(EXTRA_FILE_LOCATION), | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) | ||||
|                 ) | ||||
|             // Delete image from path and dismiss notification | ||||
|             ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION), | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) | ||||
|             ACTION_DELETE_IMAGE -> | ||||
|                 deleteImage( | ||||
|                     context, intent.getStringExtra(EXTRA_FILE_LOCATION), | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) | ||||
|                 ) | ||||
|             // Share backup file | ||||
|             ACTION_SHARE_BACKUP -> shareBackup(context, intent.getParcelableExtra(EXTRA_URI), | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) | ||||
|             ACTION_CANCEL_RESTORE -> cancelRestore(context, | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) | ||||
|             ACTION_SHARE_BACKUP -> | ||||
|                 shareBackup( | ||||
|                     context, intent.getParcelableExtra(EXTRA_URI), | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) | ||||
|                 ) | ||||
|             ACTION_CANCEL_RESTORE -> cancelRestore( | ||||
|                 context, | ||||
|                 intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) | ||||
|             ) | ||||
|             // Cancel library update and dismiss notification | ||||
|             ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS) | ||||
|             // Open reader activity | ||||
|             ACTION_OPEN_CHAPTER -> { | ||||
|                 openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1), | ||||
|                         intent.getLongExtra(EXTRA_CHAPTER_ID, -1)) | ||||
|                 openChapter( | ||||
|                     context, intent.getLongExtra(EXTRA_MANGA_ID, -1), | ||||
|                     intent.getLongExtra(EXTRA_CHAPTER_ID, -1) | ||||
|                 ) | ||||
|             } | ||||
|             // Mark updated manga chapters as read | ||||
|             ACTION_MARK_AS_READ -> { | ||||
| @@ -208,19 +221,19 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|  | ||||
|         launchIO { | ||||
|             chapterUrls.mapNotNull { db.getChapter(it, mangaId).executeAsBlocking() } | ||||
|                     .forEach { | ||||
|                         it.read = true | ||||
|                         db.updateChapterProgress(it).executeAsBlocking() | ||||
|                         if (preferences.removeAfterMarkedAsRead()) { | ||||
|                             val manga = db.getManga(mangaId).executeAsBlocking() | ||||
|                             if (manga != null) { | ||||
|                                 val source = sourceManager.get(manga.source) | ||||
|                                 if (source != null) { | ||||
|                                     downloadManager.deleteChapters(listOf(it), manga, source) | ||||
|                                 } | ||||
|                 .forEach { | ||||
|                     it.read = true | ||||
|                     db.updateChapterProgress(it).executeAsBlocking() | ||||
|                     if (preferences.removeAfterMarkedAsRead()) { | ||||
|                         val manga = db.getManga(mangaId).executeAsBlocking() | ||||
|                         if (manga != null) { | ||||
|                             val source = sourceManager.get(manga.source) | ||||
|                             if (source != null) { | ||||
|                                 downloadManager.deleteChapters(listOf(it), manga, source) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -427,11 +440,11 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|          */ | ||||
|         internal fun openChapterPendingActivity(context: Context, manga: Manga, groupId: Int): PendingIntent { | ||||
|             val newIntent = | ||||
|                     Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA) | ||||
|                             .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) | ||||
|                             .putExtra(MangaController.MANGA_EXTRA, manga.id) | ||||
|                             .putExtra("notificationId", manga.id.hashCode()) | ||||
|                             .putExtra("groupId", groupId) | ||||
|                 Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA) | ||||
|                     .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) | ||||
|                     .putExtra(MangaController.MANGA_EXTRA, manga.id) | ||||
|                     .putExtra("notificationId", manga.id.hashCode()) | ||||
|                     .putExtra("groupId", groupId) | ||||
|             return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT) | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -61,24 +61,36 @@ object Notifications { | ||||
|         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return | ||||
|  | ||||
|         val channels = listOf( | ||||
|                 NotificationChannel(CHANNEL_COMMON, context.getString(R.string.channel_common), | ||||
|                         NotificationManager.IMPORTANCE_LOW), | ||||
|                 NotificationChannel(CHANNEL_LIBRARY, context.getString(R.string.channel_library), | ||||
|                         NotificationManager.IMPORTANCE_LOW).apply { | ||||
|                     setShowBadge(false) | ||||
|                 }, | ||||
|                 NotificationChannel(CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader), | ||||
|                         NotificationManager.IMPORTANCE_LOW).apply { | ||||
|                     setShowBadge(false) | ||||
|                 }, | ||||
|                 NotificationChannel(CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters), | ||||
|                         NotificationManager.IMPORTANCE_DEFAULT), | ||||
|                 NotificationChannel(CHANNEL_UPDATES_TO_EXTS, context.getString(R.string.channel_ext_updates), | ||||
|                         NotificationManager.IMPORTANCE_DEFAULT), | ||||
|                 NotificationChannel(CHANNEL_BACKUP_RESTORE, context.getString(R.string.channel_backup_restore), | ||||
|                     NotificationManager.IMPORTANCE_HIGH).apply { | ||||
|                     setShowBadge(false) | ||||
|                 } | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_COMMON, context.getString(R.string.channel_common), | ||||
|                 NotificationManager.IMPORTANCE_LOW | ||||
|             ), | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_LIBRARY, context.getString(R.string.channel_library), | ||||
|                 NotificationManager.IMPORTANCE_LOW | ||||
|             ).apply { | ||||
|                 setShowBadge(false) | ||||
|             }, | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader), | ||||
|                 NotificationManager.IMPORTANCE_LOW | ||||
|             ).apply { | ||||
|                 setShowBadge(false) | ||||
|             }, | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters), | ||||
|                 NotificationManager.IMPORTANCE_DEFAULT | ||||
|             ), | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_UPDATES_TO_EXTS, context.getString(R.string.channel_ext_updates), | ||||
|                 NotificationManager.IMPORTANCE_DEFAULT | ||||
|             ), | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_BACKUP_RESTORE, context.getString(R.string.channel_backup_restore), | ||||
|                 NotificationManager.IMPORTANCE_HIGH | ||||
|             ).apply { | ||||
|                 setShowBadge(false) | ||||
|             } | ||||
|         ) | ||||
|         context.notificationManager.createNotificationChannels(channels) | ||||
|     } | ||||
|   | ||||
| @@ -45,12 +45,20 @@ class PreferencesHelper(val context: Context) { | ||||
|     private val flowPrefs = FlowSharedPreferences(prefs) | ||||
|  | ||||
|     private val defaultDownloadsDir = Uri.fromFile( | ||||
|             File(Environment.getExternalStorageDirectory().absolutePath + File.separator + | ||||
|                     context.getString(R.string.app_name), "downloads")) | ||||
|         File( | ||||
|             Environment.getExternalStorageDirectory().absolutePath + File.separator + | ||||
|                 context.getString(R.string.app_name), | ||||
|             "downloads" | ||||
|         ) | ||||
|     ) | ||||
|  | ||||
|     private val defaultBackupDir = Uri.fromFile( | ||||
|             File(Environment.getExternalStorageDirectory().absolutePath + File.separator + | ||||
|                     context.getString(R.string.app_name), "backup")) | ||||
|         File( | ||||
|             Environment.getExternalStorageDirectory().absolutePath + File.separator + | ||||
|                 context.getString(R.string.app_name), | ||||
|             "backup" | ||||
|         ) | ||||
|     ) | ||||
|  | ||||
|     fun startScreen() = prefs.getInt(Keys.startScreen, 1) | ||||
|  | ||||
| @@ -148,9 +156,9 @@ class PreferencesHelper(val context: Context) { | ||||
|  | ||||
|     fun setTrackCredentials(sync: TrackService, username: String, password: String) { | ||||
|         prefs.edit() | ||||
|                 .putString(Keys.trackUsername(sync.id), username) | ||||
|                 .putString(Keys.trackPassword(sync.id), password) | ||||
|                 .apply() | ||||
|             .putString(Keys.trackUsername(sync.id), username) | ||||
|             .putString(Keys.trackPassword(sync.id), password) | ||||
|             .apply() | ||||
|     } | ||||
|  | ||||
|     fun trackToken(sync: TrackService) = flowPrefs.getString(Keys.trackToken(sync.id), "") | ||||
|   | ||||
| @@ -63,7 +63,7 @@ abstract class TrackService(val id: Int) { | ||||
|  | ||||
|     open val isLogged: Boolean | ||||
|         get() = getUsername().isNotEmpty() && | ||||
|                 getPassword().isNotEmpty() | ||||
|             getPassword().isNotEmpty() | ||||
|  | ||||
|     fun getUsername() = preferences.trackUsername(this)!! | ||||
|  | ||||
|   | ||||
| @@ -150,18 +150,18 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun bind(track: Track): Observable<Track> { | ||||
|         return api.findLibManga(track, getUsername().toInt()) | ||||
|                 .flatMap { remoteTrack -> | ||||
|                     if (remoteTrack != null) { | ||||
|                         track.copyPersonalFrom(remoteTrack) | ||||
|                         track.library_id = remoteTrack.library_id | ||||
|                         update(track) | ||||
|                     } else { | ||||
|                         // Set default fields if it's not found in the list | ||||
|                         track.score = DEFAULT_SCORE.toFloat() | ||||
|                         track.status = DEFAULT_STATUS | ||||
|                         add(track) | ||||
|                     } | ||||
|             .flatMap { remoteTrack -> | ||||
|                 if (remoteTrack != null) { | ||||
|                     track.copyPersonalFrom(remoteTrack) | ||||
|                     track.library_id = remoteTrack.library_id | ||||
|                     update(track) | ||||
|                 } else { | ||||
|                     // Set default fields if it's not found in the list | ||||
|                     track.score = DEFAULT_SCORE.toFloat() | ||||
|                     track.status = DEFAULT_STATUS | ||||
|                     add(track) | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     override fun search(query: String): Observable<List<TrackSearch>> { | ||||
| @@ -170,11 +170,11 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun refresh(track: Track): Observable<Track> { | ||||
|         return api.getLibManga(track, getUsername().toInt()) | ||||
|                 .map { remoteTrack -> | ||||
|                     track.copyPersonalFrom(remoteTrack) | ||||
|                     track.total_chapters = remoteTrack.total_chapters | ||||
|                     track | ||||
|                 } | ||||
|             .map { remoteTrack -> | ||||
|                 track.copyPersonalFrom(remoteTrack) | ||||
|                 track.total_chapters = remoteTrack.total_chapters | ||||
|                 track | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     override fun login(username: String, password: String) = login(password) | ||||
|   | ||||
| @@ -25,7 +25,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|     private val authClient = client.newBuilder().addInterceptor(interceptor).build() | ||||
|  | ||||
|     fun addLibManga(track: Track): Observable<Track> { | ||||
|         val query = """ | ||||
|         val query = | ||||
|             """ | ||||
|             |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { | ||||
|                 |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {  | ||||
|                 |   id  | ||||
| @@ -34,35 +35,36 @@ 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() | ||||
|             "mangaId" to track.media_id, | ||||
|             "progress" to track.last_chapter_read, | ||||
|             "status" to track.toAnilistStatus() | ||||
|         ) | ||||
|         val payload = jsonObject( | ||||
|                 "query" to query, | ||||
|                 "variables" to variables | ||||
|             "query" to query, | ||||
|             "variables" to variables | ||||
|         ) | ||||
|         val body = payload.toString().toRequestBody(jsonMime) | ||||
|         val request = Request.Builder() | ||||
|                 .url(apiUrl) | ||||
|                 .post(body) | ||||
|                 .build() | ||||
|             .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 = JsonParser.parseString(responseBody).obj | ||||
|                     track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong | ||||
|                     track | ||||
|             .asObservableSuccess() | ||||
|             .map { netResponse -> | ||||
|                 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 | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun updateLibManga(track: Track): Observable<Track> { | ||||
|         val query = """ | ||||
|         val query = | ||||
|             """ | ||||
|             |mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) { | ||||
|                 |SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) { | ||||
|                     |id | ||||
| @@ -72,29 +74,30 @@ 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() | ||||
|             "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 | ||||
|             "query" to query, | ||||
|             "variables" to variables | ||||
|         ) | ||||
|         val body = payload.toString().toRequestBody(jsonMime) | ||||
|         val request = Request.Builder() | ||||
|                 .url(apiUrl) | ||||
|                 .post(body) | ||||
|                 .build() | ||||
|             .url(apiUrl) | ||||
|             .post(body) | ||||
|             .build() | ||||
|         return authClient.newCall(request) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { | ||||
|                     track | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { | ||||
|                 track | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun search(search: String): Observable<List<TrackSearch>> { | ||||
|         val query = """ | ||||
|         val query = | ||||
|             """ | ||||
|             |query Search(${'$'}query: String) { | ||||
|                 |Page (perPage: 50) { | ||||
|                     |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { | ||||
| @@ -119,35 +122,36 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|             |} | ||||
|             |""".trimMargin() | ||||
|         val variables = jsonObject( | ||||
|                 "query" to search | ||||
|             "query" to search | ||||
|         ) | ||||
|         val payload = jsonObject( | ||||
|                 "query" to query, | ||||
|                 "variables" to variables | ||||
|             "query" to query, | ||||
|             "variables" to variables | ||||
|         ) | ||||
|         val body = payload.toString().toRequestBody(jsonMime) | ||||
|         val request = Request.Builder() | ||||
|                 .url(apiUrl) | ||||
|                 .post(body) | ||||
|                 .build() | ||||
|             .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 = JsonParser.parseString(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() } | ||||
|             .asObservableSuccess() | ||||
|             .map { netResponse -> | ||||
|                 val responseBody = netResponse.body?.string().orEmpty() | ||||
|                 if (responseBody.isEmpty()) { | ||||
|                     throw Exception("Null Response") | ||||
|                 } | ||||
|                 val response = JsonParser.parseString(responseBody).obj | ||||
|                 val data = response["data"]!!.obj | ||||
|                 val page = data["Page"].obj | ||||
|                 val media = page["media"].array | ||||
|                 val entries = media.map { jsonToALManga(it.obj) } | ||||
|                 entries.map { it.toTrack() } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun findLibManga(track: Track, userid: Int): Observable<Track?> { | ||||
|         val query = """ | ||||
|         val query = | ||||
|             """ | ||||
|             |query (${'$'}id: Int!, ${'$'}manga_id: Int!) { | ||||
|                 |Page { | ||||
|                     |mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) { | ||||
| @@ -178,37 +182,37 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|             |} | ||||
|             |""".trimMargin() | ||||
|         val variables = jsonObject( | ||||
|                 "id" to userid, | ||||
|                 "manga_id" to track.media_id | ||||
|             "id" to userid, | ||||
|             "manga_id" to track.media_id | ||||
|         ) | ||||
|         val payload = jsonObject( | ||||
|                 "query" to query, | ||||
|                 "variables" to variables | ||||
|             "query" to query, | ||||
|             "variables" to variables | ||||
|         ) | ||||
|         val body = payload.toString().toRequestBody(jsonMime) | ||||
|         val request = Request.Builder() | ||||
|                 .url(apiUrl) | ||||
|                 .post(body) | ||||
|                 .build() | ||||
|             .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 = JsonParser.parseString(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() | ||||
|             .asObservableSuccess() | ||||
|             .map { netResponse -> | ||||
|                 val responseBody = netResponse.body?.string().orEmpty() | ||||
|                 if (responseBody.isEmpty()) { | ||||
|                     throw Exception("Null Response") | ||||
|                 } | ||||
|                 val response = JsonParser.parseString(responseBody).obj | ||||
|                 val data = response["data"]!!.obj | ||||
|                 val page = data["Page"].obj | ||||
|                 val media = page["mediaList"].array | ||||
|                 val entries = media.map { jsonToALUserManga(it.obj) } | ||||
|                 entries.firstOrNull()?.toTrack() | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun getLibManga(track: Track, userid: Int): Observable<Track> { | ||||
|         return findLibManga(track, userid) | ||||
|                 .map { it ?: throw Exception("Could not find manga") } | ||||
|             .map { it ?: throw Exception("Could not find manga") } | ||||
|     } | ||||
|  | ||||
|     fun createOAuth(token: String): OAuth { | ||||
| @@ -216,7 +220,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|     } | ||||
|  | ||||
|     fun getCurrentUser(): Observable<Pair<Int, String>> { | ||||
|         val query = """ | ||||
|         val query = | ||||
|             """ | ||||
|             |query User { | ||||
|                 |Viewer { | ||||
|                     |id | ||||
| @@ -227,41 +232,48 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|             |} | ||||
|             |""".trimMargin() | ||||
|         val payload = jsonObject( | ||||
|                 "query" to query | ||||
|             "query" to query | ||||
|         ) | ||||
|         val body = payload.toString().toRequestBody(jsonMime) | ||||
|         val request = Request.Builder() | ||||
|                 .url(apiUrl) | ||||
|                 .post(body) | ||||
|                 .build() | ||||
|             .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 = JsonParser.parseString(responseBody).obj | ||||
|                     val data = response["data"]!!.obj | ||||
|                     val viewer = data["Viewer"].obj | ||||
|                     Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString) | ||||
|             .asObservableSuccess() | ||||
|             .map { netResponse -> | ||||
|                 val responseBody = netResponse.body?.string().orEmpty() | ||||
|                 if (responseBody.isEmpty()) { | ||||
|                     throw Exception("Null Response") | ||||
|                 } | ||||
|                 val response = JsonParser.parseString(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.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) | ||||
|         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 { | ||||
| @@ -280,8 +292,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|         } | ||||
|  | ||||
|         fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon() | ||||
|                 .appendQueryParameter("client_id", clientId) | ||||
|                 .appendQueryParameter("response_type", "token") | ||||
|                 .build() | ||||
|             .appendQueryParameter("client_id", clientId) | ||||
|             .appendQueryParameter("response_type", "token") | ||||
|             .build() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -38,8 +38,8 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int | ||||
|  | ||||
|         // Add the authorization header to the original request. | ||||
|         val authRequest = originalRequest.newBuilder() | ||||
|                 .addHeader("Authorization", "Bearer ${oauth!!.access_token}") | ||||
|                 .build() | ||||
|             .addHeader("Authorization", "Bearer ${oauth!!.access_token}") | ||||
|             .build() | ||||
|  | ||||
|         return chain.proceed(authRequest) | ||||
|     } | ||||
|   | ||||
| @@ -39,23 +39,23 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun bind(track: Track): Observable<Track> { | ||||
|         return api.statusLibManga(track) | ||||
|                 .flatMap { | ||||
|                     api.findLibManga(track).flatMap { remoteTrack -> | ||||
|                         if (remoteTrack != null && it != null) { | ||||
|                             track.copyPersonalFrom(remoteTrack) | ||||
|                             track.library_id = remoteTrack.library_id | ||||
|                             track.status = remoteTrack.status | ||||
|                             track.last_chapter_read = remoteTrack.last_chapter_read | ||||
|                             refresh(track) | ||||
|                         } else { | ||||
|                             // Set default fields if it's not found in the list | ||||
|                             track.score = DEFAULT_SCORE.toFloat() | ||||
|                             track.status = DEFAULT_STATUS | ||||
|                             add(track) | ||||
|                             update(track) | ||||
|                         } | ||||
|             .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 fun search(query: String): Observable<List<TrackSearch>> { | ||||
| @@ -64,17 +64,17 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun refresh(track: Track): Observable<Track> { | ||||
|         return api.statusLibManga(track) | ||||
|                 .flatMap { | ||||
|                     track.copyPersonalFrom(it!!) | ||||
|                     api.findLibManga(track) | ||||
|                             .map { remoteTrack -> | ||||
|                                 if (remoteTrack != null) { | ||||
|                                     track.total_chapters = remoteTrack.total_chapters | ||||
|                                     track.status = remoteTrack.status | ||||
|                                 } | ||||
|                                 track | ||||
|                             } | ||||
|                 } | ||||
|             .flatMap { | ||||
|                 track.copyPersonalFrom(it!!) | ||||
|                 api.findLibManga(track) | ||||
|                     .map { remoteTrack -> | ||||
|                         if (remoteTrack != null) { | ||||
|                             track.total_chapters = remoteTrack.total_chapters | ||||
|                             track.status = remoteTrack.status | ||||
|                         } | ||||
|                         track | ||||
|                     } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     override fun getLogo() = R.drawable.ic_tracker_bangumi | ||||
|   | ||||
| @@ -26,73 +26,74 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept | ||||
|  | ||||
|     fun addLibManga(track: Track): Observable<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() | ||||
|             .url("$apiUrl/collection/${track.media_id}/update") | ||||
|             .post(body) | ||||
|             .build() | ||||
|         return authClient.newCall(request) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { | ||||
|                     track | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { | ||||
|                 track | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun updateLibManga(track: Track): Observable<Track> { | ||||
|         // chapter update | ||||
|         val body = FormBody.Builder() | ||||
|                 .add("watched_eps", track.last_chapter_read.toString()) | ||||
|                 .build() | ||||
|             .add("watched_eps", track.last_chapter_read.toString()) | ||||
|             .build() | ||||
|         val request = Request.Builder() | ||||
|                 .url("$apiUrl/subject/${track.media_id}/update/watched_eps") | ||||
|                 .post(body) | ||||
|                 .build() | ||||
|             .url("$apiUrl/subject/${track.media_id}/update/watched_eps") | ||||
|             .post(body) | ||||
|             .build() | ||||
|  | ||||
|         // read status update | ||||
|         val sbody = FormBody.Builder() | ||||
|                 .add("status", track.toBangumiStatus()) | ||||
|                 .build() | ||||
|             .add("status", track.toBangumiStatus()) | ||||
|             .build() | ||||
|         val srequest = Request.Builder() | ||||
|                 .url("$apiUrl/collection/${track.media_id}/update") | ||||
|                 .post(sbody) | ||||
|                 .build() | ||||
|             .url("$apiUrl/collection/${track.media_id}/update") | ||||
|             .post(sbody) | ||||
|             .build() | ||||
|         return authClient.newCall(srequest) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { | ||||
|                     track | ||||
|                 }.flatMap { | ||||
|                     authClient.newCall(request) | ||||
|                             .asObservableSuccess() | ||||
|                             .map { | ||||
|                                 track | ||||
|                             } | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { | ||||
|                 track | ||||
|             }.flatMap { | ||||
|                 authClient.newCall(request) | ||||
|                     .asObservableSuccess() | ||||
|                     .map { | ||||
|                         track | ||||
|                     } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun search(search: String): Observable<List<TrackSearch>> { | ||||
|         val url = Uri.parse( | ||||
|                 "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}").buildUpon() | ||||
|                 .appendQueryParameter("max_results", "20") | ||||
|                 .build() | ||||
|             "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}" | ||||
|         ).buildUpon() | ||||
|             .appendQueryParameter("max_results", "20") | ||||
|             .build() | ||||
|         val request = Request.Builder() | ||||
|                 .url(url.toString()) | ||||
|                 .get() | ||||
|                 .build() | ||||
|             .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 = JsonParser.parseString(responseBody).obj["list"]?.array | ||||
|                     response?.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) } | ||||
|             .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 = JsonParser.parseString(responseBody).obj["list"]?.array | ||||
|                 response?.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private fun jsonToSearch(obj: JsonObject): TrackSearch { | ||||
| @@ -109,9 +110,15 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept | ||||
|         return Track.create(TrackManager.BANGUMI).apply { | ||||
|             title = mangas["name"].asString | ||||
|             media_id = mangas["id"].asInt | ||||
|             score = if (mangas["rating"] != null) | ||||
|                 (if (mangas["rating"].isJsonObject) mangas["rating"].obj["score"].asFloat else 0f) | ||||
|             else 0f | ||||
|             score = if (mangas["rating"] != null) { | ||||
|                 if (mangas["rating"].isJsonObject) { | ||||
|                     mangas["rating"].obj["score"].asFloat | ||||
|                 } else { | ||||
|                     0f | ||||
|                 } | ||||
|             } else { | ||||
|                 0f | ||||
|             } | ||||
|             status = Bangumi.DEFAULT_STATUS | ||||
|             tracking_url = mangas["url"].asString | ||||
|         } | ||||
| @@ -120,37 +127,37 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept | ||||
|     fun findLibManga(track: Track): Observable<Track?> { | ||||
|         val urlMangas = "$apiUrl/subject/${track.media_id}" | ||||
|         val requestMangas = Request.Builder() | ||||
|                 .url(urlMangas) | ||||
|                 .get() | ||||
|                 .build() | ||||
|             .url(urlMangas) | ||||
|             .get() | ||||
|             .build() | ||||
|  | ||||
|         return authClient.newCall(requestMangas) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { netResponse -> | ||||
|                     // get comic info | ||||
|                     val responseBody = netResponse.body?.string().orEmpty() | ||||
|                     jsonToTrack(JsonParser.parseString(responseBody).obj) | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { netResponse -> | ||||
|                 // get comic info | ||||
|                 val responseBody = netResponse.body?.string().orEmpty() | ||||
|                 jsonToTrack(JsonParser.parseString(responseBody).obj) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun statusLibManga(track: Track): Observable<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 | ||||
|                 } | ||||
|             .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 | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun accessToken(code: String): Observable<OAuth> { | ||||
| @@ -163,14 +170,15 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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 { | ||||
| @@ -190,19 +198,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() | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -36,25 +36,28 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor { | ||||
|         } | ||||
|  | ||||
|         val authRequest = if (originalRequest.method == "GET") originalRequest.newBuilder() | ||||
|                 .header("User-Agent", "Tachiyomi") | ||||
|                 .url(originalRequest.url.newBuilder() | ||||
|                         .addQueryParameter("access_token", currAuth.access_token).build()) | ||||
|                 .build() else originalRequest.newBuilder() | ||||
|                 .post(addTocken(currAuth.access_token, originalRequest.body as FormBody)) | ||||
|                 .header("User-Agent", "Tachiyomi") | ||||
|                 .build() | ||||
|             .header("User-Agent", "Tachiyomi") | ||||
|             .url( | ||||
|                 originalRequest.url.newBuilder() | ||||
|                     .addQueryParameter("access_token", currAuth.access_token).build() | ||||
|             ) | ||||
|             .build() else originalRequest.newBuilder() | ||||
|             .post(addTocken(currAuth.access_token, originalRequest.body as FormBody)) | ||||
|             .header("User-Agent", "Tachiyomi") | ||||
|             .build() | ||||
|  | ||||
|         return chain.proceed(authRequest) | ||||
|     } | ||||
|  | ||||
|     fun newAuth(oauth: OAuth?) { | ||||
|         this.oauth = if (oauth == null) null else OAuth( | ||||
|                 oauth.access_token, | ||||
|                 oauth.token_type, | ||||
|                 System.currentTimeMillis() / 1000, | ||||
|                 oauth.expires_in, | ||||
|                 oauth.refresh_token, | ||||
|                 this.oauth?.user_id) | ||||
|             oauth.access_token, | ||||
|             oauth.token_type, | ||||
|             System.currentTimeMillis() / 1000, | ||||
|             oauth.expires_in, | ||||
|             oauth.refresh_token, | ||||
|             this.oauth?.user_id | ||||
|         ) | ||||
|  | ||||
|         bangumi.saveToken(oauth) | ||||
|     } | ||||
|   | ||||
| @@ -78,17 +78,17 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun bind(track: Track): Observable<Track> { | ||||
|         return api.findLibManga(track, getUserId()) | ||||
|                 .flatMap { remoteTrack -> | ||||
|                     if (remoteTrack != null) { | ||||
|                         track.copyPersonalFrom(remoteTrack) | ||||
|                         track.media_id = remoteTrack.media_id | ||||
|                         update(track) | ||||
|                     } else { | ||||
|                         track.score = DEFAULT_SCORE | ||||
|                         track.status = DEFAULT_STATUS | ||||
|                         add(track) | ||||
|                     } | ||||
|             .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 fun search(query: String): Observable<List<TrackSearch>> { | ||||
| @@ -97,20 +97,20 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun refresh(track: Track): Observable<Track> { | ||||
|         return api.getLibManga(track) | ||||
|                 .map { remoteTrack -> | ||||
|                     track.copyPersonalFrom(remoteTrack) | ||||
|                     track.total_chapters = remoteTrack.total_chapters | ||||
|                     track | ||||
|                 } | ||||
|             .map { remoteTrack -> | ||||
|                 track.copyPersonalFrom(remoteTrack) | ||||
|                 track.total_chapters = remoteTrack.total_chapters | ||||
|                 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() | ||||
|             .doOnNext { interceptor.newAuth(it) } | ||||
|             .flatMap { api.getCurrentUser() } | ||||
|             .doOnNext { userId -> saveCredentials(username, userId) } | ||||
|             .doOnError { logout() } | ||||
|             .toCompletable() | ||||
|     } | ||||
|  | ||||
|     override fun logout() { | ||||
|   | ||||
| @@ -33,59 +33,59 @@ 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(Rest::class.java) | ||||
|         .baseUrl(baseUrl) | ||||
|         .client(authClient) | ||||
|         .addConverterFactory(GsonConverterFactory.create(GsonBuilder().serializeNulls().create())) | ||||
|         .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) | ||||
|         .build() | ||||
|         .create(Rest::class.java) | ||||
|  | ||||
|     private val searchRest = Retrofit.Builder() | ||||
|             .baseUrl(algoliaKeyUrl) | ||||
|             .client(authClient) | ||||
|             .addConverterFactory(GsonConverterFactory.create()) | ||||
|             .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) | ||||
|             .build() | ||||
|             .create(SearchKeyRest::class.java) | ||||
|         .baseUrl(algoliaKeyUrl) | ||||
|         .client(authClient) | ||||
|         .addConverterFactory(GsonConverterFactory.create()) | ||||
|         .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) | ||||
|         .build() | ||||
|         .create(SearchKeyRest::class.java) | ||||
|  | ||||
|     private val algoliaRest = Retrofit.Builder() | ||||
|             .baseUrl(algoliaUrl) | ||||
|             .client(client) | ||||
|             .addConverterFactory(GsonConverterFactory.create()) | ||||
|             .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) | ||||
|             .build() | ||||
|             .create(AgoliaSearchRest::class.java) | ||||
|         .baseUrl(algoliaUrl) | ||||
|         .client(client) | ||||
|         .addConverterFactory(GsonConverterFactory.create()) | ||||
|         .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) | ||||
|         .build() | ||||
|         .create(AgoliaSearchRest::class.java) | ||||
|  | ||||
|     fun addLibManga(track: Track, userId: String): Observable<Track> { | ||||
|         return Observable.defer { | ||||
|             // @formatter:off | ||||
|             val data = jsonObject( | ||||
|                     "type" to "libraryEntries", | ||||
|                     "attributes" to jsonObject( | ||||
|                             "status" to track.toKitsuStatus(), | ||||
|                             "progress" to track.last_chapter_read | ||||
|                 "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" | ||||
|                         ) | ||||
|                     ), | ||||
|                     "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" | ||||
|                                     ) | ||||
|                             ) | ||||
|                     "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 | ||||
|                     } | ||||
|                 .map { json -> | ||||
|                     track.media_id = json["data"]["id"].int | ||||
|                     track | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -93,77 +93,77 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) | ||||
|         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() | ||||
|                     ) | ||||
|                 "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 } | ||||
|                 .map { track } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun search(query: String): Observable<List<TrackSearch>> { | ||||
|         return searchRest | ||||
|                 .getKey().map { json -> | ||||
|                     json["media"].asJsonObject["key"].string | ||||
|                 }.flatMap { key -> | ||||
|                     algoliaSearch(key, query) | ||||
|                 } | ||||
|             .getKey().map { json -> | ||||
|                 json["media"].asJsonObject["key"].string | ||||
|             }.flatMap { key -> | ||||
|                 algoliaSearch(key, query) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private fun algoliaSearch(key: String, query: String): Observable<List<TrackSearch>> { | ||||
|         val jsonObject = jsonObject("params" to "query=$query$algoliaFilter") | ||||
|         return algoliaRest | ||||
|                 .getSearchQuery(algoliaAppId, key, jsonObject) | ||||
|                 .map { json -> | ||||
|                     val data = json["hits"].array | ||||
|                     data.map { KitsuSearchManga(it.obj) } | ||||
|                             .filter { it.subType != "novel" } | ||||
|                             .map { it.toTrack() } | ||||
|                 } | ||||
|             .getSearchQuery(algoliaAppId, key, jsonObject) | ||||
|             .map { json -> | ||||
|                 val data = json["hits"].array | ||||
|                 data.map { KitsuSearchManga(it.obj) } | ||||
|                     .filter { it.subType != "novel" } | ||||
|                     .map { it.toTrack() } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun findLibManga(track: Track, userId: String): Observable<Track?> { | ||||
|         return rest.findLibManga(track.media_id, userId) | ||||
|                 .map { json -> | ||||
|                     val data = json["data"].array | ||||
|                     if (data.size() > 0) { | ||||
|                         val manga = json["included"].array[0].obj | ||||
|                         KitsuLibManga(data[0].obj, manga).toTrack() | ||||
|                     } else { | ||||
|                         null | ||||
|                     } | ||||
|             .map { json -> | ||||
|                 val data = json["data"].array | ||||
|                 if (data.size() > 0) { | ||||
|                     val manga = json["included"].array[0].obj | ||||
|                     KitsuLibManga(data[0].obj, manga).toTrack() | ||||
|                 } else { | ||||
|                     null | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun getLibManga(track: Track): Observable<Track> { | ||||
|         return rest.getLibManga(track.media_id) | ||||
|                 .map { json -> | ||||
|                     val data = json["data"].array | ||||
|                     if (data.size() > 0) { | ||||
|                         val manga = json["included"].array[0].obj | ||||
|                         KitsuLibManga(data[0].obj, manga).toTrack() | ||||
|                     } else { | ||||
|                         throw Exception("Could not find manga") | ||||
|                     } | ||||
|             .map { json -> | ||||
|                 val data = json["data"].array | ||||
|                 if (data.size() > 0) { | ||||
|                     val manga = json["included"].array[0].obj | ||||
|                     KitsuLibManga(data[0].obj, manga).toTrack() | ||||
|                 } else { | ||||
|                     throw Exception("Could not find manga") | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun login(username: String, password: String): Observable<OAuth> { | ||||
|         return Retrofit.Builder() | ||||
|                 .baseUrl(loginUrl) | ||||
|                 .client(client) | ||||
|                 .addConverterFactory(GsonConverterFactory.create()) | ||||
|                 .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) | ||||
|                 .build() | ||||
|                 .create(LoginRest::class.java) | ||||
|                 .requestAccessToken(username, password) | ||||
|             .baseUrl(loginUrl) | ||||
|             .client(client) | ||||
|             .addConverterFactory(GsonConverterFactory.create()) | ||||
|             .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) | ||||
|             .build() | ||||
|             .create(LoginRest::class.java) | ||||
|             .requestAccessToken(username, password) | ||||
|     } | ||||
|  | ||||
|     fun getCurrentUser(): Observable<String> { | ||||
| @@ -242,12 +242,14 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) | ||||
|             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() | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -30,10 +30,10 @@ class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor { | ||||
|  | ||||
|         // Add the authorization header to the original request. | ||||
|         val authRequest = originalRequest.newBuilder() | ||||
|                 .addHeader("Authorization", "Bearer ${oauth!!.access_token}") | ||||
|                 .header("Accept", "application/vnd.api+json") | ||||
|                 .header("Content-Type", "application/vnd.api+json") | ||||
|                 .build() | ||||
|             .addHeader("Authorization", "Bearer ${oauth!!.access_token}") | ||||
|             .header("Accept", "application/vnd.api+json") | ||||
|             .header("Content-Type", "application/vnd.api+json") | ||||
|             .build() | ||||
|  | ||||
|         return chain.proceed(authRequest) | ||||
|     } | ||||
|   | ||||
| @@ -74,17 +74,17 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun bind(track: Track): Observable<Track> { | ||||
|         return api.findLibManga(track) | ||||
|                 .flatMap { remoteTrack -> | ||||
|                     if (remoteTrack != null) { | ||||
|                         track.copyPersonalFrom(remoteTrack) | ||||
|                         update(track) | ||||
|                     } else { | ||||
|                         // Set default fields if it's not found in the list | ||||
|                         track.score = DEFAULT_SCORE.toFloat() | ||||
|                         track.status = DEFAULT_STATUS | ||||
|                         add(track) | ||||
|                     } | ||||
|             .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 fun search(query: String): Observable<List<TrackSearch>> { | ||||
| @@ -93,21 +93,21 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun refresh(track: Track): Observable<Track> { | ||||
|         return api.getLibManga(track) | ||||
|                 .map { remoteTrack -> | ||||
|                     track.copyPersonalFrom(remoteTrack) | ||||
|                     track.total_chapters = remoteTrack.total_chapters | ||||
|                     track | ||||
|                 } | ||||
|             .map { remoteTrack -> | ||||
|                 track.copyPersonalFrom(remoteTrack) | ||||
|                 track.total_chapters = remoteTrack.total_chapters | ||||
|                 track | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     override fun login(username: String, password: String): Completable { | ||||
|         logout() | ||||
|  | ||||
|         return Observable.fromCallable { api.login(username, password) } | ||||
|                 .doOnNext { csrf -> saveCSRF(csrf) } | ||||
|                 .doOnNext { saveCredentials(username, password) } | ||||
|                 .doOnError { logout() } | ||||
|                 .toCompletable() | ||||
|             .doOnNext { csrf -> saveCSRF(csrf) } | ||||
|             .doOnNext { saveCredentials(username, password) } | ||||
|             .doOnError { logout() } | ||||
|             .toCompletable() | ||||
|     } | ||||
|  | ||||
|     fun refreshLogin() { | ||||
| @@ -141,8 +141,8 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     val isAuthorized: Boolean | ||||
|         get() = super.isLogged && | ||||
|                 getCSRF().isNotEmpty() && | ||||
|                 checkCookies() | ||||
|             getCSRF().isNotEmpty() && | ||||
|             checkCookies() | ||||
|  | ||||
|     fun getCSRF(): String = preferences.trackToken(this).get() | ||||
|  | ||||
| @@ -152,8 +152,9 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { | ||||
|         var ckCount = 0 | ||||
|         val url = BASE_URL.toHttpUrlOrNull()!! | ||||
|         for (ck in networkService.cookieManager.get(url)) { | ||||
|             if (ck.name == USER_SESSION_COOKIE || ck.name == LOGGED_IN_COOKIE) | ||||
|             if (ck.name == USER_SESSION_COOKIE || ck.name == LOGGED_IN_COOKIE) { | ||||
|                 ckCount++ | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return ckCount == 2 | ||||
|   | ||||
| @@ -39,43 +39,45 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|         return if (query.startsWith(PREFIX_MY)) { | ||||
|             val realQuery = query.removePrefix(PREFIX_MY) | ||||
|             getList() | ||||
|                     .flatMap { Observable.from(it) } | ||||
|                     .filter { it.title.contains(realQuery, true) } | ||||
|                     .toList() | ||||
|                 .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)) | ||||
|                 .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" | ||||
|                 } | ||||
|                 .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() | ||||
|                     } | ||||
|                     .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() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun addLibManga(track: Track): Observable<Track> { | ||||
|         return Observable.defer { | ||||
|             authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track))) | ||||
|                     .asObservableSuccess() | ||||
|                     .map { track } | ||||
|                 .asObservableSuccess() | ||||
|                 .map { track } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -95,40 +97,40 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|  | ||||
|             // Update remote | ||||
|             authClient.newCall(POST(url = editPageUrl(track.media_id), body = mangaEditPostBody(editData))) | ||||
|                     .asObservableSuccess() | ||||
|                     .map { | ||||
|                         track | ||||
|                     } | ||||
|                 .asObservableSuccess() | ||||
|                 .map { | ||||
|                     track | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun findLibManga(track: Track): Observable<Track?> { | ||||
|         return authClient.newCall(GET(url = editPageUrl(track.media_id))) | ||||
|                 .asObservable() | ||||
|                 .map { response -> | ||||
|                     var libTrack: Track? = null | ||||
|                     response.use { | ||||
|                         if (it.priorResponse?.isRedirect != true) { | ||||
|                             val trackForm = Jsoup.parse(it.consumeBody()) | ||||
|             .asObservable() | ||||
|             .map { response -> | ||||
|                 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 | ||||
|                                 started_reading_date = trackForm.searchDatePicker("#add_manga_start_date") | ||||
|                                 finished_reading_date = trackForm.searchDatePicker("#add_manga_finish_date") | ||||
|                             } | ||||
|                         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 | ||||
|                             started_reading_date = trackForm.searchDatePicker("#add_manga_start_date") | ||||
|                             finished_reading_date = trackForm.searchDatePicker("#add_manga_finish_date") | ||||
|                         } | ||||
|                     } | ||||
|                     libTrack | ||||
|                 } | ||||
|                 libTrack | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun getLibManga(track: Track): Observable<Track> { | ||||
|         return findLibManga(track) | ||||
|                 .map { it ?: throw Exception("Could not find manga") } | ||||
|             .map { it ?: throw Exception("Could not find manga") } | ||||
|     } | ||||
|  | ||||
|     fun login(username: String, password: String): String { | ||||
| @@ -143,8 +145,8 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|         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) { | ||||
| @@ -157,45 +159,45 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|  | ||||
|     private fun getList(): Observable<List<TrackSearch>> { | ||||
|         return getListUrl() | ||||
|                 .flatMap { url -> | ||||
|                     getListXml(url) | ||||
|             .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) | ||||
|                     started_reading_date = it.searchDateXml("my_start_date") | ||||
|                     finished_reading_date = it.searchDateXml("my_finish_date") | ||||
|                 } | ||||
|                 .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) | ||||
|                         started_reading_date = it.searchDateXml("my_start_date") | ||||
|                         finished_reading_date = it.searchDateXml("my_finish_date") | ||||
|                     } | ||||
|                 } | ||||
|                 .toList() | ||||
|             } | ||||
|             .toList() | ||||
|     } | ||||
|  | ||||
|     private fun getListUrl(): Observable<String> { | ||||
|         return authClient.newCall(POST(url = exportListUrl(), body = exportPostBody())) | ||||
|                 .asObservable() | ||||
|                 .map { response -> | ||||
|                     baseUrl + Jsoup.parse(response.consumeBody()) | ||||
|                             .select("div.goodresult") | ||||
|                             .select("a") | ||||
|                             .attr("href") | ||||
|                 } | ||||
|             .asObservable() | ||||
|             .map { response -> | ||||
|                 baseUrl + Jsoup.parse(response.consumeBody()) | ||||
|                     .select("div.goodresult") | ||||
|                     .select("a") | ||||
|                     .attr("href") | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private fun getListXml(url: String): Observable<Document> { | ||||
|         return authClient.newCall(GET(url)) | ||||
|                 .asObservable() | ||||
|                 .map { response -> | ||||
|                     Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser()) | ||||
|                 } | ||||
|             .asObservable() | ||||
|             .map { response -> | ||||
|                 Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser()) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private fun Response.consumeBody(): String? { | ||||
| @@ -222,28 +224,28 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|         val tables = page.select("form#main-form table") | ||||
|  | ||||
|         return MyAnimeListEditData( | ||||
|                 entry_id = tables[0].select("input[name=entry_id]").`val`(), // Always 0 | ||||
|                 manga_id = tables[0].select("#manga_id").`val`(), | ||||
|                 status = tables[0].select("#add_manga_status > option[selected]").`val`(), | ||||
|                 num_read_volumes = tables[0].select("#add_manga_num_read_volumes").`val`(), | ||||
|                 last_completed_vol = tables[0].select("input[name=last_completed_vol]").`val`(), // Always empty | ||||
|                 num_read_chapters = tables[0].select("#add_manga_num_read_chapters").`val`(), | ||||
|                 score = tables[0].select("#add_manga_score > option[selected]").`val`(), | ||||
|                 start_date_month = tables[0].select("#add_manga_start_date_month > option[selected]").`val`(), | ||||
|                 start_date_day = tables[0].select("#add_manga_start_date_day > option[selected]").`val`(), | ||||
|                 start_date_year = tables[0].select("#add_manga_start_date_year > option[selected]").`val`(), | ||||
|                 finish_date_month = tables[0].select("#add_manga_finish_date_month > option[selected]").`val`(), | ||||
|                 finish_date_day = tables[0].select("#add_manga_finish_date_day > option[selected]").`val`(), | ||||
|                 finish_date_year = tables[0].select("#add_manga_finish_date_year > option[selected]").`val`(), | ||||
|                 tags = tables[1].select("#add_manga_tags").`val`(), | ||||
|                 priority = tables[1].select("#add_manga_priority > option[selected]").`val`(), | ||||
|                 storage_type = tables[1].select("#add_manga_storage_type > option[selected]").`val`(), | ||||
|                 num_retail_volumes = tables[1].select("#add_manga_num_retail_volumes").`val`(), | ||||
|                 num_read_times = tables[1].select("#add_manga_num_read_times").`val`(), | ||||
|                 reread_value = tables[1].select("#add_manga_reread_value > option[selected]").`val`(), | ||||
|                 comments = tables[1].select("#add_manga_comments").`val`(), | ||||
|                 is_asked_to_discuss = tables[1].select("#add_manga_is_asked_to_discuss > option[selected]").`val`(), | ||||
|                 sns_post_type = tables[1].select("#add_manga_sns_post_type > option[selected]").`val`() | ||||
|             entry_id = tables[0].select("input[name=entry_id]").`val`(), // Always 0 | ||||
|             manga_id = tables[0].select("#manga_id").`val`(), | ||||
|             status = tables[0].select("#add_manga_status > option[selected]").`val`(), | ||||
|             num_read_volumes = tables[0].select("#add_manga_num_read_volumes").`val`(), | ||||
|             last_completed_vol = tables[0].select("input[name=last_completed_vol]").`val`(), // Always empty | ||||
|             num_read_chapters = tables[0].select("#add_manga_num_read_chapters").`val`(), | ||||
|             score = tables[0].select("#add_manga_score > option[selected]").`val`(), | ||||
|             start_date_month = tables[0].select("#add_manga_start_date_month > option[selected]").`val`(), | ||||
|             start_date_day = tables[0].select("#add_manga_start_date_day > option[selected]").`val`(), | ||||
|             start_date_year = tables[0].select("#add_manga_start_date_year > option[selected]").`val`(), | ||||
|             finish_date_month = tables[0].select("#add_manga_finish_date_month > option[selected]").`val`(), | ||||
|             finish_date_day = tables[0].select("#add_manga_finish_date_day > option[selected]").`val`(), | ||||
|             finish_date_year = tables[0].select("#add_manga_finish_date_year > option[selected]").`val`(), | ||||
|             tags = tables[1].select("#add_manga_tags").`val`(), | ||||
|             priority = tables[1].select("#add_manga_priority > option[selected]").`val`(), | ||||
|             storage_type = tables[1].select("#add_manga_storage_type > option[selected]").`val`(), | ||||
|             num_retail_volumes = tables[1].select("#add_manga_num_retail_volumes").`val`(), | ||||
|             num_read_times = tables[1].select("#add_manga_num_read_times").`val`(), | ||||
|             reread_value = tables[1].select("#add_manga_reread_value > option[selected]").`val`(), | ||||
|             comments = tables[1].select("#add_manga_comments").`val`(), | ||||
|             is_asked_to_discuss = tables[1].select("#add_manga_is_asked_to_discuss > option[selected]").`val`(), | ||||
|             sns_post_type = tables[1].select("#add_manga_sns_post_type > option[selected]").`val`() | ||||
|         ) | ||||
|     } | ||||
|  | ||||
| @@ -259,98 +261,99 @@ 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 editPageUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon() | ||||
|                 .appendPath(mediaId.toString()) | ||||
|                 .appendPath("edit") | ||||
|                 .toString() | ||||
|             .appendPath(mediaId.toString()) | ||||
|             .appendPath("edit") | ||||
|             .toString() | ||||
|  | ||||
|         private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon() | ||||
|                 .appendPath("add.json") | ||||
|                 .toString() | ||||
|             .appendPath("add.json") | ||||
|             .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()) | ||||
|         } | ||||
|  | ||||
|         private fun mangaEditPostBody(track: MyAnimeListEditData): RequestBody { | ||||
|             return FormBody.Builder() | ||||
|                     .add("entry_id", track.entry_id) | ||||
|                     .add("manga_id", track.manga_id) | ||||
|                     .add("add_manga[status]", track.status) | ||||
|                     .add("add_manga[num_read_volumes]", track.num_read_volumes) | ||||
|                     .add("last_completed_vol", track.last_completed_vol) | ||||
|                     .add("add_manga[num_read_chapters]", track.num_read_chapters) | ||||
|                     .add("add_manga[score]", track.score) | ||||
|                     .add("add_manga[start_date][month]", track.start_date_month) | ||||
|                     .add("add_manga[start_date][day]", track.start_date_day) | ||||
|                     .add("add_manga[start_date][year]", track.start_date_year) | ||||
|                     .add("add_manga[finish_date][month]", track.finish_date_month) | ||||
|                     .add("add_manga[finish_date][day]", track.finish_date_day) | ||||
|                     .add("add_manga[finish_date][year]", track.finish_date_year) | ||||
|                     .add("add_manga[tags]", track.tags) | ||||
|                     .add("add_manga[priority]", track.priority) | ||||
|                     .add("add_manga[storage_type]", track.storage_type) | ||||
|                     .add("add_manga[num_retail_volumes]", track.num_retail_volumes) | ||||
|                     .add("add_manga[num_read_times]", track.num_read_chapters) | ||||
|                     .add("add_manga[reread_value]", track.reread_value) | ||||
|                     .add("add_manga[comments]", track.comments) | ||||
|                     .add("add_manga[is_asked_to_discuss]", track.is_asked_to_discuss) | ||||
|                     .add("add_manga[sns_post_type]", track.sns_post_type) | ||||
|                     .add("submitIt", track.submitIt) | ||||
|                     .build() | ||||
|                 .add("entry_id", track.entry_id) | ||||
|                 .add("manga_id", track.manga_id) | ||||
|                 .add("add_manga[status]", track.status) | ||||
|                 .add("add_manga[num_read_volumes]", track.num_read_volumes) | ||||
|                 .add("last_completed_vol", track.last_completed_vol) | ||||
|                 .add("add_manga[num_read_chapters]", track.num_read_chapters) | ||||
|                 .add("add_manga[score]", track.score) | ||||
|                 .add("add_manga[start_date][month]", track.start_date_month) | ||||
|                 .add("add_manga[start_date][day]", track.start_date_day) | ||||
|                 .add("add_manga[start_date][year]", track.start_date_year) | ||||
|                 .add("add_manga[finish_date][month]", track.finish_date_month) | ||||
|                 .add("add_manga[finish_date][day]", track.finish_date_day) | ||||
|                 .add("add_manga[finish_date][year]", track.finish_date_year) | ||||
|                 .add("add_manga[tags]", track.tags) | ||||
|                 .add("add_manga[priority]", track.priority) | ||||
|                 .add("add_manga[storage_type]", track.storage_type) | ||||
|                 .add("add_manga[num_retail_volumes]", track.num_retail_volumes) | ||||
|                 .add("add_manga[num_read_times]", track.num_read_chapters) | ||||
|                 .add("add_manga[reread_value]", track.reread_value) | ||||
|                 .add("add_manga[comments]", track.comments) | ||||
|                 .add("add_manga[is_asked_to_discuss]", track.is_asked_to_discuss) | ||||
|                 .add("add_manga[sns_post_type]", track.sns_post_type) | ||||
|                 .add("submitIt", track.submitIt) | ||||
|                 .build() | ||||
|         } | ||||
|  | ||||
|         private fun Element.searchDateXml(field: String): Long { | ||||
|             val text = selectText(field, "0000-00-00")!! | ||||
|             // MAL sets the data to 0000-00-00 when date is invalid or missing | ||||
|             if (text == "0000-00-00") | ||||
|             if (text == "0000-00-00") { | ||||
|                 return 0L | ||||
|             } | ||||
|  | ||||
|             return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(text)?.time ?: 0L | ||||
|         } | ||||
| @@ -359,8 +362,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|             val month = select(id + "_month > option[selected]").`val`().toIntOrNull() | ||||
|             val day = select(id + "_day > option[selected]").`val`().toIntOrNull() | ||||
|             val year = select(id + "_year > option[selected]").`val`().toIntOrNull() | ||||
|             if (year == null || month == null || day == null) | ||||
|             if (year == null || month == null || day == null) { | ||||
|                 return 0L | ||||
|             } | ||||
|  | ||||
|             return GregorianCalendar(year, month - 1, day).timeInMillis | ||||
|         } | ||||
| @@ -370,18 +374,18 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|         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" | ||||
|  | ||||
| @@ -472,8 +476,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|         fun copyPersonalFrom(track: Track) { | ||||
|             num_read_chapters = track.last_chapter_read.toString() | ||||
|             val numScore = track.score.toInt() | ||||
|             if (numScore in 1..9) | ||||
|             if (numScore in 1..9) { | ||||
|                 score = numScore.toString() | ||||
|             } | ||||
|             status = track.status.toString() | ||||
|             if (track.started_reading_date == 0L) { | ||||
|                 start_date_month = "" | ||||
|   | ||||
| @@ -53,7 +53,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor | ||||
|     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()) | ||||
|     } | ||||
|   | ||||
| @@ -51,18 +51,18 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun bind(track: Track): Observable<Track> { | ||||
|         return api.findLibManga(track, getUsername()) | ||||
|                 .flatMap { remoteTrack -> | ||||
|                     if (remoteTrack != null) { | ||||
|                         track.copyPersonalFrom(remoteTrack) | ||||
|                         track.library_id = remoteTrack.library_id | ||||
|                         update(track) | ||||
|                     } else { | ||||
|                         // Set default fields if it's not found in the list | ||||
|                         track.score = DEFAULT_SCORE.toFloat() | ||||
|                         track.status = DEFAULT_STATUS | ||||
|                         add(track) | ||||
|                     } | ||||
|             .flatMap { remoteTrack -> | ||||
|                 if (remoteTrack != null) { | ||||
|                     track.copyPersonalFrom(remoteTrack) | ||||
|                     track.library_id = remoteTrack.library_id | ||||
|                     update(track) | ||||
|                 } else { | ||||
|                     // Set default fields if it's not found in the list | ||||
|                     track.score = DEFAULT_SCORE.toFloat() | ||||
|                     track.status = DEFAULT_STATUS | ||||
|                     add(track) | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     override fun search(query: String): Observable<List<TrackSearch>> { | ||||
| @@ -71,13 +71,13 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun refresh(track: Track): Observable<Track> { | ||||
|         return api.findLibManga(track, getUsername()) | ||||
|                 .map { remoteTrack -> | ||||
|                     if (remoteTrack != null) { | ||||
|                         track.copyPersonalFrom(remoteTrack) | ||||
|                         track.total_chapters = remoteTrack.total_chapters | ||||
|                     } | ||||
|                     track | ||||
|             .map { remoteTrack -> | ||||
|                 if (remoteTrack != null) { | ||||
|                     track.copyPersonalFrom(remoteTrack) | ||||
|                     track.total_chapters = remoteTrack.total_chapters | ||||
|                 } | ||||
|                 track | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     override fun getLogo() = R.drawable.ic_tracker_shikimori | ||||
|   | ||||
| @@ -30,49 +30,49 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter | ||||
|  | ||||
|     fun addLibManga(track: Track, user_id: String): Observable<Track> { | ||||
|         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_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() | ||||
|             ) | ||||
|         ) | ||||
|         val body = payload.toString().toRequestBody(jsonime) | ||||
|         val request = Request.Builder() | ||||
|                 .url("$apiUrl/v2/user_rates") | ||||
|                 .post(body) | ||||
|                 .build() | ||||
|             .url("$apiUrl/v2/user_rates") | ||||
|             .post(body) | ||||
|             .build() | ||||
|         return authClient.newCall(request) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { | ||||
|                     track | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { | ||||
|                 track | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id) | ||||
|  | ||||
|     fun search(search: String): Observable<List<TrackSearch>> { | ||||
|         val url = Uri.parse("$apiUrl/mangas").buildUpon() | ||||
|                 .appendQueryParameter("order", "popularity") | ||||
|                 .appendQueryParameter("search", search) | ||||
|                 .appendQueryParameter("limit", "20") | ||||
|                 .build() | ||||
|             .appendQueryParameter("order", "popularity") | ||||
|             .appendQueryParameter("search", search) | ||||
|             .appendQueryParameter("limit", "20") | ||||
|             .build() | ||||
|         val request = Request.Builder() | ||||
|                 .url(url.toString()) | ||||
|                 .get() | ||||
|                 .build() | ||||
|             .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 = JsonParser.parseString(responseBody).array | ||||
|                     response.map { jsonToSearch(it.obj) } | ||||
|             .asObservableSuccess() | ||||
|             .map { netResponse -> | ||||
|                 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 { | ||||
| @@ -103,45 +103,45 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter | ||||
|  | ||||
|     fun findLibManga(track: Track, user_id: String): Observable<Track?> { | ||||
|         val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon() | ||||
|                 .appendQueryParameter("user_id", user_id) | ||||
|                 .appendQueryParameter("target_id", track.media_id.toString()) | ||||
|                 .appendQueryParameter("target_type", "Manga") | ||||
|                 .build() | ||||
|             .appendQueryParameter("user_id", user_id) | ||||
|             .appendQueryParameter("target_id", track.media_id.toString()) | ||||
|             .appendQueryParameter("target_type", "Manga") | ||||
|             .build() | ||||
|         val request = Request.Builder() | ||||
|                 .url(url.toString()) | ||||
|                 .get() | ||||
|                 .build() | ||||
|             .url(url.toString()) | ||||
|             .get() | ||||
|             .build() | ||||
|  | ||||
|         val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon() | ||||
|                 .appendPath(track.media_id.toString()) | ||||
|                 .build() | ||||
|             .appendPath(track.media_id.toString()) | ||||
|             .build() | ||||
|         val requestMangas = Request.Builder() | ||||
|                 .url(urlMangas.toString()) | ||||
|                 .get() | ||||
|                 .build() | ||||
|             .url(urlMangas.toString()) | ||||
|             .get() | ||||
|             .build() | ||||
|         return authClient.newCall(requestMangas) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { netResponse -> | ||||
|                     val responseBody = netResponse.body?.string().orEmpty() | ||||
|                     JsonParser.parseString(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 = JsonParser.parseString(responseBody).array | ||||
|                                 if (response.size() > 1) { | ||||
|                                     throw Exception("Too much mangas in response") | ||||
|                                 } | ||||
|                                 val entry = response.map { | ||||
|                                     jsonToTrack(it.obj, mangas) | ||||
|                                 } | ||||
|                                 entry.firstOrNull() | ||||
|                             } | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { netResponse -> | ||||
|                 val responseBody = netResponse.body?.string().orEmpty() | ||||
|                 JsonParser.parseString(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 = JsonParser.parseString(responseBody).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 { | ||||
| @@ -159,14 +159,15 @@ 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 { | ||||
| @@ -186,18 +187,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() | ||||
|             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()) | ||||
|         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() | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -29,9 +29,9 @@ class ShikimoriInterceptor(val shikimori: Shikimori, val gson: Gson) : Intercept | ||||
|         } | ||||
|         // Add the authorization header to the original request. | ||||
|         val authRequest = originalRequest.newBuilder() | ||||
|                 .addHeader("Authorization", "Bearer ${oauth!!.access_token}") | ||||
|                 .header("User-Agent", "Tachiyomi") | ||||
|                 .build() | ||||
|             .addHeader("Authorization", "Bearer ${oauth!!.access_token}") | ||||
|             .header("User-Agent", "Tachiyomi") | ||||
|             .build() | ||||
|  | ||||
|         return chain.proceed(authRequest) | ||||
|     } | ||||
|   | ||||
| @@ -18,7 +18,7 @@ import java.util.concurrent.TimeUnit | ||||
| import kotlinx.coroutines.runBlocking | ||||
|  | ||||
| class UpdaterJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|         Worker(context, workerParams) { | ||||
|     Worker(context, workerParams) { | ||||
|  | ||||
|     override fun doWork(): Result { | ||||
|         return runBlocking { | ||||
| @@ -37,9 +37,11 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|                         setContentText(context.getString(R.string.update_check_notification_update_available)) | ||||
|                         setSmallIcon(android.R.drawable.stat_sys_download_done) | ||||
|                         // Download action | ||||
|                         addAction(android.R.drawable.stat_sys_download_done, | ||||
|                                 context.getString(R.string.action_download), | ||||
|                                 PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)) | ||||
|                         addAction( | ||||
|                             android.R.drawable.stat_sys_download_done, | ||||
|                             context.getString(R.string.action_download), | ||||
|                             PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|                 Result.success() | ||||
| @@ -59,15 +61,16 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|  | ||||
|         fun setupTask(context: Context) { | ||||
|             val constraints = Constraints.Builder() | ||||
|                     .setRequiredNetworkType(NetworkType.CONNECTED) | ||||
|                     .build() | ||||
|                 .setRequiredNetworkType(NetworkType.CONNECTED) | ||||
|                 .build() | ||||
|  | ||||
|             val request = PeriodicWorkRequestBuilder<UpdaterJob>( | ||||
|                     3, TimeUnit.DAYS, | ||||
|                     3, TimeUnit.HOURS) | ||||
|                     .addTag(TAG) | ||||
|                     .setConstraints(constraints) | ||||
|                     .build() | ||||
|                 3, TimeUnit.DAYS, | ||||
|                 3, TimeUnit.HOURS | ||||
|             ) | ||||
|                 .addTag(TAG) | ||||
|                 .setConstraints(constraints) | ||||
|                 .build() | ||||
|  | ||||
|             WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request) | ||||
|         } | ||||
|   | ||||
| @@ -69,13 +69,17 @@ internal class UpdaterNotifier(private val context: Context) { | ||||
|             setProgress(0, 0, false) | ||||
|             // Install action | ||||
|             setContentIntent(NotificationHandler.installApkPendingActivity(context, uri)) | ||||
|             addAction(R.drawable.ic_system_update_alt_white_24dp, | ||||
|                     context.getString(R.string.action_install), | ||||
|                     NotificationHandler.installApkPendingActivity(context, uri)) | ||||
|             addAction( | ||||
|                 R.drawable.ic_system_update_alt_white_24dp, | ||||
|                 context.getString(R.string.action_install), | ||||
|                 NotificationHandler.installApkPendingActivity(context, uri) | ||||
|             ) | ||||
|             // Cancel action | ||||
|             addAction(R.drawable.ic_close_24dp, | ||||
|                     context.getString(R.string.action_cancel), | ||||
|                     NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)) | ||||
|             addAction( | ||||
|                 R.drawable.ic_close_24dp, | ||||
|                 context.getString(R.string.action_cancel), | ||||
|                 NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER) | ||||
|             ) | ||||
|         } | ||||
|         notificationBuilder.show() | ||||
|     } | ||||
| @@ -92,13 +96,17 @@ internal class UpdaterNotifier(private val context: Context) { | ||||
|             setOnlyAlertOnce(false) | ||||
|             setProgress(0, 0, false) | ||||
|             // Retry action | ||||
|             addAction(R.drawable.ic_refresh_24dp, | ||||
|                     context.getString(R.string.action_retry), | ||||
|                     UpdaterService.downloadApkPendingService(context, url)) | ||||
|             addAction( | ||||
|                 R.drawable.ic_refresh_24dp, | ||||
|                 context.getString(R.string.action_retry), | ||||
|                 UpdaterService.downloadApkPendingService(context, url) | ||||
|             ) | ||||
|             // Cancel action | ||||
|             addAction(R.drawable.ic_close_24dp, | ||||
|                     context.getString(R.string.action_cancel), | ||||
|                     NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)) | ||||
|             addAction( | ||||
|                 R.drawable.ic_close_24dp, | ||||
|                 context.getString(R.string.action_cancel), | ||||
|                 NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER) | ||||
|             ) | ||||
|         } | ||||
|         notificationBuilder.show(Notifications.ID_UPDATER) | ||||
|     } | ||||
|   | ||||
| @@ -16,8 +16,8 @@ class DevRepoUpdateChecker : UpdateChecker() { | ||||
|  | ||||
|     private val client: OkHttpClient by lazy { | ||||
|         Injekt.get<NetworkHelper>().client.newBuilder() | ||||
|                 .followRedirects(false) | ||||
|                 .build() | ||||
|             .followRedirects(false) | ||||
|             .build() | ||||
|     } | ||||
|  | ||||
|     private val versionRegex: Regex by lazy { | ||||
|   | ||||
| @@ -15,10 +15,10 @@ interface GithubService { | ||||
|     companion object { | ||||
|         fun create(): GithubService { | ||||
|             val restAdapter = Retrofit.Builder() | ||||
|                     .baseUrl("https://api.github.com") | ||||
|                     .addConverterFactory(GsonConverterFactory.create()) | ||||
|                     .client(Injekt.get<NetworkHelper>().client) | ||||
|                     .build() | ||||
|                 .baseUrl("https://api.github.com") | ||||
|                 .addConverterFactory(GsonConverterFactory.create()) | ||||
|                 .client(Injekt.get<NetworkHelper>().client) | ||||
|                 .build() | ||||
|  | ||||
|             return restAdapter.create(GithubService::class.java) | ||||
|         } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user