fix anime backups

closes #30
This commit is contained in:
jmir1 2021-06-30 16:47:13 +02:00
parent 5be3309d81
commit 45a50a7e91
14 changed files with 709 additions and 37 deletions

View file

@ -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<Episode>): Pair<List<Episode>, List<Episode>> {
@ -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<Anime> =
animedatabaseHelper.getFavoriteAnimes().executeAsBlocking()

View file

@ -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<T : AbstractBackupManager>(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<T : AbstractBackupManager>(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<Episode>): Pair<List<Episode>, List<Episode>> {
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<T : AbstractBackupManager>(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<AnimeTrack>) {
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<T : AbstractBackupManager>(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 ->

View file

@ -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

View file

@ -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

View file

@ -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<Anime>, flags: Int): List<BackupAnime> {
return animes.map {
backupAnimeObject(it, flags)
}
}
private fun backupExtensionInfo(mangas: List<Manga>): List<BackupSource> {
return mangas
.asSequence()
@ -106,6 +104,16 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
.toList()
}
private fun backupAnimeExtensionInfo(animes: List<Anime>): List<BackupAnimeSource> {
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<BackupCategory> {
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<BackupCategory>) {
// 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<Int>, backupCategories: List<BackupCategory>) {
val dbCategories = animedatabaseHelper.getCategories().executeAsBlocking()
val animeCategoriesToUpdate = ArrayList<AnimeCategory>(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<BackupAnimeHistory>) {
// List containing history to be updated
val historyToBeUpdated = ArrayList<AnimeHistory>(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<AnimeTrack>) {
// 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<AnimeTrack>()
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<Chapter>) {
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<Episode>) {
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) }
}
}

View file

@ -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<BackupCategory>) {
animedb.inTransaction {
backupManager.restoreCategoriesAnime(backupCategories)
}
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
}
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>) {
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<BackupCategory>) {
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<Episode>,
categories: List<Int>,
history: List<BackupAnimeHistory>,
tracks: List<AnimeTrack>,
backupCategories: List<BackupCategory>
) {
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<Episode>,
categories: List<Int>,
history: List<BackupAnimeHistory>,
tracks: List<AnimeTrack>,
backupCategories: List<BackupCategory>
) {
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<Chapter>,
@ -148,6 +244,19 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories)
}
private fun restoreAnimeNoFetch(
backupAnime: Anime,
episodes: List<Episode>,
categories: List<Int>,
history: List<BackupAnimeHistory>,
tracks: List<AnimeTrack>,
backupCategories: List<BackupCategory>
) {
backupManager.restoreEpisodesForAnime(backupAnime, episodes)
restoreExtraForAnime(backupAnime, categories, history, tracks, backupCategories)
}
private fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
// 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<Int>, history: List<BackupAnimeHistory>, tracks: List<AnimeTrack>, backupCategories: List<BackupCategory>) {
// Restore categories
backupManager.restoreCategoriesForAnime(anime, categories, backupCategories)
// Restore history
backupManager.restoreHistoryForAnime(history)
// Restore tracking
backupManager.restoreTrackForAnime(anime, tracks)
}
}

View file

@ -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() +
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()

View file

@ -7,6 +7,9 @@ import kotlinx.serialization.protobuf.ProtoNumber
data class Backup(
@ProtoNumber(1) val backupManga: List<BackupManga>,
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
@ProtoNumber(3) val backupAnime: List<BackupAnime>,
@ProtoNumber(4) var backupCategoriesAnime: List<BackupCategory> = emptyList(),
// Bump by 100 to specify this is a 0.x value
@ProtoNumber(100) var backupSources: List<BackupSource> = emptyList(),
@ProtoNumber(101) var backupAnimeSources: List<BackupAnimeSource> = emptyList(),
)

View file

@ -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<String> = 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<BackupEpisode> = emptyList(),
@ProtoNumber(17) var categories: List<Int> = emptyList(),
@ProtoNumber(18) var tracking: List<BackupAnimeTracking> = 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<BackupAnimeHistory> = 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<EpisodeImpl> {
return episodes.map {
it.toEpisodeImpl()
}
}
fun getTrackingImpl(): List<AnimeTrackImpl> {
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
)
}
}
}

View file

@ -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
)

View file

@ -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
)
}
}
}

View file

@ -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
)
}
}
}

View file

@ -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
)
}
}
}

View file

@ -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"
}
}