From 45a50a7e918e61e8540ebb43bf34fa45afd27bb3 Mon Sep 17 00:00:00 2001 From: jmir1 Date: Wed, 30 Jun 2021 16:47:13 +0200 Subject: [PATCH] fix anime backups closes #30 --- .../data/backup/AbstractBackupManager.kt | 10 +- .../data/backup/AbstractBackupRestore.kt | 53 +++- .../backup/AbstractBackupRestoreValidator.kt | 2 + .../data/backup/BackupCreateService.kt | 4 - .../data/backup/full/FullBackupManager.kt | 284 +++++++++++++++++- .../data/backup/full/FullBackupRestore.kt | 138 ++++++++- .../backup/full/FullBackupRestoreValidator.kt | 12 +- .../data/backup/full/models/Backup.kt | 3 + .../data/backup/full/models/BackupAnime.kt | 87 ++++++ .../backup/full/models/BackupAnimeHistory.kt | 10 + .../backup/full/models/BackupAnimeSource.kt | 20 ++ .../backup/full/models/BackupAnimeTracking.kt | 65 ++++ .../data/backup/full/models/BackupEpisode.kt | 56 ++++ .../data/backup/full/models/BackupFull.kt | 2 +- 14 files changed, 709 insertions(+), 37 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupAnime.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupAnimeHistory.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupAnimeSource.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupAnimeTracking.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupEpisode.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt index 2e13d280d..b59f55eb8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup import android.content.Context import android.net.Uri import eu.kanade.tachiyomi.animesource.AnimeSource +import eu.kanade.tachiyomi.animesource.AnimeSourceManager import eu.kanade.tachiyomi.animesource.model.toSEpisode import eu.kanade.tachiyomi.data.database.AnimeDatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper @@ -26,6 +27,7 @@ abstract class AbstractBackupManager(protected val context: Context) { internal val databaseHelper: DatabaseHelper by injectLazy() internal val animedatabaseHelper: AnimeDatabaseHelper by injectLazy() internal val sourceManager: SourceManager by injectLazy() + internal val animesourceManager: AnimeSourceManager by injectLazy() internal val trackManager: TrackManager by injectLazy() protected val preferences: PreferencesHelper by injectLazy() @@ -70,8 +72,8 @@ abstract class AbstractBackupManager(protected val context: Context) { * Fetches chapter information. * * @param source source of manga - * @param manga manga that needs updating - * @param chapters list of chapters in the backup + * @param anime anime that needs updating + * @param episodes list of episodes in the backup * @return Updated manga chapters. */ internal suspend fun restoreEpisodes(source: AnimeSource, anime: Anime, episodes: List): Pair, List> { @@ -94,9 +96,9 @@ abstract class AbstractBackupManager(protected val context: Context) { databaseHelper.getFavoriteMangas().executeAsBlocking() /** - * Returns list containing manga from library + * Returns list containing anime from library * - * @return [Manga] from library + * @return [Anime] from library */ protected fun getFavoriteAnime(): List = animedatabaseHelper.getFavoriteAnimes().executeAsBlocking() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupRestore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupRestore.kt index 10fa5bd31..582df3101 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupRestore.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupRestore.kt @@ -3,8 +3,13 @@ package eu.kanade.tachiyomi.data.backup import android.content.Context import android.net.Uri import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.animesource.AnimeSource +import eu.kanade.tachiyomi.data.database.AnimeDatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Anime +import eu.kanade.tachiyomi.data.database.models.AnimeTrack import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Episode import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.TrackManager @@ -21,6 +26,7 @@ import java.util.Locale abstract class AbstractBackupRestore(protected val context: Context, protected val notifier: BackupNotifier) { protected val db: DatabaseHelper by injectLazy() + protected val animedb: AnimeDatabaseHelper by injectLazy() protected val trackManager: TrackManager by injectLazy() var job: Job? = null @@ -79,6 +85,28 @@ abstract class AbstractBackupRestore(protected val co } } + /** + * Fetches chapter information. + * + * @param source source of manga + * @param anime manga that needs updating + * @return Updated manga chapters. + */ + internal suspend fun updateEpisodes(source: AnimeSource, anime: Anime, episodes: List): Pair, List> { + return try { + backupManager.restoreEpisodes(source, anime, episodes) + } catch (e: Exception) { + // If there's any error, return empty update and continue. + val errorMessage = if (e is NoChaptersException) { + context.getString(R.string.no_chapters_error) + } else { + e.message + } + errors.add(Date() to "${anime.title} - $errorMessage") + Pair(emptyList(), emptyList()) + } + } + /** * Refreshes tracking information. * @@ -102,6 +130,29 @@ abstract class AbstractBackupRestore(protected val co } } + /** + * Refreshes tracking information. + * + * @param anime manga that needs updating. + * @param tracks list containing tracks from restore file. + */ + internal suspend fun updateAnimeTracking(anime: Anime, tracks: List) { + tracks.forEach { track -> + val service = trackManager.getService(track.sync_id) + if (service != null && service.isLogged) { + try { + val updatedTrack = service.refresh(track) + animedb.insertTrack(updatedTrack).executeAsBlocking() + } catch (e: Exception) { + errors.add(Date() to "${anime.title} - ${e.message}") + } + } else { + val serviceName = service?.nameRes()?.let { context.getString(it) } + errors.add(Date() to "${anime.title} - ${context.getString(R.string.tracker_not_logged_in, serviceName)}") + } + } + } + /** * Called to update dialog in [BackupConst] * @@ -120,7 +171,7 @@ abstract class AbstractBackupRestore(protected val co internal fun writeErrorLog(): File { try { if (errors.isNotEmpty()) { - val file = context.createFileInCacheDir("tachiyomi_restore.txt") + val file = context.createFileInCacheDir("aniyomi_restore.txt") val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) file.bufferedWriter().use { out -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupRestoreValidator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupRestoreValidator.kt index 2dc959691..04903a5fe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupRestoreValidator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupRestoreValidator.kt @@ -2,12 +2,14 @@ package eu.kanade.tachiyomi.data.backup import android.content.Context import android.net.Uri +import eu.kanade.tachiyomi.animesource.AnimeSourceManager import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.SourceManager import uy.kohesive.injekt.injectLazy abstract class AbstractBackupRestoreValidator { protected val sourceManager: SourceManager by injectLazy() + protected val animesourceManager: AnimeSourceManager by injectLazy() protected val trackManager: TrackManager by injectLazy() abstract fun validate(context: Context, uri: Uri): Results diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateService.kt index 67f1ed85c..9a7dffcae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateService.kt @@ -22,13 +22,9 @@ class BackupCreateService : Service() { companion object { // Filter options internal const val BACKUP_CATEGORY = 0x1 - internal const val BACKUP_ANIMECATEGORY = 0x1 internal const val BACKUP_CATEGORY_MASK = 0x1 - internal const val BACKUP_ANIMECATEGORY_MASK = 0x1 internal const val BACKUP_CHAPTER = 0x2 - internal const val BACKUP_EPISODE = 0x2 internal const val BACKUP_CHAPTER_MASK = 0x2 - internal const val BACKUP_EPISODE_MASK = 0x2 internal const val BACKUP_HISTORY = 0x4 internal const val BACKUP_HISTORY_MASK = 0x4 internal const val BACKUP_TRACK = 0x8 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt index 61a244857..5e69cce56 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt @@ -12,20 +12,8 @@ import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HIST import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK -import eu.kanade.tachiyomi.data.backup.full.models.Backup -import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory -import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter -import eu.kanade.tachiyomi.data.backup.full.models.BackupFull -import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory -import eu.kanade.tachiyomi.data.backup.full.models.BackupManga -import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer -import eu.kanade.tachiyomi.data.backup.full.models.BackupSource -import eu.kanade.tachiyomi.data.backup.full.models.BackupTracking -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.History -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaCategory -import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.backup.full.models.* +import eu.kanade.tachiyomi.data.database.models.* import kotlinx.serialization.protobuf.ProtoBuf import okio.buffer import okio.gzip @@ -49,11 +37,15 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { databaseHelper.inTransaction { val databaseManga = getFavoriteManga() + val databaseAnime = getFavoriteAnime() backup = Backup( backupManga(databaseManga, flags), backupCategories(), - backupExtensionInfo(databaseManga) + backupAnime(databaseAnime, flags), + backupCategoriesAnime(), + backupExtensionInfo(databaseManga), + backupAnimeExtensionInfo(databaseAnime) ) } @@ -66,7 +58,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { // Delete older backups val numberOfBackups = numberOfBackups() - val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.proto.gz""") + val backupRegex = Regex("""aniyomi_\d+-\d+-\d+_\d+-\d+.proto.gz""") dir.listFiles { _, filename -> backupRegex.matches(filename) } .orEmpty() .sortedByDescending { it.name } @@ -96,6 +88,12 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { } } + private fun backupAnime(animes: List, flags: Int): List { + return animes.map { + backupAnimeObject(it, flags) + } + } + private fun backupExtensionInfo(mangas: List): List { return mangas .asSequence() @@ -106,6 +104,16 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { .toList() } + private fun backupAnimeExtensionInfo(animes: List): List { + return animes + .asSequence() + .map { it.source } + .distinct() + .map { animesourceManager.getOrStub(it) } + .map { BackupAnimeSource.copyFrom(it) } + .toList() + } + /** * Backup the categories of library * @@ -117,6 +125,17 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { .map { BackupCategory.copyFrom(it) } } + /** + * Backup the categories of library + * + * @return list of [BackupCategory] to be backed up + */ + private fun backupCategoriesAnime(): List { + return animedatabaseHelper.getCategories() + .executeAsBlocking() + .map { BackupCategory.copyFrom(it) } + } + /** * Convert a manga to Json * @@ -171,6 +190,60 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { return mangaObject } + /** + * Convert a manga to Json + * + * @param anime manga that gets converted + * @param options options for the backup + * @return [BackupAnime] containing anime in a serializable form + */ + private fun backupAnimeObject(anime: Anime, options: Int): BackupAnime { + // Entry for this manga + val animeObject = BackupAnime.copyFrom(anime) + + // Check if user wants chapter information in backup + if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) { + // Backup all the chapters + val episodes = animedatabaseHelper.getEpisodes(anime).executeAsBlocking() + if (episodes.isNotEmpty()) { + animeObject.episodes = episodes.map { BackupEpisode.copyFrom(it) } + } + } + + // Check if user wants category information in backup + if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { + // Backup categories for this manga + val categoriesForAnime = animedatabaseHelper.getCategoriesForAnime(anime).executeAsBlocking() + if (categoriesForAnime.isNotEmpty()) { + animeObject.categories = categoriesForAnime.mapNotNull { it.order } + } + } + + // Check if user wants track information in backup + if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) { + val tracks = animedatabaseHelper.getTracks(anime).executeAsBlocking() + if (tracks.isNotEmpty()) { + animeObject.tracking = tracks.map { BackupAnimeTracking.copyFrom(it) } + } + } + + // Check if user wants history information in backup + if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) { + val historyForAnime = animedatabaseHelper.getHistoryByAnimeId(anime.id!!).executeAsBlocking() + if (historyForAnime.isNotEmpty()) { + val history = historyForAnime.mapNotNull { history -> + val url = animedatabaseHelper.getEpisode(history.episode_id).executeAsBlocking()?.url + url?.let { BackupAnimeHistory(url, history.last_seen) } + } + if (history.isNotEmpty()) { + animeObject.history = history + } + } + } + + return animeObject + } + fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) { manga.id = dbManga.id manga.copyFrom(dbManga) @@ -190,6 +263,25 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { } } + fun restoreAnimeNoFetch(anime: Anime, dbAnime: Anime) { + anime.id = dbAnime.id + anime.copyFrom(dbAnime) + insertAnime(anime) + } + + /** + * Fetches anime information + * + * @param anime anime that needs updating + * @return Updated anime info. + */ + fun restoreAnime(anime: Anime): Anime { + return anime.also { + it.initialized = it.description != null + it.id = insertAnime(it) + } + } + /** * Restore the categories from Json * @@ -223,6 +315,39 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { } } + /** + * Restore the categories from Json + * + * @param backupCategories list containing categories + */ + internal fun restoreCategoriesAnime(backupCategories: List) { + // Get categories from file and from db + val dbCategories = animedatabaseHelper.getCategories().executeAsBlocking() + + // Iterate over them + backupCategories.map { it.getCategoryImpl() }.forEach { category -> + // Used to know if the category is already in the db + var found = false + for (dbCategory in dbCategories) { + // If the category is already in the db, assign the id to the file's category + // and do nothing + if (category.name == dbCategory.name) { + category.id = dbCategory.id + found = true + break + } + } + // If the category isn't in the db, remove the id and insert a new category + // Store the inserted id in the category + if (!found) { + // Let the db assign the id + category.id = null + val result = animedatabaseHelper.insertCategory(category).executeAsBlocking() + category.id = result.insertedId()?.toInt() + } + } + } + /** * Restores the categories a manga is in. * @@ -251,6 +376,34 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { } } + /** + * Restores the categories an anime is in. + * + * @param anime the anime whose categories have to be restored. + * @param categories the categories to restore. + */ + internal fun restoreCategoriesForAnime(anime: Anime, categories: List, backupCategories: List) { + val dbCategories = animedatabaseHelper.getCategories().executeAsBlocking() + val animeCategoriesToUpdate = ArrayList(categories.size) + categories.forEach { backupCategoryOrder -> + backupCategories.firstOrNull { + it.order == backupCategoryOrder + }?.let { backupCategory -> + dbCategories.firstOrNull { dbCategory -> + dbCategory.name == backupCategory.name + }?.let { dbCategory -> + animeCategoriesToUpdate += AnimeCategory.create(anime, dbCategory) + } + } + } + + // Update database + if (animeCategoriesToUpdate.isNotEmpty()) { + animedatabaseHelper.deleteOldAnimesCategories(listOf(anime)).executeAsBlocking() + animedatabaseHelper.insertAnimesCategories(animeCategoriesToUpdate).executeAsBlocking() + } + } + /** * Restore history from Json * @@ -280,6 +433,35 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking() } + /** + * Restore history from Json + * + * @param history list containing history to be restored + */ + internal fun restoreHistoryForAnime(history: List) { + // List containing history to be updated + val historyToBeUpdated = ArrayList(history.size) + for ((url, lastSeen) in history) { + val dbHistory = animedatabaseHelper.getHistoryByEpisodeUrl(url).executeAsBlocking() + // Check if history already in database and update + if (dbHistory != null) { + dbHistory.apply { + last_seen = max(lastSeen, dbHistory.last_seen) + } + historyToBeUpdated.add(dbHistory) + } else { + // If not in database create + animedatabaseHelper.getEpisode(url).executeAsBlocking()?.let { + val historyToAdd = AnimeHistory.create(it).apply { + last_seen = lastSeen + } + historyToBeUpdated.add(historyToAdd) + } + } + } + animedatabaseHelper.updateAnimeHistoryLastSeen(historyToBeUpdated).executeAsBlocking() + } + /** * Restores the sync of a manga. * @@ -323,6 +505,49 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { } } + /** + * Restores the sync of a manga. + * + * @param anime the anime whose sync have to be restored. + * @param tracks the track list to restore. + */ + internal fun restoreTrackForAnime(anime: Anime, tracks: List) { + // Fix foreign keys with the current anime id + tracks.map { it.anime_id = anime.id!! } + + // Get tracks from database + val dbTracks = animedatabaseHelper.getTracks(anime).executeAsBlocking() + val trackToUpdate = mutableListOf() + + tracks.forEach { track -> + var isInDatabase = false + for (dbTrack in dbTracks) { + if (track.sync_id == dbTrack.sync_id) { + // The sync is already in the db, only update its fields + if (track.media_id != dbTrack.media_id) { + dbTrack.media_id = track.media_id + } + if (track.library_id != dbTrack.library_id) { + dbTrack.library_id = track.library_id + } + dbTrack.last_episode_seen = max(dbTrack.last_episode_seen, track.last_episode_seen) + isInDatabase = true + trackToUpdate.add(dbTrack) + break + } + } + if (!isInDatabase) { + // Insert new sync. Let the db assign the id + track.id = null + trackToUpdate.add(track) + } + } + // Update database + if (trackToUpdate.isNotEmpty()) { + animedatabaseHelper.insertTracks(trackToUpdate).executeAsBlocking() + } + } + internal fun restoreChaptersForManga(manga: Manga, chapters: List) { val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking() @@ -349,4 +574,31 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { newChapters[true]?.let { updateKnownChapters(it) } newChapters[false]?.let { insertChapters(it) } } + + internal fun restoreEpisodesForAnime(anime: Anime, episodes: List) { + val dbEpisodes = animedatabaseHelper.getEpisodes(anime).executeAsBlocking() + + episodes.forEach { episode -> + val dbEpisode = dbEpisodes.find { it.url == episode.url } + if (dbEpisode != null) { + episode.id = dbEpisode.id + episode.copyFrom(dbEpisode) + if (dbEpisode.seen && !episode.seen) { + episode.seen = dbEpisode.seen + episode.last_second_seen = dbEpisode.last_second_seen + } else if (episode.last_second_seen == 0L && dbEpisode.last_second_seen != 0L) { + episode.last_second_seen = dbEpisode.last_second_seen + } + if (!episode.bookmark && dbEpisode.bookmark) { + episode.bookmark = dbEpisode.bookmark + } + } + + episode.anime_id = anime.id + } + + val newEpisodes = episodes.groupBy { it.id != null } + newEpisodes[true]?.let { updateKnownEpisodes(it) } + newEpisodes[false]?.let { insertEpisodes(it) } + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestore.kt index 8e0dd5408..72b494a36 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestore.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestore.kt @@ -5,13 +5,8 @@ import android.net.Uri import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore import eu.kanade.tachiyomi.data.backup.BackupNotifier -import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory -import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory -import eu.kanade.tachiyomi.data.backup.full.models.BackupManga -import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.backup.full.models.* +import eu.kanade.tachiyomi.data.database.models.* import okio.buffer import okio.gzip import okio.source @@ -25,15 +20,21 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() } val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString) - restoreAmount = backup.backupManga.size + 1 // +1 for categories + restoreAmount = backup.backupManga.size + backup.backupAnime.size + 2 // +2 for categories // Restore categories if (backup.backupCategories.isNotEmpty()) { restoreCategories(backup.backupCategories) } + // Restore categories + if (backup.backupCategoriesAnime.isNotEmpty()) { + restoreCategoriesAnime(backup.backupCategoriesAnime) + } + // Store source mapping for error messages - sourceMapping = backup.backupSources.map { it.sourceId to it.name }.toMap() + sourceMapping = backup.backupSources.map { it.sourceId to it.name }.toMap() + + backup.backupAnimeSources.map { it.sourceId to it.name }.toMap() // Restore individual manga backup.backupManga.forEach { @@ -44,6 +45,15 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa restoreManga(it, backup.backupCategories) } + // Restore individual anime + backup.backupAnime.forEach { + if (job?.isActive != true) { + return false + } + + restoreAnime(it, backup.backupCategoriesAnime) + } + // TODO: optionally trigger online library + tracker update return true @@ -58,6 +68,15 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories)) } + private fun restoreCategoriesAnime(backupCategories: List) { + animedb.inTransaction { + backupManager.restoreCategoriesAnime(backupCategories) + } + + restoreProgress += 1 + showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories)) + } + private fun restoreManga(backupManga: BackupManga, backupCategories: List) { val manga = backupManga.getMangaImpl() val chapters = backupManga.getChaptersImpl() @@ -76,6 +95,24 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa showRestoreProgress(restoreProgress, restoreAmount, manga.title) } + private fun restoreAnime(backupAnime: BackupAnime, backupCategories: List) { + val anime = backupAnime.getAnimeImpl() + val episodes = backupAnime.getEpisodesImpl() + val categories = backupAnime.categories + val history = backupAnime.history + val tracks = backupAnime.getTrackingImpl() + + try { + restoreAnimeData(anime, episodes, categories, history, tracks, backupCategories) + } catch (e: Exception) { + val sourceName = sourceMapping[anime.source] ?: anime.source.toString() + errors.add(Date() to "${anime.title} [$sourceName]: ${e.message}") + } + + restoreProgress += 1 + showRestoreProgress(restoreProgress, restoreAmount, anime.title) + } + /** * Returns a manga restore observable * @@ -108,6 +145,38 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa } } + /** + * Returns a manga restore observable + * + * @param manga manga data from json + * @param chapters chapters data from json + * @param categories categories data from json + * @param history history data from json + * @param tracks tracking data from json + */ + private fun restoreAnimeData( + anime: Anime, + episodes: List, + categories: List, + history: List, + tracks: List, + backupCategories: List + ) { + animedb.inTransaction { + val dbAnime = backupManager.getAnimeFromDatabase(anime) + if (dbAnime == null) { + // Manga not in database + restoreAnimeFetch(anime, episodes, categories, history, tracks, backupCategories) + } else { + // Manga in database + // Copy information from manga already in database + backupManager.restoreAnimeNoFetch(anime, dbAnime) + // Fetch rest of manga information + restoreAnimeNoFetch(anime, episodes, categories, history, tracks, backupCategories) + } + } + } + /** * Fetches manga information * @@ -135,6 +204,33 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa } } + /** + * Fetches anime information + * + * @param anime anime that needs updating + * @param episodes episodes of anime that needs updating + * @param categories categories that need updating + */ + private fun restoreAnimeFetch( + anime: Anime, + episodes: List, + categories: List, + history: List, + tracks: List, + backupCategories: List + ) { + try { + val fetchedAnime = backupManager.restoreAnime(anime) + fetchedAnime.id ?: return + + backupManager.restoreEpisodesForAnime(fetchedAnime, episodes) + + restoreExtraForAnime(fetchedAnime, categories, history, tracks, backupCategories) + } catch (e: Exception) { + errors.add(Date() to "${anime.title} - ${e.message}") + } + } + private fun restoreMangaNoFetch( backupManga: Manga, chapters: List, @@ -148,6 +244,19 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa restoreExtraForManga(backupManga, categories, history, tracks, backupCategories) } + private fun restoreAnimeNoFetch( + backupAnime: Anime, + episodes: List, + categories: List, + history: List, + tracks: List, + backupCategories: List + ) { + backupManager.restoreEpisodesForAnime(backupAnime, episodes) + + restoreExtraForAnime(backupAnime, categories, history, tracks, backupCategories) + } + private fun restoreExtraForManga(manga: Manga, categories: List, history: List, tracks: List, backupCategories: List) { // Restore categories backupManager.restoreCategoriesForManga(manga, categories, backupCategories) @@ -158,4 +267,15 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa // Restore tracking backupManager.restoreTrackForManga(manga, tracks) } + + private fun restoreExtraForAnime(anime: Anime, categories: List, history: List, tracks: List, backupCategories: List) { + // Restore categories + backupManager.restoreCategoriesForAnime(anime, categories, backupCategories) + + // Restore history + backupManager.restoreHistoryForAnime(history) + + // Restore tracking + backupManager.restoreTrackForAnime(anime, tracks) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestoreValidator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestoreValidator.kt index 90e6be5c1..d5e620558 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestoreValidator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestoreValidator.kt @@ -22,17 +22,25 @@ class FullBackupRestoreValidator : AbstractBackupRestoreValidator() { val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() } val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString) - if (backup.backupManga.isEmpty()) { + if (backup.backupManga.isEmpty() && backup.backupAnime.isEmpty()) { throw Exception(context.getString(R.string.invalid_backup_file_missing_manga)) } val sources = backup.backupSources.map { it.sourceId to it.name }.toMap() + val animesources = backup.backupAnimeSources.map { it.sourceId to it.name }.toMap() val missingSources = sources .filter { sourceManager.get(it.key) == null } .values - .sorted() + .sorted() + + animesources + .filter { animesourceManager.get(it.key) == null } + .values + .sorted() val trackers = backup.backupManga + .flatMap { it.tracking } + .map { it.syncId } + .distinct() + backup.backupAnime .flatMap { it.tracking } .map { it.syncId } .distinct() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/Backup.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/Backup.kt index b938639e7..53257ae47 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/Backup.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/Backup.kt @@ -7,6 +7,9 @@ import kotlinx.serialization.protobuf.ProtoNumber data class Backup( @ProtoNumber(1) val backupManga: List, @ProtoNumber(2) var backupCategories: List = emptyList(), + @ProtoNumber(3) val backupAnime: List, + @ProtoNumber(4) var backupCategoriesAnime: List = emptyList(), // Bump by 100 to specify this is a 0.x value @ProtoNumber(100) var backupSources: List = emptyList(), + @ProtoNumber(101) var backupAnimeSources: List = emptyList(), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupAnime.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupAnime.kt new file mode 100644 index 000000000..df5a34926 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupAnime.kt @@ -0,0 +1,87 @@ +package eu.kanade.tachiyomi.data.backup.full.models + +import eu.kanade.tachiyomi.data.database.models.Anime +import eu.kanade.tachiyomi.data.database.models.AnimeImpl +import eu.kanade.tachiyomi.data.database.models.AnimeTrackImpl +import eu.kanade.tachiyomi.data.database.models.EpisodeImpl +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber + +@Serializable +data class BackupAnime( + // in 1.x some of these values have different names + @ProtoNumber(1) var source: Long, + // url is called key in 1.x + @ProtoNumber(2) var url: String, + @ProtoNumber(3) var title: String = "", + @ProtoNumber(4) var artist: String? = null, + @ProtoNumber(5) var author: String? = null, + @ProtoNumber(6) var description: String? = null, + @ProtoNumber(7) var genre: List = emptyList(), + @ProtoNumber(8) var status: Int = 0, + // thumbnailUrl is called cover in 1.x + @ProtoNumber(9) var thumbnailUrl: String? = null, + // @ProtoNumber(10) val customCover: String = "", 1.x value, not used in 0.x + // @ProtoNumber(11) val lastUpdate: Long = 0, 1.x value, not used in 0.x + // @ProtoNumber(12) val lastInit: Long = 0, 1.x value, not used in 0.x + @ProtoNumber(13) var dateAdded: Long = 0, + // @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x + @ProtoNumber(16) var episodes: List = emptyList(), + @ProtoNumber(17) var categories: List = emptyList(), + @ProtoNumber(18) var tracking: List = emptyList(), + // Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x + @ProtoNumber(100) var favorite: Boolean = true, + @ProtoNumber(101) var episodeFlags: Int = 0, + @ProtoNumber(102) var history: List = emptyList(), + @ProtoNumber(103) var viewer_flags: Int = 0 +) { + fun getAnimeImpl(): AnimeImpl { + return AnimeImpl().apply { + url = this@BackupAnime.url + title = this@BackupAnime.title + artist = this@BackupAnime.artist + author = this@BackupAnime.author + description = this@BackupAnime.description + genre = this@BackupAnime.genre.joinToString() + status = this@BackupAnime.status + thumbnail_url = this@BackupAnime.thumbnailUrl + favorite = this@BackupAnime.favorite + source = this@BackupAnime.source + date_added = this@BackupAnime.dateAdded + viewer_flags = this@BackupAnime.viewer_flags + episode_flags = this@BackupAnime.episodeFlags + } + } + + fun getEpisodesImpl(): List { + return episodes.map { + it.toEpisodeImpl() + } + } + + fun getTrackingImpl(): List { + return tracking.map { + it.getTrackingImpl() + } + } + + companion object { + fun copyFrom(anime: Anime): BackupAnime { + return BackupAnime( + url = anime.url, + title = anime.title, + artist = anime.artist, + author = anime.author, + description = anime.description, + genre = anime.getGenres() ?: emptyList(), + status = anime.status, + thumbnailUrl = anime.thumbnail_url, + favorite = anime.favorite, + source = anime.source, + dateAdded = anime.date_added, + viewer_flags = anime.viewer_flags, + episodeFlags = anime.episode_flags + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupAnimeHistory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupAnimeHistory.kt new file mode 100644 index 000000000..16413a84f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupAnimeHistory.kt @@ -0,0 +1,10 @@ +package eu.kanade.tachiyomi.data.backup.full.models + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber + +@Serializable +data class BackupAnimeHistory( + @ProtoNumber(0) var url: String, + @ProtoNumber(1) var lastSeen: Long +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupAnimeSource.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupAnimeSource.kt new file mode 100644 index 000000000..c4c09a4b9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupAnimeSource.kt @@ -0,0 +1,20 @@ +package eu.kanade.tachiyomi.data.backup.full.models + +import eu.kanade.tachiyomi.animesource.AnimeSource +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber + +@Serializable +data class BackupAnimeSource( + @ProtoNumber(0) var name: String = "", + @ProtoNumber(1) var sourceId: Long +) { + companion object { + fun copyFrom(source: AnimeSource): BackupAnimeSource { + return BackupAnimeSource( + name = source.name, + sourceId = source.id + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupAnimeTracking.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupAnimeTracking.kt new file mode 100644 index 000000000..0a6824b55 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupAnimeTracking.kt @@ -0,0 +1,65 @@ +package eu.kanade.tachiyomi.data.backup.full.models + +import eu.kanade.tachiyomi.data.database.models.AnimeTrack +import eu.kanade.tachiyomi.data.database.models.AnimeTrackImpl +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber + +@Serializable +data class BackupAnimeTracking( + // in 1.x some of these values have different types or names + // syncId is called siteId in 1,x + @ProtoNumber(1) var syncId: Int, + // LibraryId is not null in 1.x + @ProtoNumber(2) var libraryId: Long, + @ProtoNumber(3) var mediaId: Int = 0, + // trackingUrl is called mediaUrl in 1.x + @ProtoNumber(4) var trackingUrl: String = "", + @ProtoNumber(5) var title: String = "", + // lastChapterRead is called last read, and it has been changed to a float in 1.x + @ProtoNumber(6) var lastChapterRead: Float = 0F, + @ProtoNumber(7) var totalChapters: Int = 0, + @ProtoNumber(8) var score: Float = 0F, + @ProtoNumber(9) var status: Int = 0, + // startedReadingDate is called startReadTime in 1.x + @ProtoNumber(10) var startedWatchingDate: Long = 0, + // finishedReadingDate is called endReadTime in 1.x + @ProtoNumber(11) var finishedWatchingDate: Long = 0, +) { + fun getTrackingImpl(): AnimeTrackImpl { + return AnimeTrackImpl().apply { + sync_id = this@BackupAnimeTracking.syncId + media_id = this@BackupAnimeTracking.mediaId + library_id = this@BackupAnimeTracking.libraryId + title = this@BackupAnimeTracking.title + // convert from float to int because of 1.x types + last_episode_seen = this@BackupAnimeTracking.lastChapterRead.toInt() + total_episodes = this@BackupAnimeTracking.totalChapters + score = this@BackupAnimeTracking.score + status = this@BackupAnimeTracking.status + started_watching_date = this@BackupAnimeTracking.startedWatchingDate + finished_watching_date = this@BackupAnimeTracking.finishedWatchingDate + tracking_url = this@BackupAnimeTracking.trackingUrl + } + } + + companion object { + fun copyFrom(track: AnimeTrack): BackupAnimeTracking { + return BackupAnimeTracking( + syncId = track.sync_id, + mediaId = track.media_id, + // forced not null so its compatible with 1.x backup system + libraryId = track.library_id!!, + title = track.title, + // convert to float for 1.x + lastChapterRead = track.last_episode_seen.toFloat(), + totalChapters = track.total_episodes, + score = track.score, + status = track.status, + startedWatchingDate = track.started_watching_date, + finishedWatchingDate = track.finished_watching_date, + trackingUrl = track.tracking_url + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupEpisode.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupEpisode.kt new file mode 100644 index 000000000..ed4227bbe --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupEpisode.kt @@ -0,0 +1,56 @@ +package eu.kanade.tachiyomi.data.backup.full.models + +import eu.kanade.tachiyomi.data.database.models.Episode +import eu.kanade.tachiyomi.data.database.models.EpisodeImpl +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber + +@Serializable +data class BackupEpisode( + // in 1.x some of these values have different names + // url is called key in 1.x + @ProtoNumber(1) var url: String, + @ProtoNumber(2) var name: String, + @ProtoNumber(3) var scanlator: String? = null, + @ProtoNumber(4) var seen: Boolean = false, + @ProtoNumber(5) var bookmark: Boolean = false, + // lastPageRead is called progress in 1.x + @ProtoNumber(6) var lastSecondSeen: Long = 0, + @ProtoNumber(7) var dateFetch: Long = 0, + @ProtoNumber(8) var dateUpload: Long = 0, + // episodeNumber is called number is 1.x + @ProtoNumber(9) var episodeNumber: Float = 0F, + @ProtoNumber(10) var sourceOrder: Int = 0, +) { + fun toEpisodeImpl(): EpisodeImpl { + return EpisodeImpl().apply { + url = this@BackupEpisode.url + name = this@BackupEpisode.name + episode_number = this@BackupEpisode.episodeNumber + scanlator = this@BackupEpisode.scanlator + seen = this@BackupEpisode.seen + bookmark = this@BackupEpisode.bookmark + last_second_seen = this@BackupEpisode.lastSecondSeen + date_fetch = this@BackupEpisode.dateFetch + date_upload = this@BackupEpisode.dateUpload + source_order = this@BackupEpisode.sourceOrder + } + } + + companion object { + fun copyFrom(episode: Episode): BackupEpisode { + return BackupEpisode( + url = episode.url, + name = episode.name, + episodeNumber = episode.episode_number, + scanlator = episode.scanlator, + seen = episode.seen, + bookmark = episode.bookmark, + lastSecondSeen = episode.last_second_seen, + dateFetch = episode.date_fetch, + dateUpload = episode.date_upload, + sourceOrder = episode.source_order + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupFull.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupFull.kt index 3ed6328b0..024195a21 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupFull.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupFull.kt @@ -7,6 +7,6 @@ import java.util.Locale object BackupFull { fun getDefaultFilename(): String { val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date()) - return "tachiyomi_$date.proto.gz" + return "aniyomi_$date.proto.gz" } }