From 4c37f4c764afc47e0be6c63b1389b54f240b03f5 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Sun, 14 Jun 2026 02:34:31 +0600 Subject: [PATCH] Adapt source implementation for TachiyomiX 1.6 (#3243) --- .../java/eu/kanade/domain/DomainModule.kt | 3 + .../domain/manga/interactor/UpdateManga.kt | 69 ------ .../presentation/browse/GlobalSearchScreen.kt | 8 +- .../browse/MigrateSearchScreen.kt | 4 +- .../settings/screen/SettingsTrackingScreen.kt | 2 +- .../creators/PreferenceBackupCreator.kt | 2 +- .../data/library/LibraryUpdateJob.kt | 30 +-- .../data/library/MetadataUpdateJob.kt | 23 +- .../extension/util/ExtensionLoader.kt | 5 +- .../tachiyomi/source/AndroidSourceManager.kt | 8 +- .../browse/extension/ExtensionsScreenModel.kt | 2 +- .../details/ExtensionDetailsScreenModel.kt | 3 +- .../search/MigrateSearchScreenModel.kt | 10 +- .../search/MigrateSourceSearchScreen.kt | 2 +- .../source/browse/BrowseSourceScreen.kt | 7 +- .../source/browse/BrowseSourceScreenModel.kt | 35 +-- .../globalsearch/GlobalSearchScreenModel.kt | 4 +- .../source/globalsearch/SearchScreenModel.kt | 19 +- .../ui/deeplink/DeepLinkScreenModel.kt | 19 +- .../tachiyomi/ui/manga/MangaScreenModel.kt | 108 ++++----- .../kanade/tachiyomi/util/MangaExtensions.kt | 27 --- .../migration/usecases/MigrateMangaUseCase.kt | 13 +- .../interactor/UpdateMangaFromRemote.kt | 142 ++++++++++++ .../domain/source/models/RemoteMangaUpdate.kt | 9 + .../migration/config/MigrationConfigScreen.kt | 2 +- .../list/MigrationListScreenModel.kt | 30 +-- .../list/search/SmartSourceSearchEngine.kt | 8 +- .../data/source/SourcePagingSource.kt | 10 +- .../data/source/SourceRepositoryImpl.kt | 16 +- .../domain/source/model/StubSource.kt | 20 +- .../domain/source/service/SourceManager.kt | 7 +- .../tachiyomi/source/CatalogueSource.kt | 100 ++++----- .../eu/kanade/tachiyomi/source/Source.kt | 108 +++++---- .../tachiyomi/source/model/SMangaUpdate.kt | 4 + .../tachiyomi/source/online/HttpSource.kt | 212 ++++++++++-------- .../source/online/ParsedHttpSource.kt | 26 ++- .../tachiyomi/source/local/LocalSource.kt | 20 +- .../tachiyomi/source/local/LocalSource.kt | 4 +- 38 files changed, 597 insertions(+), 524 deletions(-) create mode 100644 app/src/main/java/mihon/domain/source/interactor/UpdateMangaFromRemote.kt create mode 100644 app/src/main/java/mihon/domain/source/models/RemoteMangaUpdate.kt create mode 100644 source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SMangaUpdate.kt diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index b69d60186..30937379d 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -35,6 +35,7 @@ import mihon.domain.extension.interactor.RemoveExtensionStore import mihon.domain.extension.interactor.UpdateExtensionStores import mihon.domain.extension.repository.ExtensionStoreRepository import mihon.domain.migration.usecases.MigrateMangaUseCase +import mihon.domain.source.interactor.UpdateMangaFromRemote import mihon.domain.upcoming.interactor.GetUpcomingManga import tachiyomi.data.category.CategoryRepositoryImpl import tachiyomi.data.chapter.ChapterRepositoryImpl @@ -204,5 +205,7 @@ class DomainModule : InjektModule { addFactory { ToggleIncognito(get()) } addFactory { GetIncognitoState(get(), get(), get()) } + + addFactory { UpdateMangaFromRemote(get(), get(), get(), get(), get(), get(), get()) } } } diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt index 52bce0fc3..7c93e8748 100644 --- a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt @@ -1,17 +1,9 @@ package eu.kanade.domain.manga.interactor -import eu.kanade.domain.manga.model.hasCustomCover -import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.source.model.SManga -import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.repository.MangaRepository -import tachiyomi.source.local.isLocal -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import java.time.Instant import java.time.ZonedDateTime @@ -28,67 +20,6 @@ class UpdateManga( return mangaRepository.updateAll(mangaUpdates) } - suspend fun awaitUpdateFromSource( - localManga: Manga, - remoteManga: SManga, - manualFetch: Boolean, - coverCache: CoverCache = Injekt.get(), - libraryPreferences: LibraryPreferences = Injekt.get(), - downloadManager: DownloadManager = Injekt.get(), - ): Boolean { - val remoteTitle = try { - remoteManga.title - } catch (_: UninitializedPropertyAccessException) { - "" - } - - // if the manga isn't a favorite (or 'update titles' preference is enabled), set its title from source and update in db - val title = - if (remoteTitle.isNotEmpty() && (!localManga.favorite || libraryPreferences.updateMangaTitles.get())) { - remoteTitle - } else { - null - } - - val coverLastModified = - when { - // Never refresh covers if the url is empty to avoid "losing" existing covers - remoteManga.thumbnail_url.isNullOrEmpty() -> null - !manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null - localManga.isLocal() -> Instant.now().toEpochMilli() - localManga.hasCustomCover(coverCache) -> { - coverCache.deleteFromCache(localManga, false) - null - } - else -> { - coverCache.deleteFromCache(localManga, false) - Instant.now().toEpochMilli() - } - } - - val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() } - - val success = mangaRepository.update( - MangaUpdate( - id = localManga.id, - title = title, - coverLastModified = coverLastModified, - author = remoteManga.author, - artist = remoteManga.artist, - description = remoteManga.description, - genre = remoteManga.getGenres(), - thumbnailUrl = thumbnailUrl, - status = remoteManga.status.toLong(), - updateStrategy = remoteManga.update_strategy, - initialized = true, - ), - ) - if (success && title != null) { - downloadManager.renameManga(localManga, title) - } - return success - } - suspend fun awaitUpdateFetchInterval( manga: Manga, dateTime: ZonedDateTime = ZonedDateTime.now(), diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index 8d949235a..06de14885 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -10,7 +10,7 @@ import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem import eu.kanade.presentation.browse.components.GlobalSearchResultItem import eu.kanade.presentation.browse.components.GlobalSearchToolbar -import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter @@ -27,7 +27,7 @@ fun GlobalSearchScreen( onChangeSearchFilter: (SourceFilter) -> Unit, onToggleResults: () -> Unit, getManga: @Composable (Manga) -> State, - onClickSource: (CatalogueSource) -> Unit, + onClickSource: (Source) -> Unit, onClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit, ) { @@ -62,10 +62,10 @@ fun GlobalSearchScreen( @Composable internal fun GlobalSearchContent( - items: Map, + items: Map, contentPadding: PaddingValues, getManga: @Composable (Manga) -> State, - onClickSource: (CatalogueSource) -> Unit, + onClickSource: (Source) -> Unit, onClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit, fromSourceId: Long? = null, diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt index 48b86bba5..9cfd8bcc4 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt @@ -3,7 +3,7 @@ package eu.kanade.presentation.browse import androidx.compose.runtime.Composable import androidx.compose.runtime.State import eu.kanade.presentation.browse.components.GlobalSearchToolbar -import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import tachiyomi.domain.manga.model.Manga @@ -19,7 +19,7 @@ fun MigrateSearchScreen( onChangeSearchFilter: (SourceFilter) -> Unit, onToggleResults: () -> Unit, getManga: @Composable (Manga) -> State, - onClickSource: (CatalogueSource) -> Unit, + onClickSource: (Source) -> Unit, onClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit, ) { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt index 96384f34e..7babffd26 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt @@ -111,7 +111,7 @@ object SettingsTrackingScreen : SearchableSettings { .filter { it is EnhancedTracker } .partition { service -> val acceptedSources = (service as EnhancedTracker).getAcceptedSources() - sourceManager.getCatalogueSources().any { it::class.qualifiedName in acceptedSources } + sourceManager.getAll().any { it::class.qualifiedName in acceptedSources } } var enhancedTrackerInfo = stringResource(MR.strings.enhanced_tracking_info) if (enhancedTrackers.second.isNotEmpty()) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt index 5b72ce384..e77a32f40 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt @@ -28,7 +28,7 @@ class PreferenceBackupCreator( } fun createSource(includePrivatePreferences: Boolean): List { - return sourceManager.getCatalogueSources() + return sourceManager.getAll() .filterIsInstance() .map { BackupSourcePreferences( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index d9e47fe60..7543d4bfd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -18,10 +18,6 @@ import androidx.work.WorkInfo import androidx.work.WorkQuery import androidx.work.WorkerParameters import androidx.work.workDataOf -import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource -import eu.kanade.domain.manga.interactor.UpdateManga -import eu.kanade.domain.manga.model.toSManga -import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.source.model.SManga @@ -41,6 +37,7 @@ import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import logcat.LogPriority import mihon.domain.chapter.interactor.FilterChaptersForDownload +import mihon.domain.source.interactor.UpdateMangaFromRemote import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.preference.getAndSet import tachiyomi.core.common.util.lang.withIOContext @@ -83,13 +80,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet private val sourceManager: SourceManager = Injekt.get() private val libraryPreferences: LibraryPreferences = Injekt.get() private val downloadManager: DownloadManager = Injekt.get() - private val coverCache: CoverCache = Injekt.get() private val getLibraryManga: GetLibraryManga = Injekt.get() private val getManga: GetManga = Injekt.get() - private val updateManga: UpdateManga = Injekt.get() - private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get() private val fetchInterval: FetchInterval = Injekt.get() private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get() + private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get() private val notifier = LibraryUpdateNotifier(context) @@ -331,19 +326,16 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet private suspend fun updateManga(manga: Manga, fetchWindow: Pair): List { val source = sourceManager.getOrStub(manga.source) - // Update manga metadata if needed - if (libraryPreferences.autoUpdateMetadata.get()) { - val networkManga = source.getMangaDetails(manga.toSManga()) - updateManga.awaitUpdateFromSource(manga, networkManga, manualFetch = false, coverCache) - } + val update = updateMangaFromRemote( + source = source, + manga = manga, + fetchDetails = libraryPreferences.autoUpdateMetadata.get(), + fetchChapters = true, + fetchWindow = fetchWindow, + ) + .getOrThrow() - val chapters = source.getChapterList(manga.toSManga()) - - // Get manga from database to account for if it was removed during the update and - // to get latest data so it doesn't get overwritten later on - val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList() - - return syncChaptersWithSource.await(chapters, dbManga, source, false, fetchWindow) + return if (update.manga.favorite) update.newChapters else emptyList() } private suspend fun withUpdateNotification( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt index b519bb483..966de7fe9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt @@ -10,12 +10,7 @@ import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkInfo import androidx.work.WorkQuery import androidx.work.WorkerParameters -import eu.kanade.domain.manga.interactor.UpdateManga -import eu.kanade.domain.manga.model.copyFrom -import eu.kanade.domain.manga.model.toSManga -import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.notification.Notifications -import eu.kanade.tachiyomi.util.prepUpdateCover import eu.kanade.tachiyomi.util.system.isRunning import eu.kanade.tachiyomi.util.system.setForegroundSafely import eu.kanade.tachiyomi.util.system.workManager @@ -27,12 +22,12 @@ import kotlinx.coroutines.ensureActive import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import logcat.LogPriority +import mihon.domain.source.interactor.UpdateMangaFromRemote import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.manga.interactor.GetLibraryManga import tachiyomi.domain.manga.model.Manga -import tachiyomi.domain.manga.model.toMangaUpdate import tachiyomi.domain.source.service.SourceManager import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -46,9 +41,8 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame CoroutineWorker(context, workerParams) { private val sourceManager: SourceManager = Injekt.get() - private val coverCache: CoverCache = Injekt.get() private val getLibraryManga: GetLibraryManga = Injekt.get() - private val updateManga: UpdateManga = Injekt.get() + private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get() private val notifier = LibraryUpdateNotifier(context) @@ -120,14 +114,11 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame ) { val source = sourceManager.get(manga.source) ?: return@withUpdateNotification try { - val networkManga = source.getMangaDetails(manga.toSManga()) - val updatedManga = manga.prepUpdateCover(coverCache, networkManga, true) - .copyFrom(networkManga) - try { - updateManga.await(updatedManga.toMangaUpdate()) - } catch (e: Exception) { - logcat(LogPriority.ERROR) { "Manga doesn't exist anymore" } - } + updateMangaFromRemote( + source = source, + manga = manga, + fetchDetails = true, + ).getOrThrow() } catch (e: Throwable) { // Ignore errors and continue logcat(LogPriority.ERROR, e) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index db6b0aa40..90d9d1598 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -10,7 +10,6 @@ import eu.kanade.domain.extension.interactor.TrustExtension import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.LoadResult -import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.util.lang.Hash @@ -301,9 +300,7 @@ internal object ExtensionLoader { } } - val langs = sources.filterIsInstance() - .map { it.lang } - .toSet() + val langs = sources.map { it.lang }.toSet() val lang = when (langs.size) { 0 -> "" 1 -> langs.first() diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt index 7730e152b..0682b8cf3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt @@ -38,9 +38,7 @@ class AndroidSourceManager( private val stubSourcesMap = ConcurrentHashMap() - override val catalogueSources: Flow> = sourcesMapFlow.map { - it.values.filterIsInstance() - } + override val sources: Flow> = sourcesMapFlow.map { it.values.toList() } init { scope.launchIO { @@ -87,9 +85,9 @@ class AndroidSourceManager( } } - override fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance() + override fun getAll() = sourcesMapFlow.value.values.toList() - override fun getCatalogueSources() = sourcesMapFlow.value.values.filterIsInstance() + override fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance() override fun getStubSources(): List { val onlineSourceIds = getOnlineSources().map { it.id } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt index e326d297d..c6238cb08 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt @@ -120,7 +120,7 @@ class ExtensionsScreenModel( when (extension) { is Extension.Installed -> extension.sources.any { source -> source.name.contains(subquery, ignoreCase = true) || - (source as? HttpSource)?.baseUrl?.contains(subquery, ignoreCase = true) == true || + (source as? HttpSource)?.getHomeUrl()?.contains(subquery, ignoreCase = true) == true || source.id == subquery.toLongOrNull() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt index b682245f1..b9cf70a0a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt @@ -99,7 +99,8 @@ class ExtensionDetailsScreenModel( val urls = extension.sources .filterIsInstance() - .mapNotNull { it.baseUrl.takeUnless { url -> url.isEmpty() } } + .flatMap { listOf(it.baseUrl, it.getHomeUrl()) } + .filter { it.isNotEmpty() } .distinct() val cleared = urls.sumOf { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt index ddb89c480..e0aea2ba6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt @@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.search import cafe.adriel.voyager.core.model.screenModelScope import eu.kanade.domain.source.service.SourcePreferences -import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import kotlinx.coroutines.flow.update @@ -21,8 +21,8 @@ class MigrateSearchScreenModel( private val migrationSources by lazy { sourcePreferences.migrationSources.get() } - override val sortComparator = { map: Map -> - compareBy( + override val sortComparator = { map: Map -> + compareBy( { (map[it] as? SearchItemResult.Success)?.isEmpty ?: true }, { migrationSources.indexOf(it.id) }, ) @@ -41,7 +41,7 @@ class MigrateSearchScreenModel( } } - override fun getEnabledSources(): List { - return migrationSources.mapNotNull { sourceManager.get(it) as? CatalogueSource } + override fun getEnabledSources(): List { + return migrationSources.mapNotNull { sourceManager.get(it) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt index 5d73c077e..ff5e08315 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt @@ -110,7 +110,7 @@ data class MigrateSourceSearchScreen( val source = screenModel.source as? HttpSource ?: return@BrowseSourceContent navigator.push( WebViewScreen( - url = source.baseUrl, + url = source.getHomeUrl(), initialTitle = source.name, sourceId = source.id, ), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index 1153b3025..7250e41e6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -44,7 +44,6 @@ import eu.kanade.presentation.category.components.ChangeCategoryDialog import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.presentation.util.AssistContentScreen import eu.kanade.presentation.util.Screen -import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing @@ -111,7 +110,7 @@ data class BrowseSourceScreen( val source = screenModel.source as? HttpSource ?: return@f navigator.push( WebViewScreen( - url = source.baseUrl, + url = source.getHomeUrl(), initialTitle = source.name, sourceId = source.id, ), @@ -119,7 +118,7 @@ data class BrowseSourceScreen( } LaunchedEffect(screenModel.source) { - assistUrl = (screenModel.source as? HttpSource)?.baseUrl + assistUrl = (screenModel.source as? HttpSource)?.getHomeUrl() } Scaffold( @@ -166,7 +165,7 @@ data class BrowseSourceScreen( Text(text = stringResource(MR.strings.popular)) }, ) - if ((screenModel.source as CatalogueSource).supportsLatest) { + if (screenModel.source.supportsLatest) { FilterChip( selected = state.listing == Listing.Latest, onClick = { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt index 114df78f2..55e3204b4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt @@ -20,7 +20,6 @@ import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.presentation.util.ioCoroutineScope import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.util.removeCovers import kotlinx.coroutines.flow.SharingStarted @@ -74,22 +73,20 @@ class BrowseSourceScreenModel( val source = sourceManager.getOrStub(sourceId) init { - if (source is CatalogueSource) { - mutableState.update { - var query: String? = null - var listing = it.listing + mutableState.update { + var query: String? = null + var listing = it.listing - if (listing is Listing.Search) { - query = listing.query - listing = Listing.Search(query, source.getFilterList()) - } - - it.copy( - listing = listing, - filters = source.getFilterList(), - toolbarQuery = query, - ) + if (listing is Listing.Search) { + query = listing.query + listing = Listing.Search(query, source.getFilterList()) } + + it.copy( + listing = listing, + filters = source.getFilterList(), + toolbarQuery = query, + ) } if (!getIncognitoState.await(source.id)) { @@ -129,8 +126,6 @@ class BrowseSourceScreenModel( } fun resetFilters() { - if (source !is CatalogueSource) return - mutableState.update { it.copy(filters = source.getFilterList()) } } @@ -139,8 +134,6 @@ class BrowseSourceScreenModel( } fun setFilters(filters: FilterList) { - if (source !is CatalogueSource) return - mutableState.update { it.copy( filters = filters, @@ -149,8 +142,6 @@ class BrowseSourceScreenModel( } fun search(query: String? = null, filters: FilterList? = null) { - if (source !is CatalogueSource) return - val input = state.value.listing as? Listing.Search ?: Listing.Search(query = null, filters = source.getFilterList()) @@ -166,8 +157,6 @@ class BrowseSourceScreenModel( } fun searchGenre(genreName: String) { - if (source !is CatalogueSource) return - val defaultFilters = source.getFilterList() var genreExists = false diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt index 33c715768..161fbc360 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt @@ -1,6 +1,6 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch -import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source class GlobalSearchScreenModel( initialQuery: String = "", @@ -18,7 +18,7 @@ class GlobalSearchScreenModel( } } - override fun getEnabledSources(): List { + override fun getEnabledSources(): List { return super.getEnabledSources() .filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt index 3bb0c60b6..4274b191c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt @@ -8,7 +8,7 @@ import cafe.adriel.voyager.core.model.screenModelScope import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.presentation.util.ioCoroutineScope import eu.kanade.tachiyomi.extension.ExtensionManager -import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source import kotlinx.coroutines.Job import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.async @@ -52,8 +52,8 @@ abstract class SearchScreenModel( protected var extensionFilter: String? = null - open val sortComparator = { map: Map -> - compareBy( + open val sortComparator = { map: Map -> + compareBy( { (map[it] as? SearchItemResult.Success)?.isEmpty ?: true }, { "${it.id}" !in pinnedSources }, { "${it.name.lowercase()} (${it.lang})" }, @@ -79,8 +79,8 @@ abstract class SearchScreenModel( } } - open fun getEnabledSources(): List { - return sourceManager.getCatalogueSources() + open fun getEnabledSources(): List { + return sourceManager.getAll() .filter { it.lang in enabledLanguages && "${it.id}" !in disabledSources } .sortedWith( compareBy( @@ -90,7 +90,7 @@ abstract class SearchScreenModel( ) } - private fun getSelectedSources(): List { + private fun getSelectedSources(): List { val enabledSources = getEnabledSources() val filter = extensionFilter @@ -101,7 +101,6 @@ abstract class SearchScreenModel( return extensionManager.installedExtensionsFlow.value .filter { it.pkgName == filter } .flatMap { it.sources } - .filterIsInstance() .filter { it in enabledSources } } @@ -179,7 +178,7 @@ abstract class SearchScreenModel( } } - private fun updateItems(items: Map) { + private fun updateItems(items: Map) { mutableState.update { it.copy( items = items @@ -188,7 +187,7 @@ abstract class SearchScreenModel( } } - private fun updateItem(source: CatalogueSource, result: SearchItemResult) { + private fun updateItem(source: Source, result: SearchItemResult) { updateItems(state.value.items + (source to result)) } @@ -209,7 +208,7 @@ abstract class SearchScreenModel( val searchQuery: String? = null, val sourceFilter: SourceFilter = SourceFilter.PinnedOnly, val onlyShowHasResults: Boolean = false, - val items: Map = mapOf(), + val items: Map = mapOf(), val dialog: Dialog? = null, ) { val progress: Int = items.count { it.value !is SearchItemResult.Loading } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt index 8c71e414c..a6de02541 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt @@ -3,14 +3,13 @@ package eu.kanade.tachiyomi.ui.deeplink import androidx.compose.runtime.Immutable import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope -import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource -import eu.kanade.domain.manga.model.toSManga import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.online.ResolvableSource import eu.kanade.tachiyomi.source.online.UriType import kotlinx.coroutines.flow.update import mihon.domain.manga.model.toDomainManga +import mihon.domain.source.interactor.UpdateMangaFromRemote import tachiyomi.core.common.util.lang.launchIO import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId import tachiyomi.domain.chapter.model.Chapter @@ -25,12 +24,12 @@ class DeepLinkScreenModel( private val sourceManager: SourceManager = Injekt.get(), private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(), - private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), + private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(), ) : StateScreenModel(State.Loading) { init { screenModelScope.launchIO { - val source = sourceManager.getCatalogueSources() + val source = sourceManager.getAll() .filterIsInstance() .firstOrNull { it.getUriType(query) != UriType.Unknown } @@ -61,13 +60,11 @@ class DeepLinkScreenModel( private suspend fun getChapterFromSChapter(sChapter: SChapter, manga: Manga, source: Source): Chapter? { val localChapter = getChapterByUrlAndMangaId.await(sChapter.url, manga.id) - return if (localChapter == null) { - val sourceChapters = source.getChapterList(manga.toSManga()) - val newChapters = syncChaptersWithSource.await(sourceChapters, manga, source, false) - newChapters.find { it.url == sChapter.url } - } else { - localChapter - } + return localChapter + ?: updateMangaFromRemote(manga, fetchChapters = true) + .getOrElse { return null } + .newChapters + .find { it.url == sChapter.url } } sealed interface State { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index 899474b0c..b8d858a5f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -16,13 +16,11 @@ import eu.kanade.core.util.addOrRemove import eu.kanade.core.util.insertSeparators import eu.kanade.domain.chapter.interactor.GetAvailableScanlators import eu.kanade.domain.chapter.interactor.SetReadStatus -import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.manga.interactor.GetExcludedScanlators import eu.kanade.domain.manga.interactor.SetExcludedScanlators import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.model.chaptersFiltered import eu.kanade.domain.manga.model.downloadedFilter -import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.domain.track.interactor.RefreshTracks import eu.kanade.domain.track.interactor.TrackChapter @@ -36,14 +34,12 @@ import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.TrackerManager -import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.util.chapter.getNextUnread import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.system.toast -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine @@ -54,13 +50,13 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import logcat.LogPriority import mihon.domain.chapter.interactor.FilterChaptersForDownload +import mihon.domain.source.interactor.UpdateMangaFromRemote import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.preference.CheckboxState import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.preference.mapAsCheckboxState import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchNonCancellable -import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.category.interactor.GetCategories @@ -111,13 +107,13 @@ class MangaScreenModel( private val setReadStatus: SetReadStatus = Injekt.get(), private val updateChapter: UpdateChapter = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(), - private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), private val getCategories: GetCategories = Injekt.get(), private val getTracks: GetTracks = Injekt.get(), private val addTracks: AddTracks = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(), private val mangaRepository: MangaRepository = Injekt.get(), private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get(), + private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(), val snackbarHostState: SnackbarHostState = SnackbarHostState(), ) : StateScreenModel(State.Loading) { @@ -237,11 +233,11 @@ class MangaScreenModel( // Fetch info-chapters when needed if (screenModelScope.isActive) { - val fetchFromSourceTasks = listOf( - async { if (needRefreshInfo) fetchMangaFromSource() }, - async { if (needRefreshChapter) fetchChaptersFromSource() }, + fetchAllFromSource( + manualFetch = false, + fetchDetails = needRefreshInfo, + fetchChapters = needRefreshChapter, ) - fetchFromSourceTasks.awaitAll() } // Initial loading finished @@ -252,38 +248,54 @@ class MangaScreenModel( fun fetchAllFromSource(manualFetch: Boolean = true) { screenModelScope.launch { updateSuccessState { it.copy(isRefreshingData = true) } - val fetchFromSourceTasks = listOf( - async { fetchMangaFromSource(manualFetch) }, - async { fetchChaptersFromSource(manualFetch) }, + fetchAllFromSource( + manualFetch = manualFetch, + fetchDetails = true, + fetchChapters = true, ) - fetchFromSourceTasks.awaitAll() updateSuccessState { it.copy(isRefreshingData = false) } } } - // Manga info - start - - /** - * Fetch manga information from source. - */ - private suspend fun fetchMangaFromSource(manualFetch: Boolean = false) { + private suspend fun fetchAllFromSource( + manualFetch: Boolean, + fetchDetails: Boolean, + fetchChapters: Boolean, + ) { val state = successState ?: return try { - withIOContext { - val networkManga = state.source.getMangaDetails(state.manga.toSManga()) - updateManga.awaitUpdateFromSource(state.manga, networkManga, manualFetch) - } - } catch (e: Throwable) { - // Ignore early hints "errors" that aren't handled by OkHttp - if (e is HttpException && e.code == 103) return + withUIContext { + val update = updateMangaFromRemote( + source = state.source, + manga = state.manga, + fetchDetails = fetchDetails, + fetchChapters = fetchChapters, + manualFetch = manualFetch, + ) + .getOrThrow() + + if (manualFetch) { + downloadNewChapters(update.newChapters) + } + } + } catch (_: CancellationException) { + // ignore + } catch (e: Exception) { + val message = if (e is NoChaptersException) { + context.stringResource(MR.strings.no_chapters_error) + } else { + logcat(LogPriority.ERROR, e) + with(context) { e.formattedMessage } + } - logcat(LogPriority.ERROR, e) screenModelScope.launch { - snackbarHostState.showSnackbar(message = with(context) { e.formattedMessage }) + snackbarHostState.showSnackbar(message = message) } } } + // Manga info - start + fun toggleFavorite() { toggleFavorite( onRemoved = { @@ -548,42 +560,6 @@ class MangaScreenModel( } } - /** - * Requests an updated list of chapters from the source. - */ - private suspend fun fetchChaptersFromSource(manualFetch: Boolean = false) { - val state = successState ?: return - try { - withIOContext { - val chapters = state.source.getChapterList(state.manga.toSManga()) - - val newChapters = syncChaptersWithSource.await( - chapters, - state.manga, - state.source, - manualFetch, - ) - - if (manualFetch) { - downloadNewChapters(newChapters) - } - } - } catch (e: Throwable) { - val message = if (e is NoChaptersException) { - context.stringResource(MR.strings.no_chapters_error) - } else { - logcat(LogPriority.ERROR, e) - with(context) { e.formattedMessage } - } - - screenModelScope.launch { - snackbarHostState.showSnackbar(message = message) - } - val newManga = mangaRepository.getMangaById(mangaId) - updateSuccessState { it.copy(manga = newManga, isRefreshingData = false) } - } - } - /** * @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled] */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt index 42773a08f..dd6e3e299 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt @@ -13,33 +13,6 @@ import uy.kohesive.injekt.api.get import java.io.InputStream import java.time.Instant -/** - * Call before updating [Manga.thumbnail_url] to ensure old cover can be cleared from cache - */ -fun Manga.prepUpdateCover(coverCache: CoverCache, remoteManga: SManga, refreshSameUrl: Boolean): Manga { - // Never refresh covers if the new url is null, as the current url has possibly become invalid - val newUrl = remoteManga.thumbnail_url ?: return this - - // Never refresh covers if the url is empty to avoid "losing" existing covers - if (newUrl.isEmpty()) return this - - if (!refreshSameUrl && thumbnailUrl == newUrl) return this - - return when { - isLocal() -> { - this.copy(coverLastModified = Instant.now().toEpochMilli()) - } - hasCustomCover(coverCache) -> { - coverCache.deleteFromCache(this, false) - this - } - else -> { - coverCache.deleteFromCache(this, false) - this.copy(coverLastModified = Instant.now().toEpochMilli()) - } - } -} - fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Manga { if (isLocal()) return this return if (coverCache.deleteFromCache(this, true) > 0) { diff --git a/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt b/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt index f5e4c792c..e272eb66c 100644 --- a/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt +++ b/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt @@ -1,9 +1,7 @@ package mihon.domain.migration.usecases -import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.model.hasCustomCover -import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.download.DownloadManager @@ -11,6 +9,7 @@ import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.TrackerManager import kotlinx.coroutines.CancellationException import mihon.domain.migration.models.MigrationFlag +import mihon.domain.source.interactor.UpdateMangaFromRemote import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId @@ -30,13 +29,13 @@ class MigrateMangaUseCase( private val downloadManager: DownloadManager, private val updateManga: UpdateManga, private val getChaptersByMangaId: GetChaptersByMangaId, - private val syncChaptersWithSource: SyncChaptersWithSource, private val updateChapter: UpdateChapter, private val getCategories: GetCategories, private val setMangaCategories: SetMangaCategories, private val getTracks: GetTracks, private val insertTrack: InsertTrack, private val coverCache: CoverCache, + private val updateMangaFromRemote: UpdateMangaFromRemote, ) { private val enhancedServices by lazy { trackerManager.trackers.filterIsInstance() } @@ -46,13 +45,7 @@ class MigrateMangaUseCase( val flags = sourcePreferences.migrationFlags.get() try { - val chapters = targetSource.getChapterList(target.toSManga()) - - try { - syncChaptersWithSource.await(chapters, target, targetSource) - } catch (_: Exception) { - // Worst case, chapters won't be synced - } + updateMangaFromRemote(target, fetchChapters = true).getOrThrow() // Update chapters read, bookmark and dateFetch if (MigrationFlag.CHAPTER in flags) { diff --git a/app/src/main/java/mihon/domain/source/interactor/UpdateMangaFromRemote.kt b/app/src/main/java/mihon/domain/source/interactor/UpdateMangaFromRemote.kt new file mode 100644 index 000000000..624770577 --- /dev/null +++ b/app/src/main/java/mihon/domain/source/interactor/UpdateMangaFromRemote.kt @@ -0,0 +1,142 @@ +package mihon.domain.source.interactor + +import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource +import eu.kanade.domain.chapter.model.toSChapter +import eu.kanade.domain.manga.model.hasCustomCover +import eu.kanade.domain.manga.model.toSManga +import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.model.SManga +import logcat.LogPriority +import mihon.domain.source.models.RemoteMangaUpdate +import tachiyomi.core.common.util.lang.withIOContext +import tachiyomi.core.common.util.system.logcat +import tachiyomi.domain.chapter.model.Chapter +import tachiyomi.domain.chapter.repository.ChapterRepository +import tachiyomi.domain.library.service.LibraryPreferences +import tachiyomi.domain.manga.model.Manga +import tachiyomi.domain.manga.model.MangaUpdate +import tachiyomi.domain.manga.repository.MangaRepository +import tachiyomi.domain.source.service.SourceManager +import tachiyomi.source.local.isLocal +import java.time.Instant + +class UpdateMangaFromRemote( + private val sourceManager: SourceManager, + private val chapterRepository: ChapterRepository, + private val mangaRepository: MangaRepository, + private val syncChaptersWithSource: SyncChaptersWithSource, + private val coverCache: CoverCache, + private val libraryPreferences: LibraryPreferences, + private val downloadManager: DownloadManager, +) { + suspend operator fun invoke( + manga: Manga, + fetchDetails: Boolean = false, + fetchChapters: Boolean = false, + manualFetch: Boolean = false, + fetchWindow: Pair = Pair(0, 0), + ): Result { + val source = sourceManager.getOrStub(manga.source) + return invoke( + source = source, + manga = manga, + fetchDetails = fetchDetails, + fetchChapters = fetchChapters, + manualFetch = manualFetch, + ) + } + + suspend operator fun invoke( + source: Source, + manga: Manga, + fetchDetails: Boolean = false, + fetchChapters: Boolean = false, + manualFetch: Boolean = false, + fetchWindow: Pair = Pair(0, 0), + ): Result { + return try { + val chapters = chapterRepository.getChapterByMangaId(manga.id) + val update = withIOContext { + source.getMangaUpdate( + manga = manga.toSManga(), + chapters = chapters.map(Chapter::toSChapter), + fetchDetails = fetchDetails, + fetchChapters = fetchChapters, + ) + } + awaitUpdateFromSource(manga, update.manga, manualFetch) + val newChapters = syncChaptersWithSource.await( + rawSourceChapters = update.chapters, + manga = manga, + source = source, + manualFetch = manualFetch, + fetchWindow = fetchWindow, + ) + val updatedManga = mangaRepository.getMangaById(manga.id) + + Result.success(RemoteMangaUpdate(manga = updatedManga, newChapters = newChapters)) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + Result.failure(e) + } + } + + private suspend fun awaitUpdateFromSource( + localManga: Manga, + remoteManga: SManga, + manualFetch: Boolean, + ): Boolean { + val remoteTitle = try { + remoteManga.title + } catch (_: UninitializedPropertyAccessException) { + "" + } + + // if the manga isn't a favorite (or 'update titles' preference is enabled), set its title from source and update in db + val title = + if (remoteTitle.isNotEmpty() && (!localManga.favorite || libraryPreferences.updateMangaTitles.get())) { + remoteTitle + } else { + null + } + + val coverLastModified = when { + // Never refresh covers if the url is empty to avoid "losing" existing covers + remoteManga.thumbnail_url.isNullOrEmpty() -> null + !manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null + localManga.isLocal() -> Instant.now().toEpochMilli() + localManga.hasCustomCover(coverCache) -> { + coverCache.deleteFromCache(localManga, false) + null + } + else -> { + coverCache.deleteFromCache(localManga, false) + Instant.now().toEpochMilli() + } + } + + val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() } + + val success = mangaRepository.update( + MangaUpdate( + id = localManga.id, + title = title, + coverLastModified = coverLastModified, + author = remoteManga.author, + artist = remoteManga.artist, + description = remoteManga.description, + genre = remoteManga.getGenres(), + thumbnailUrl = thumbnailUrl, + status = remoteManga.status.toLong(), + updateStrategy = remoteManga.update_strategy, + initialized = true, + ), + ) + if (success && title != null) { + downloadManager.renameManga(localManga, title) + } + return success + } +} diff --git a/app/src/main/java/mihon/domain/source/models/RemoteMangaUpdate.kt b/app/src/main/java/mihon/domain/source/models/RemoteMangaUpdate.kt new file mode 100644 index 000000000..50f34f6c5 --- /dev/null +++ b/app/src/main/java/mihon/domain/source/models/RemoteMangaUpdate.kt @@ -0,0 +1,9 @@ +package mihon.domain.source.models + +import tachiyomi.domain.chapter.model.Chapter +import tachiyomi.domain.manga.model.Manga + +data class RemoteMangaUpdate( + val manga: Manga, + val newChapters: List, +) diff --git a/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt b/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt index fb445b8ae..d8790d44d 100644 --- a/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt +++ b/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt @@ -344,7 +344,7 @@ class MigrationConfigScreen(private val mangaIds: Collection) : Screen() { val includedSources = sourcePreferences.migrationSources.get() val disabledSources = sourcePreferences.disabledSources.get() .mapNotNull { it.toLongOrNull() } - val sources = sourceManager.getCatalogueSources() + val sources = sourceManager.getAll() .asSequence() .filterIsInstance() .filter { it.lang in languages } diff --git a/app/src/main/java/mihon/feature/migration/list/MigrationListScreenModel.kt b/app/src/main/java/mihon/feature/migration/list/MigrationListScreenModel.kt index 1198dc3dc..301014d87 100644 --- a/app/src/main/java/mihon/feature/migration/list/MigrationListScreenModel.kt +++ b/app/src/main/java/mihon/feature/migration/list/MigrationListScreenModel.kt @@ -4,10 +4,8 @@ import androidx.annotation.FloatRange import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource -import eu.kanade.domain.manga.interactor.UpdateManga -import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.source.service.SourcePreferences -import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.getNameForMangaInfo import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job @@ -24,6 +22,7 @@ import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import logcat.LogPriority import mihon.domain.migration.usecases.MigrateMangaUseCase +import mihon.domain.source.interactor.UpdateMangaFromRemote import mihon.feature.migration.list.models.MigratingManga import mihon.feature.migration.list.models.MigratingManga.SearchResult import mihon.feature.migration.list.search.SmartSourceSearchEngine @@ -45,10 +44,10 @@ class MigrationListScreenModel( private val sourceManager: SourceManager = Injekt.get(), private val getManga: GetManga = Injekt.get(), private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), - private val updateManga: UpdateManga = Injekt.get(), private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), private val migrateManga: MigrateMangaUseCase = Injekt.get(), + private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(), ) : StateScreenModel(State()) { private val smartSearchEngine = SmartSourceSearchEngine(extraSearchQuery) @@ -110,7 +109,7 @@ class MigrationListScreenModel( val deepSearchMode = preferences.migrationDeepSearchMode.get() val sources = preferences.migrationSources.get() - .mapNotNull { sourceManager.get(it) as? CatalogueSource } + .mapNotNull { sourceManager.get(it) } for (manga in mangas) { if (!currentCoroutineContext().isActive) break @@ -148,8 +147,7 @@ class MigrationListScreenModel( if (result != null && result.first.thumbnailUrl == null) { try { - val newManga = sourceManager.getOrStub(result.first.source).getMangaDetails(result.first.toSManga()) - updateManga.awaitUpdateFromSource(result.first, newManga, true) + updateMangaFromRemote(result.first, fetchDetails = true, manualFetch = true).getOrThrow().manga } catch (e: CancellationException) { throw e } catch (_: Exception) { @@ -174,7 +172,7 @@ class MigrationListScreenModel( private suspend fun searchSource( manga: Manga, - source: CatalogueSource, + source: Source, deepSearchMode: Boolean, ): Pair? { return try { @@ -188,8 +186,7 @@ class MigrationListScreenModel( val localManga = networkToLocalManga(searchResult) try { - val chapters = source.getChapterList(localManga.toSManga()) - syncChaptersWithSource.await(chapters, localManga, source) + updateMangaFromRemote(localManga, fetchChapters = true).getOrThrow() } catch (e: Exception) { logcat(LogPriority.ERROR, e) } @@ -224,12 +221,10 @@ class MigrationListScreenModel( val manga = getManga.await(target) ?: return@async null try { val source = sourceManager.get(manga.source)!! - val chapters = source.getChapterList(manga.toSManga()) - syncChaptersWithSource.await(chapters, manga, source) + updateMangaFromRemote(source = source, manga = manga, fetchChapters = true).getOrThrow().manga } catch (_: Exception) { - return@async null + null } - manga } .await() @@ -239,13 +234,6 @@ class MigrationListScreenModel( return@launchIO } - try { - val newManga = sourceManager.getOrStub(result.source).getMangaDetails(result.toSManga()) - updateManga.awaitUpdateFromSource(result, newManga, true) - } catch (e: CancellationException) { - throw e - } catch (_: Exception) { - } migratingManga.searchResult.value = result.toSuccessSearchResult() updateMigrationProgress() } diff --git a/app/src/main/java/mihon/feature/migration/list/search/SmartSourceSearchEngine.kt b/app/src/main/java/mihon/feature/migration/list/search/SmartSourceSearchEngine.kt index 8a801b2f1..19771c408 100644 --- a/app/src/main/java/mihon/feature/migration/list/search/SmartSourceSearchEngine.kt +++ b/app/src/main/java/mihon/feature/migration/list/search/SmartSourceSearchEngine.kt @@ -1,6 +1,6 @@ package mihon.feature.migration.list.search -import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.SManga import mihon.domain.manga.model.toDomainManga import tachiyomi.domain.manga.model.Manga @@ -9,19 +9,19 @@ class SmartSourceSearchEngine(extraSearchParams: String?) : BaseSmartSearchEngin override fun getTitle(result: SManga) = result.title - suspend fun regularSearch(source: CatalogueSource, title: String): Manga? { + suspend fun regularSearch(source: Source, title: String): Manga? { return regularSearch(makeSearchAction(source), title).let { it?.toDomainManga(source.id) } } - suspend fun deepSearch(source: CatalogueSource, title: String): Manga? { + suspend fun deepSearch(source: Source, title: String): Manga? { return deepSearch(makeSearchAction(source), title).let { it?.toDomainManga(source.id) } } - private fun makeSearchAction(source: CatalogueSource): SearchAction = { query -> + private fun makeSearchAction(source: Source): SearchAction = { query -> source.getSearchManga(1, query, source.getFilterList()).mangas } } diff --git a/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt b/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt index bd34a91b5..425706741 100644 --- a/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt +++ b/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt @@ -1,7 +1,7 @@ package tachiyomi.data.source import androidx.paging.PagingState -import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import mihon.domain.manga.model.toDomainManga @@ -13,7 +13,7 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class SourceSearchPagingSource( - source: CatalogueSource, + source: Source, private val query: String, private val filters: FilterList, ) : BaseSourcePagingSource(source) { @@ -22,20 +22,20 @@ class SourceSearchPagingSource( } } -class SourcePopularPagingSource(source: CatalogueSource) : BaseSourcePagingSource(source) { +class SourcePopularPagingSource(source: Source) : BaseSourcePagingSource(source) { override suspend fun requestNextPage(currentPage: Int): MangasPage { return source.getPopularManga(currentPage) } } -class SourceLatestPagingSource(source: CatalogueSource) : BaseSourcePagingSource(source) { +class SourceLatestPagingSource(source: Source) : BaseSourcePagingSource(source) { override suspend fun requestNextPage(currentPage: Int): MangasPage { return source.getLatestUpdates(currentPage) } } abstract class BaseSourcePagingSource( - protected val source: CatalogueSource, + protected val source: Source, private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), ) : SourcePagingSource() { diff --git a/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt index 3a677ae28..7decf8f7c 100644 --- a/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt @@ -1,6 +1,5 @@ package tachiyomi.data.source -import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.online.HttpSource @@ -22,7 +21,7 @@ class SourceRepositoryImpl( ) : SourceRepository { override fun getSources(): Flow> { - return sourceManager.catalogueSources.map { sources -> + return sourceManager.sources.map { sources -> sources.map { mapSourceToDomainSource(it).copy( supportsLatest = it.supportsLatest, @@ -32,7 +31,7 @@ class SourceRepositoryImpl( } override fun getOnlineSources(): Flow> { - return sourceManager.catalogueSources.map { sources -> + return sourceManager.sources.map { sources -> sources .filterIsInstance() .map(::mapSourceToDomainSource) @@ -43,7 +42,7 @@ class SourceRepositoryImpl( val sourceIdWithFavoriteCountFlow = database.mangasQueries .getSourceIdWithFavoriteCount() .subscribeToList() - return combine(sourceIdWithFavoriteCountFlow, sourceManager.catalogueSources) { sourceIdWithFavoriteCount, _ -> + return combine(sourceIdWithFavoriteCountFlow, sourceManager.sources) { sourceIdWithFavoriteCount, _ -> sourceIdWithFavoriteCount } .map { @@ -77,18 +76,15 @@ class SourceRepositoryImpl( query: String, filterList: FilterList, ): SourcePagingSource { - val source = sourceManager.get(sourceId) as CatalogueSource - return SourceSearchPagingSource(source, query, filterList) + return SourceSearchPagingSource(sourceManager.getOrStub(sourceId), query, filterList) } override fun getPopular(sourceId: Long): SourcePagingSource { - val source = sourceManager.get(sourceId) as CatalogueSource - return SourcePopularPagingSource(source) + return SourcePopularPagingSource(sourceManager.getOrStub(sourceId)) } override fun getLatest(sourceId: Long): SourcePagingSource { - val source = sourceManager.get(sourceId) as CatalogueSource - return SourceLatestPagingSource(source) + return SourceLatestPagingSource(sourceManager.getOrStub(sourceId)) } private fun mapSourceToDomainSource(source: Source): DomainSource = DomainSource( diff --git a/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt b/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt index 0fee2af18..76ada5b60 100644 --- a/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt +++ b/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt @@ -1,9 +1,12 @@ package tachiyomi.domain.source.model import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.model.SMangaUpdate class StubSource( override val id: Long, @@ -13,11 +16,22 @@ class StubSource( private val isInvalid: Boolean = name.isBlank() || lang.isBlank() - override suspend fun getMangaDetails(manga: SManga): SManga = + override val supportsLatest: Boolean = false + + override suspend fun getPopularManga(page: Int): MangasPage = throw SourceNotInstalledException() + + override suspend fun getLatestUpdates(page: Int): MangasPage = throw SourceNotInstalledException() + + override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage = throw SourceNotInstalledException() - override suspend fun getChapterList(manga: SManga): List = - throw SourceNotInstalledException() + override suspend fun getMangaUpdate( + manga: SManga, + chapters: List, + fetchDetails: Boolean, + fetchChapters: Boolean, + ): SMangaUpdate = throw SourceNotInstalledException() + override suspend fun getPageList(chapter: SChapter): List = throw SourceNotInstalledException() diff --git a/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt b/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt index da4fb3929..554ccf0fd 100644 --- a/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt +++ b/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt @@ -1,6 +1,5 @@ package tachiyomi.domain.source.service -import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.online.HttpSource import kotlinx.coroutines.flow.Flow @@ -11,15 +10,15 @@ interface SourceManager { val isInitialized: StateFlow - val catalogueSources: Flow> + val sources: Flow> fun get(sourceKey: Long): Source? fun getOrStub(sourceKey: Long): Source - fun getOnlineSources(): List + fun getAll(): List - fun getCatalogueSources(): List + fun getOnlineSources(): List fun getStubSources(): List } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt index 9be673541..0aa0de3c4 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt @@ -2,6 +2,12 @@ package eu.kanade.tachiyomi.source import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.model.SMangaUpdate +import kotlinx.coroutines.async +import kotlinx.coroutines.supervisorScope import rx.Observable import tachiyomi.core.common.util.lang.awaitSingle @@ -12,69 +18,61 @@ interface CatalogueSource : Source { */ override val lang: String - /** - * Whether the source has support for latest updates. - */ - val supportsLatest: Boolean - - /** - * Get a page with a list of manga. - * - * @since extensions-lib 1.5 - * @param page the page number to retrieve. - */ @Suppress("DEPRECATION") - suspend fun getPopularManga(page: Int): MangasPage { - return fetchPopularManga(page).awaitSingle() + override suspend fun getPopularManga(page: Int): MangasPage = fetchPopularManga(page).awaitSingle() + + @Suppress("DEPRECATION") + override suspend fun getLatestUpdates(page: Int): MangasPage = fetchLatestUpdates(page).awaitSingle() + + @Suppress("DEPRECATION") + override suspend fun getSearchManga( + page: Int, + query: String, + filters: FilterList, + ): MangasPage = fetchSearchManga(page, query, filters).awaitSingle() + + @Suppress("DEPRECATION") + override suspend fun getMangaUpdate( + manga: SManga, + chapters: List, + fetchDetails: Boolean, + fetchChapters: Boolean, + ): SMangaUpdate = supervisorScope { + val asyncManga = if (fetchDetails) async { fetchMangaDetails(manga).awaitSingle() } else null + val asyncChapters = if (fetchChapters) async { fetchChapterList(manga).awaitSingle() } else null + SMangaUpdate(asyncManga?.await() ?: manga, asyncChapters?.await() ?: chapters) } + @Suppress("DEPRECATION") + override suspend fun getPageList(chapter: SChapter): List = fetchPageList(chapter).awaitSingle() + /** - * Get a page with a list of manga. + * Returns an observable containing a page with a list of manga. + * + * @param page the page number to retrieve. + */ + @Deprecated("Use the suspend API instead", ReplaceWith("getPopularManga")) + fun fetchPopularManga(page: Int): Observable = throw UnsupportedOperationException() + + /** + * Returns an observable containing a page with a list of manga. * - * @since extensions-lib 1.5 * @param page the page number to retrieve. * @param query the search query. * @param filters the list of filters to apply. */ - @Suppress("DEPRECATION") - suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { - return fetchSearchManga(page, query, filters).awaitSingle() - } + @Deprecated("Use the suspend API instead", ReplaceWith("getSearchManga")) + fun fetchSearchManga( + page: Int, + query: String, + filters: FilterList, + ): Observable = throw UnsupportedOperationException() /** - * Get a page with a list of latest manga updates. + * Returns an observable containing a page with a list of latest manga updates. * - * @since extensions-lib 1.5 * @param page the page number to retrieve. */ - @Suppress("DEPRECATION") - suspend fun getLatestUpdates(page: Int): MangasPage { - return fetchLatestUpdates(page).awaitSingle() - } - - /** - * Returns the list of filters for the source. - */ - fun getFilterList(): FilterList - - @Deprecated( - "Use the non-RxJava API instead", - ReplaceWith("getPopularManga"), - ) - fun fetchPopularManga(page: Int): Observable = - throw IllegalStateException("Not used") - - @Deprecated( - "Use the non-RxJava API instead", - ReplaceWith("getSearchManga"), - ) - fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = - throw IllegalStateException("Not used") - - @Deprecated( - "Use the non-RxJava API instead", - ReplaceWith("getLatestUpdates"), - ) - fun fetchLatestUpdates(page: Int): Observable = - throw IllegalStateException("Not used") + @Deprecated("Use the suspend API instead", ReplaceWith("getLatestUpdates")) + fun fetchLatestUpdates(page: Int): Observable = throw UnsupportedOperationException() } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt index 83fcd7962..708012a8f 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt @@ -1,13 +1,15 @@ package eu.kanade.tachiyomi.source +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.util.awaitSingle +import eu.kanade.tachiyomi.source.model.SMangaUpdate import rx.Observable /** - * A basic interface for creating a source. It could be an online source, a local source, etc. + * A basic interface for creating a source. It could be an online source, a local source, etc... */ interface Source { @@ -25,60 +27,80 @@ interface Source { get() = "" /** - * Get the updated details for a manga. - * - * @since extensions-lib 1.5 - * @param manga the manga to update. - * @return the updated manga. + * Whether the source has support for latest updates. */ - @Suppress("DEPRECATION") - suspend fun getMangaDetails(manga: SManga): SManga { - return fetchMangaDetails(manga).awaitSingle() - } + val supportsLatest: Boolean /** - * Get all the available chapters for a manga. - * - * @since extensions-lib 1.5 - * @param manga the manga to update. - * @return the chapters for the manga. + * Returns the list of filters for the source. */ - @Suppress("DEPRECATION") - suspend fun getChapterList(manga: SManga): List { - return fetchChapterList(manga).awaitSingle() - } + fun getFilterList(): FilterList = FilterList() + + /** + * Get a page with a list of manga. + * + * @since tachiyomix 1.6 + * @param page the page number to retrieve. + */ + suspend fun getPopularManga(page: Int): MangasPage + + /** + * Get a page with a list of latest manga updates. + * + * @since tachiyomix 1.6 + * @param page the page number to retrieve. + */ + suspend fun getLatestUpdates(page: Int): MangasPage + + /** + * Get a page with a list of manga. + * + * @since tachiyomix 1.6 + * @param page the page number to retrieve. + * @param query the search query. + * @param filters the list of filters to apply. + */ + suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage + + /** + * Fetches updated information for a manga. + * + * Depending on the provided flags or source availability, this may include + * updated manga metadata, available chapters, or both. + * + * If a value is not requested, the existing provided value can be returned as-is. + * The host app may apply any returned updates regardless of the flags, + * so care should be taken to only return accurate and intentional changes. + * + * @since tachiyomix 1.6 + * @param manga The manga to fetch updates for. + * @param chapters Existing chapters of the manga + * @param fetchDetails Whether to fetch updated manga details. + * @param fetchChapters Whether to fetch available chapters. + */ + suspend fun getMangaUpdate( + manga: SManga, + chapters: List, + fetchDetails: Boolean, + fetchChapters: Boolean, + ): SMangaUpdate /** * Get the list of pages a chapter has. Pages should be returned * in the expected order; the index is ignored. * - * @since extensions-lib 1.5 + * @since tachiyomix 1.6 * @param chapter the chapter. * @return the pages for the chapter. */ - @Suppress("DEPRECATION") - suspend fun getPageList(chapter: SChapter): List { - return fetchPageList(chapter).awaitSingle() - } + suspend fun getPageList(chapter: SChapter): List - @Deprecated( - "Use the non-RxJava API instead", - ReplaceWith("getMangaDetails"), - ) - fun fetchMangaDetails(manga: SManga): Observable = - throw IllegalStateException("Not used") + @Deprecated("Use the combined suspend API instead", ReplaceWith("getMangaUpdate")) + fun fetchMangaDetails(manga: SManga): Observable = throw UnsupportedOperationException() - @Deprecated( - "Use the non-RxJava API instead", - ReplaceWith("getChapterList"), - ) - fun fetchChapterList(manga: SManga): Observable> = - throw IllegalStateException("Not used") + @Deprecated("Use the combined suspend API instead", ReplaceWith("getMangaUpdate")) + fun fetchChapterList(manga: SManga): Observable> = throw UnsupportedOperationException() - @Deprecated( - "Use the non-RxJava API instead", - ReplaceWith("getPageList"), - ) - fun fetchPageList(chapter: SChapter): Observable> = - throw IllegalStateException("Not used") + @Deprecated("Use the suspend API instead", ReplaceWith("getPageList")) + fun fetchPageList(chapter: SChapter): Observable> = throw UnsupportedOperationException() } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SMangaUpdate.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SMangaUpdate.kt new file mode 100644 index 000000000..2b35cf6a3 --- /dev/null +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SMangaUpdate.kt @@ -0,0 +1,4 @@ +package eu.kanade.tachiyomi.source.model + +@Suppress("UNUSED") +class SMangaUpdate(val manga: SManga, val chapters: List) diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt index dabcba678..73817cd4d 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -25,7 +25,6 @@ import java.security.MessageDigest /** * A simple implementation for sources from a website. */ -@Suppress("unused") abstract class HttpSource : CatalogueSource { /** @@ -38,11 +37,24 @@ abstract class HttpSource : CatalogueSource { */ abstract val baseUrl: String + /** + * Returns the base (home) URL of the website as a string. + * + * This is typically the root address that serves as the main entry point + * to the site's content, such as "https://mihon.tech". + * + * This method is used in the browse screen to determine the URL + * opened when tapping "Open in WebView". + * + * @return The website’s home page URL. Defaults to [baseUrl]. + */ + open fun getHomeUrl(): String = baseUrl + /** * Version id used to generate the source id. If the site completely changes and urls are * incompatible, you may increase this value and it'll be considered as a new source. */ - open val versionId = 1 + open val versionId: Int = 1 /** * ID of the source. By default it uses a generated id using the first 16 characters (64 bits) @@ -54,7 +66,7 @@ abstract class HttpSource : CatalogueSource { * * Note: the generated ID sets the sign bit to `0`. */ - override val id by lazy { generateId(name, lang, versionId) } + override val id: Long by lazy { generateId(name, lang, versionId) } /** * Headers used for requests. @@ -64,8 +76,7 @@ abstract class HttpSource : CatalogueSource { /** * Default network client for doing requests. */ - open val client: OkHttpClient - get() = network.client + open val client: OkHttpClient get() = network.client /** * Generates a unique ID for the source based on the provided [name], [lang] and @@ -93,14 +104,14 @@ abstract class HttpSource : CatalogueSource { /** * Headers builder for requests. Implementations can override this method for custom headers. */ - protected open fun headersBuilder() = Headers.Builder().apply { + protected open fun headersBuilder(): Headers.Builder = Headers.Builder().apply { add("User-Agent", network.defaultUserAgentProvider()) } /** * Visible name of the source. */ - override fun toString() = "$name (${lang.uppercase()})" + override fun toString(): String = "$name (${lang.uppercase()})" /** * Returns an observable containing a page with a list of manga. Normally it's not needed to @@ -109,7 +120,7 @@ abstract class HttpSource : CatalogueSource { * @param page the page number to retrieve. */ @Suppress("DEPRECATION") - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga")) + @Deprecated("Use the suspend API instead", ReplaceWith("getPopularManga")) override fun fetchPopularManga(page: Int): Observable { return client.newCall(popularMangaRequest(page)) .asObservableSuccess() @@ -123,14 +134,22 @@ abstract class HttpSource : CatalogueSource { * * @param page the page number to retrieve. */ - protected abstract fun popularMangaRequest(page: Int): Request + @Deprecated( + message = "The helper functions are inherently limiting and hides the underlying implementation. " + + "Source developers should make their own implementation according to their needs.", + ) + protected open fun popularMangaRequest(page: Int): Request = throw UnsupportedOperationException() /** * Parses the response from the site and returns a [MangasPage] object. * * @param response the response from the site. */ - protected abstract fun popularMangaParse(response: Response): MangasPage + @Deprecated( + message = "The helper functions are inherently limiting and hides the underlying implementation. " + + "Source developers should make their own implementation according to their needs.", + ) + protected open fun popularMangaParse(response: Response): MangasPage = throw UnsupportedOperationException() /** * Returns an observable containing a page with a list of manga. Normally it's not needed to @@ -141,21 +160,10 @@ abstract class HttpSource : CatalogueSource { * @param filters the list of filters to apply. */ @Suppress("DEPRECATION") - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga")) - override fun fetchSearchManga( - page: Int, - query: String, - filters: FilterList, - ): Observable { - return Observable.defer { - try { - client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess() - } catch (e: NoClassDefFoundError) { - // RxJava doesn't handle Errors, which tends to happen during global searches - // if an old extension using non-existent classes is still around - throw RuntimeException(e) - } - } + @Deprecated("Use the suspend API instead", ReplaceWith("getSearchManga")) + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return client.newCall(searchMangaRequest(page, query, filters)) + .asObservableSuccess() .map { response -> searchMangaParse(response) } @@ -168,18 +176,26 @@ abstract class HttpSource : CatalogueSource { * @param query the search query. * @param filters the list of filters to apply. */ - protected abstract fun searchMangaRequest( + @Deprecated( + message = "The helper functions are inherently limiting and hides the underlying implementation. " + + "Source developers should make their own implementation according to their needs.", + ) + protected open fun searchMangaRequest( page: Int, query: String, filters: FilterList, - ): Request + ): Request = throw UnsupportedOperationException() /** * Parses the response from the site and returns a [MangasPage] object. * * @param response the response from the site. */ - protected abstract fun searchMangaParse(response: Response): MangasPage + @Deprecated( + message = "The helper functions are inherently limiting and hides the underlying implementation. " + + "Source developers should make their own implementation according to their needs.", + ) + protected open fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException() /** * Returns an observable containing a page with a list of latest manga updates. @@ -187,7 +203,7 @@ abstract class HttpSource : CatalogueSource { * @param page the page number to retrieve. */ @Suppress("DEPRECATION") - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates")) + @Deprecated("Use the suspend API instead", ReplaceWith("getLatestUpdates")) override fun fetchLatestUpdates(page: Int): Observable { return client.newCall(latestUpdatesRequest(page)) .asObservableSuccess() @@ -201,29 +217,31 @@ abstract class HttpSource : CatalogueSource { * * @param page the page number to retrieve. */ - protected abstract fun latestUpdatesRequest(page: Int): Request + @Deprecated( + message = "The helper functions are inherently limiting and hides the underlying implementation. " + + "Source developers should make their own implementation according to their needs.", + ) + protected open fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException() /** * Parses the response from the site and returns a [MangasPage] object. * * @param response the response from the site. */ - protected abstract fun latestUpdatesParse(response: Response): MangasPage + @Deprecated( + message = "The helper functions are inherently limiting and hides the underlying implementation. " + + "Source developers should make their own implementation according to their needs.", + ) + protected open fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException() /** - * Get the updated details for a manga. - * Normally it's not needed to override this method. + * Returns an observable with the updated details for a manga. Normally it's not needed to + * override this method. * - * @param manga the manga to update. - * @return the updated manga. + * @param manga the manga to be updated. */ @Suppress("DEPRECATION") - override suspend fun getMangaDetails(manga: SManga): SManga { - return fetchMangaDetails(manga).awaitSingle() - } - - @Suppress("DEPRECATION") - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails")) + @Deprecated("Use the combined suspend API instead", replaceWith = ReplaceWith("getMangaUpdate")) override fun fetchMangaDetails(manga: SManga): Observable { return client.newCall(mangaDetailsRequest(manga)) .asObservableSuccess() @@ -238,6 +256,10 @@ abstract class HttpSource : CatalogueSource { * * @param manga the manga to be updated. */ + @Deprecated( + message = "The helper functions are inherently limiting and hides the underlying implementation. " + + "Source developers should make their own implementation according to their needs.", + ) open fun mangaDetailsRequest(manga: SManga): Request { return GET(baseUrl + manga.url, headers) } @@ -247,22 +269,20 @@ abstract class HttpSource : CatalogueSource { * * @param response the response from the site. */ - protected abstract fun mangaDetailsParse(response: Response): SManga + @Deprecated( + message = "The helper functions are inherently limiting and hides the underlying implementation. " + + "Source developers should make their own implementation according to their needs.", + ) + protected open fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException() /** - * Get all the available chapters for a manga. - * Normally it's not needed to override this method. + * Returns an observable with the updated chapter list for a manga. Normally it's not needed to + * override this method. * - * @param manga the manga to update. - * @return the chapters for the manga. + * @param manga the manga to look for chapters. */ @Suppress("DEPRECATION") - override suspend fun getChapterList(manga: SManga): List { - return fetchChapterList(manga).awaitSingle() - } - - @Suppress("DEPRECATION") - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList")) + @Deprecated("Use the combined suspend API instead", replaceWith = ReplaceWith("getMangaUpdate")) override fun fetchChapterList(manga: SManga): Observable> { return client.newCall(chapterListRequest(manga)) .asObservableSuccess() @@ -277,6 +297,10 @@ abstract class HttpSource : CatalogueSource { * * @param manga the manga to look for chapters. */ + @Deprecated( + message = "The helper functions are inherently limiting and hides the underlying implementation. " + + "Source developers should make their own implementation according to their needs.", + ) protected open fun chapterListRequest(manga: SManga): Request { return GET(baseUrl + manga.url, headers) } @@ -286,29 +310,19 @@ abstract class HttpSource : CatalogueSource { * * @param response the response from the site. */ - protected abstract fun chapterListParse(response: Response): List + @Deprecated( + message = "The helper functions are inherently limiting and hides the underlying implementation. " + + "Source developers should make their own implementation according to their needs.", + ) + protected open fun chapterListParse(response: Response): List = throw UnsupportedOperationException() /** - * Parses the response from the site and returns a SChapter Object. + * Returns an observable with the page list for a chapter. * - * @param response the response from the site. - */ - protected abstract fun chapterPageParse(response: Response): SChapter - - /** - * Get the list of pages a chapter has. Pages should be returned - * in the expected order; the index is ignored. - * - * @param chapter the chapter. - * @return the pages for the chapter. + * @param chapter the chapter whose page list has to be fetched. */ @Suppress("DEPRECATION") - override suspend fun getPageList(chapter: SChapter): List { - return fetchPageList(chapter).awaitSingle() - } - - @Suppress("DEPRECATION") - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList")) + @Deprecated("Use the suspend API instead", ReplaceWith("getPageList")) override fun fetchPageList(chapter: SChapter): Observable> { return client.newCall(pageListRequest(chapter)) .asObservableSuccess() @@ -323,6 +337,10 @@ abstract class HttpSource : CatalogueSource { * * @param chapter the chapter whose page list has to be fetched. */ + @Deprecated( + message = "The helper functions are inherently limiting and hides the underlying implementation. " + + "Source developers should make their own implementation according to their needs.", + ) protected open fun pageListRequest(chapter: SChapter): Request { return GET(baseUrl + chapter.url, headers) } @@ -332,34 +350,45 @@ abstract class HttpSource : CatalogueSource { * * @param response the response from the site. */ - protected abstract fun pageListParse(response: Response): List + @Deprecated( + message = "The helper functions are inherently limiting and hides the underlying implementation. " + + "Source developers should make their own implementation according to their needs.", + ) + protected open fun pageListParse(response: Response): List = throw UnsupportedOperationException() /** * Returns an observable with the page containing the source url of the image. If there's any * error, it will return null instead of throwing an exception. * - * @since extensions-lib 1.5 * @param page the page whose source image has to be fetched. */ @Suppress("DEPRECATION") - open suspend fun getImageUrl(page: Page): String { - return fetchImageUrl(page).awaitSingle() - } - - @Suppress("DEPRECATION") - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl")) + @Deprecated("Use the suspend API instead", ReplaceWith("getImageUrl")) open fun fetchImageUrl(page: Page): Observable { return client.newCall(imageUrlRequest(page)) .asObservableSuccess() .map { imageUrlParse(it) } } + /** + * Returns the image url for the provided [page]. The function is only called if [Page.imageUrl] is null. + * + * @since tachiyomix 1.6 + * @param page the page whose source image has to be fetched. + */ + @Suppress("DEPRECATION") + open suspend fun getImageUrl(page: Page): String = fetchImageUrl(page).awaitSingle() + /** * Returns the request for getting the url to the source image. Override only if it's needed to * override the url, send different headers or request method like POST. * * @param page the chapter whose page list has to be fetched */ + @Deprecated( + message = "The helper functions are inherently limiting and hides the underlying implementation. " + + "Source developers should make their own implementation according to their needs.", + ) protected open fun imageUrlRequest(page: Page): Request { return GET(page.url, headers) } @@ -369,16 +398,13 @@ abstract class HttpSource : CatalogueSource { * * @param response the response from the site. */ - protected abstract fun imageUrlParse(response: Response): String + @Deprecated( + message = "The helper functions are inherently limiting and hides the underlying implementation. " + + "Source developers should make their own implementation according to their needs.", + ) + protected open fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() - /** - * Returns the response of the source image. - * Typically does not need to be overridden. - * - * @since extensions-lib 1.5 - * @param page the page whose source image has to be downloaded. - */ - open suspend fun getImage(page: Page): Response { + suspend fun getImage(page: Page): Response { return client.newCachelessCallWithProgress(imageRequest(page), page) .awaitSuccess() } @@ -399,6 +425,7 @@ abstract class HttpSource : CatalogueSource { * * @param url the full url to the chapter. */ + @Suppress("Unused") fun SChapter.setUrlWithoutDomain(url: String) { this.url = getUrlWithoutDomain(url) } @@ -409,6 +436,7 @@ abstract class HttpSource : CatalogueSource { * * @param url the full url to the manga. */ + @Suppress("Unused") fun SManga.setUrlWithoutDomain(url: String) { this.url = getUrlWithoutDomain(url) } @@ -429,7 +457,7 @@ abstract class HttpSource : CatalogueSource { out += "#" + uri.fragment } out - } catch (e: URISyntaxException) { + } catch (_: URISyntaxException) { orig } } @@ -441,6 +469,7 @@ abstract class HttpSource : CatalogueSource { * @param manga the manga * @return url of the manga */ + @Suppress("DEPRECATION") open fun getMangaUrl(manga: SManga): String { return mangaDetailsRequest(manga).url.toString() } @@ -452,6 +481,7 @@ abstract class HttpSource : CatalogueSource { * @param chapter the chapter * @return url of the chapter */ + @Suppress("DEPRECATION") open fun getChapterUrl(chapter: SChapter): String { return pageListRequest(chapter).url.toString() } @@ -463,10 +493,6 @@ abstract class HttpSource : CatalogueSource { * @param chapter the chapter to be added. * @param manga the manga of the chapter. */ + @Deprecated("All modifications should be done when constructing the chapter") open fun prepareNewChapter(chapter: SChapter, manga: SManga) {} - - /** - * Returns the list of filters for the source. - */ - override fun getFilterList() = FilterList() } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt index 34376f847..f69bb4dd1 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt @@ -12,7 +12,10 @@ import org.jsoup.nodes.Element /** * A simple implementation for sources from a website using Jsoup, an HTML parser. */ -@Suppress("unused") +@Deprecated( + message = "In most cases sources only require a subset of the methods from this class. " + + "Source developers should make their own implementation according to their needs.", +) abstract class ParsedHttpSource : HttpSource() { /** @@ -20,6 +23,9 @@ abstract class ParsedHttpSource : HttpSource() { * * @param response the response from the site. */ + @Deprecated( + "The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.", + ) override fun popularMangaParse(response: Response): MangasPage { val document = response.asJsoup() @@ -58,6 +64,9 @@ abstract class ParsedHttpSource : HttpSource() { * * @param response the response from the site. */ + @Deprecated( + "The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.", + ) override fun searchMangaParse(response: Response): MangasPage { val document = response.asJsoup() @@ -96,6 +105,9 @@ abstract class ParsedHttpSource : HttpSource() { * * @param response the response from the site. */ + @Deprecated( + "The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.", + ) override fun latestUpdatesParse(response: Response): MangasPage { val document = response.asJsoup() @@ -134,6 +146,9 @@ abstract class ParsedHttpSource : HttpSource() { * * @param response the response from the site. */ + @Deprecated( + "The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.", + ) override fun mangaDetailsParse(response: Response): SManga { return mangaDetailsParse(response.asJsoup()) } @@ -150,6 +165,9 @@ abstract class ParsedHttpSource : HttpSource() { * * @param response the response from the site. */ + @Deprecated( + "The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.", + ) override fun chapterListParse(response: Response): List { val document = response.asJsoup() return document.select(chapterListSelector()).map { chapterFromElement(it) } @@ -172,6 +190,9 @@ abstract class ParsedHttpSource : HttpSource() { * * @param response the response from the site. */ + @Deprecated( + "The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.", + ) override fun pageListParse(response: Response): List { return pageListParse(response.asJsoup()) } @@ -188,6 +209,9 @@ abstract class ParsedHttpSource : HttpSource() { * * @param response the response from the site. */ + @Deprecated( + "The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.", + ) override fun imageUrlParse(response: Response): String { return imageUrlParse(response.asJsoup()) } diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt index 91acd8595..d2e925ca6 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -2,7 +2,6 @@ package tachiyomi.source.local import android.content.Context import com.hippo.unifile.UniFile -import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.model.FilterList @@ -10,9 +9,11 @@ import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.model.SMangaUpdate import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.supervisorScope import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import logcat.LogPriority @@ -50,7 +51,7 @@ actual class LocalSource( private val context: Context, private val fileSystem: LocalSourceFileSystem, private val coverManager: LocalCoverManager, -) : CatalogueSource, UnmeteredSource { +) : Source, UnmeteredSource { private val json: Json by injectLazy() private val xml: XML by injectLazy() @@ -138,8 +139,19 @@ actual class LocalSource( MangasPage(mangas, false) } + override suspend fun getMangaUpdate( + manga: SManga, + chapters: List, + fetchDetails: Boolean, + fetchChapters: Boolean, + ): SMangaUpdate = supervisorScope { + val asyncManga = if (fetchDetails) async { getMangaDetails(manga) } else null + val asyncChapters = if (fetchChapters) async { getChapterList(manga) } else null + SMangaUpdate(asyncManga?.await() ?: manga, asyncChapters?.await() ?: chapters) + } + // Manga details related - override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext { + private suspend fun getMangaDetails(manga: SManga): SManga = withIOContext { coverManager.find(manga.url)?.let { manga.thumbnail_url = it.uri.toString() } @@ -253,7 +265,7 @@ actual class LocalSource( } // Chapters - override suspend fun getChapterList(manga: SManga): List = withIOContext { + private suspend fun getChapterList(manga: SManga): List = withIOContext { val chapters = fileSystem.getFilesInMangaDirectory(manga.url) // Only keep supported formats .filterNot { it.name.orEmpty().startsWith('.') } diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/LocalSource.kt index d592f1c96..522139ba9 100644 --- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -1,6 +1,6 @@ package tachiyomi.source.local -import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.UnmeteredSource -expect class LocalSource : CatalogueSource, UnmeteredSource +expect class LocalSource : Source, UnmeteredSource