From a379604974d6205d6be9a3ab00c51e6125bd7f3a Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 15 Jun 2024 13:49:57 +0300 Subject: [PATCH] Transfer scrobbling information within migration #930 --- .../alternatives/domain/MigrateUseCase.kt | 272 ++++++++++-------- .../kotatsu/details/ui/DetailsViewModel.kt | 4 +- .../scrobbling/common/domain/Scrobbler.kt | 2 +- .../selector/ScrobblingSelectorViewModel.kt | 2 +- 4 files changed, 164 insertions(+), 116 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/MigrateUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/MigrateUseCase.kt index 495e42f45..12d92bcf0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/MigrateUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/MigrateUseCase.kt @@ -12,136 +12,184 @@ import org.koitharu.kotatsu.history.data.toMangaHistory import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler +import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus import org.koitharu.kotatsu.tracker.data.TrackEntity import javax.inject.Inject -class MigrateUseCase @Inject constructor( - private val mangaRepositoryFactory: MangaRepository.Factory, - private val mangaDataRepository: MangaDataRepository, - private val database: MangaDatabase, - private val progressUpdateUseCase: ProgressUpdateUseCase, -) { - - suspend operator fun invoke(oldManga: Manga, newManga: Manga) { - val oldDetails = if (oldManga.chapters.isNullOrEmpty()) { - runCatchingCancellable { - mangaRepositoryFactory.create(oldManga.source).getDetails(oldManga) - }.getOrDefault(oldManga) - } else { - oldManga - } - val newDetails = if (newManga.chapters.isNullOrEmpty()) { - mangaRepositoryFactory.create(newManga.source).getDetails(newManga) - } else { - newManga - } - mangaDataRepository.storeManga(newDetails) - database.withTransaction { - // replace favorites - val favoritesDao = database.getFavouritesDao() - val oldFavourites = favoritesDao.findAllRaw(oldDetails.id) - if (oldFavourites.isNotEmpty()) { - favoritesDao.delete(oldManga.id) - for (f in oldFavourites) { - val e = f.copy( - mangaId = newManga.id, +class MigrateUseCase + @Inject + constructor( + private val mangaRepositoryFactory: MangaRepository.Factory, + private val mangaDataRepository: MangaDataRepository, + private val database: MangaDatabase, + private val progressUpdateUseCase: ProgressUpdateUseCase, + private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>, + ) { + suspend operator fun invoke( + oldManga: Manga, + newManga: Manga, + ) { + val oldDetails = + if (oldManga.chapters.isNullOrEmpty()) { + runCatchingCancellable { + mangaRepositoryFactory.create(oldManga.source).getDetails(oldManga) + }.getOrDefault(oldManga) + } else { + oldManga + } + val newDetails = + if (newManga.chapters.isNullOrEmpty()) { + mangaRepositoryFactory.create(newManga.source).getDetails(newManga) + } else { + newManga + } + mangaDataRepository.storeManga(newDetails) + database.withTransaction { + // replace favorites + val favoritesDao = database.getFavouritesDao() + val oldFavourites = favoritesDao.findAllRaw(oldDetails.id) + if (oldFavourites.isNotEmpty()) { + favoritesDao.delete(oldManga.id) + for (f in oldFavourites) { + val e = + f.copy( + mangaId = newManga.id, + ) + favoritesDao.upsert(e) + } + } + // replace history + val historyDao = database.getHistoryDao() + val oldHistory = historyDao.find(oldDetails.id) + val newHistory = + if (oldHistory != null) { + val newHistory = makeNewHistory(oldDetails, newDetails, oldHistory) + historyDao.delete(oldDetails.id) + historyDao.upsert(newHistory) + newHistory + } else { + null + } + // track + val tracksDao = database.getTracksDao() + val oldTrack = tracksDao.find(oldDetails.id) + if (oldTrack != null) { + val lastChapter = newDetails.chapters?.lastOrNull() + val newTrack = + TrackEntity( + mangaId = newDetails.id, + lastChapterId = lastChapter?.id ?: 0L, + newChapters = 0, + lastCheckTime = System.currentTimeMillis(), + lastChapterDate = lastChapter?.uploadDate ?: 0L, + lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION, + lastError = null, + ) + tracksDao.delete(oldDetails.id) + tracksDao.upsert(newTrack) + } + // scrobbling + for (scrobbler in scrobblers) { + if (!scrobbler.isEnabled) { + continue + } + val prevInfo = scrobbler.getScrobblingInfoOrNull(oldDetails.id) ?: continue + scrobbler.unregisterScrobbling(oldDetails.id) + scrobbler.linkManga(newDetails.id, prevInfo.targetId) + scrobbler.updateScrobblingInfo( + mangaId = newDetails.id, + rating = prevInfo.rating, + status = + prevInfo.status ?: when { + newHistory == null -> ScrobblingStatus.PLANNED + newHistory.percent == 1f -> ScrobblingStatus.COMPLETED + else -> ScrobblingStatus.READING + }, + comment = prevInfo.comment, ) - favoritesDao.upsert(e) + if (newHistory != null) { + scrobbler.scrobble( + manga = newDetails, + chapterId = newHistory.chapterId, + ) + } } } - // replace history - val historyDao = database.getHistoryDao() - val oldHistory = historyDao.find(oldDetails.id) - if (oldHistory != null) { - val newHistory = makeNewHistory(oldDetails, newDetails, oldHistory) - historyDao.delete(oldDetails.id) - historyDao.upsert(newHistory) - } - // track - val tracksDao = database.getTracksDao() - val oldTrack = tracksDao.find(oldDetails.id) - if (oldTrack != null) { - val lastChapter = newDetails.chapters?.lastOrNull() - val newTrack = TrackEntity( - mangaId = newDetails.id, - lastChapterId = lastChapter?.id ?: 0L, - newChapters = 0, - lastCheckTime = System.currentTimeMillis(), - lastChapterDate = lastChapter?.uploadDate ?: 0L, - lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION, - lastError = null, - ) - tracksDao.delete(oldDetails.id) - tracksDao.upsert(newTrack) - } + progressUpdateUseCase(newManga) } - progressUpdateUseCase(newManga) - } - private fun makeNewHistory( - oldManga: Manga, - newManga: Manga, - history: HistoryEntity, - ): HistoryEntity { - if (oldManga.chapters.isNullOrEmpty()) { // probably broken manga/source - val branch = newManga.getPreferredBranch(null) - val chapters = checkNotNull(newManga.getChapters(branch)) - val currentChapter = if (history.percent in 0f..1f) { - chapters[(chapters.lastIndex * history.percent).toInt()] - } else { - chapters.first() + private fun makeNewHistory( + oldManga: Manga, + newManga: Manga, + history: HistoryEntity, + ): HistoryEntity { + if (oldManga.chapters.isNullOrEmpty()) { // probably broken manga/source + val branch = newManga.getPreferredBranch(null) + val chapters = checkNotNull(newManga.getChapters(branch)) + val currentChapter = + if (history.percent in 0f..1f) { + chapters[(chapters.lastIndex * history.percent).toInt()] + } else { + chapters.first() + } + return HistoryEntity( + mangaId = newManga.id, + createdAt = history.createdAt, + updatedAt = System.currentTimeMillis(), + chapterId = currentChapter.id, + page = history.page, + scroll = history.scroll, + percent = history.percent, + deletedAt = 0, + chaptersCount = chapters.size, + ) } + val branch = oldManga.getPreferredBranch(history.toMangaHistory()) + val oldChapters = checkNotNull(oldManga.getChapters(branch)) + var index = oldChapters.indexOfFirst { it.id == history.chapterId } + if (index < 0) { + index = + if (history.percent in 0f..1f) { + (oldChapters.lastIndex * history.percent).toInt() + } else { + 0 + } + } + val newChapters = checkNotNull(newManga.chapters).groupBy { it.branch } + val newBranch = + if (newChapters.containsKey(branch)) { + branch + } else { + newManga.getPreferredBranch(null) + } + val newChapterId = + checkNotNull(newChapters[newBranch]) + .let { + val oldChapter = oldChapters[index] + it.findByNumber(oldChapter.volume, oldChapter.number) ?: it.getOrNull(index) ?: it.last() + }.id + return HistoryEntity( mangaId = newManga.id, createdAt = history.createdAt, updatedAt = System.currentTimeMillis(), - chapterId = currentChapter.id, + chapterId = newChapterId, page = history.page, scroll = history.scroll, - percent = history.percent, + percent = PROGRESS_NONE, deletedAt = 0, - chaptersCount = chapters.size, + chaptersCount = checkNotNull(newChapters[newBranch]).size, ) } - val branch = oldManga.getPreferredBranch(history.toMangaHistory()) - val oldChapters = checkNotNull(oldManga.getChapters(branch)) - var index = oldChapters.indexOfFirst { it.id == history.chapterId } - if (index < 0) { - index = if (history.percent in 0f..1f) { - (oldChapters.lastIndex * history.percent).toInt() + + private fun List.findByNumber( + volume: Int, + number: Float, + ): MangaChapter? = + if (number <= 0f) { + null } else { - 0 + firstOrNull { it.volume == volume && it.number == number } } - } - val newChapters = checkNotNull(newManga.chapters).groupBy { it.branch } - val newBranch = if (newChapters.containsKey(branch)) { - branch - } else { - newManga.getPreferredBranch(null) - } - val newChapterId = checkNotNull(newChapters[newBranch]).let { - val oldChapter = oldChapters[index] - it.findByNumber(oldChapter.volume, oldChapter.number) ?: it.getOrNull(index) ?: it.last() - }.id - - return HistoryEntity( - mangaId = newManga.id, - createdAt = history.createdAt, - updatedAt = System.currentTimeMillis(), - chapterId = newChapterId, - page = history.page, - scroll = history.scroll, - percent = PROGRESS_NONE, - deletedAt = 0, - chaptersCount = checkNotNull(newChapters[newBranch]).size, - ) } - - private fun List.findByNumber(volume: Int, number: Float): MangaChapter? { - return if (number <= 0f) { - null - } else { - firstOrNull { it.volume == volume && it.number == number } - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index bccdde97f..f7f332801 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -162,7 +162,7 @@ class DetailsViewModel @Inject constructor( val onMangaRemoved = MutableEventFlow() val isScrobblingAvailable: Boolean - get() = scrobblers.any { it.isAvailable } + get() = scrobblers.any { it.isEnabled } val scrobblingInfo: StateFlow> = interactor.observeScrobblingInfo(mangaId) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) @@ -393,7 +393,7 @@ class DetailsViewModel @Inject constructor( private fun getScrobbler(index: Int): Scrobbler? { val info = scrobblingInfo.value.getOrNull(index) val scrobbler = if (info != null) { - scrobblers.find { it.scrobblerService == info.scrobbler && it.isAvailable } + scrobblers.find { it.scrobblerService == info.scrobbler && it.isEnabled } } else { null } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/Scrobbler.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/Scrobbler.kt index 9db96ade8..e0bc73f37 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/Scrobbler.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/Scrobbler.kt @@ -51,7 +51,7 @@ abstract class Scrobbler( } } - val isAvailable: Boolean + val isEnabled: Boolean get() = repository.isAuthorized suspend fun authorize(authCode: String): ScrobblerUser { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt index a984526c3..df8238106 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt @@ -42,7 +42,7 @@ class ScrobblingSelectorViewModel @Inject constructor( val manga = savedStateHandle.require(MangaIntent.KEY_MANGA).manga - val availableScrobblers = scrobblers.filter { it.isAvailable } + val availableScrobblers = scrobblers.filter { it.isEnabled } val selectedScrobblerIndex = MutableStateFlow(0)