Adapt source implementation for TachiyomiX 1.6 (#3243)

This commit is contained in:
AntsyLich
2026-06-14 02:34:31 +06:00
committed by GitHub
parent 430b13bb81
commit 4c37f4c764
38 changed files with 597 additions and 524 deletions
@@ -35,6 +35,7 @@ import mihon.domain.extension.interactor.RemoveExtensionStore
import mihon.domain.extension.interactor.UpdateExtensionStores import mihon.domain.extension.interactor.UpdateExtensionStores
import mihon.domain.extension.repository.ExtensionStoreRepository import mihon.domain.extension.repository.ExtensionStoreRepository
import mihon.domain.migration.usecases.MigrateMangaUseCase import mihon.domain.migration.usecases.MigrateMangaUseCase
import mihon.domain.source.interactor.UpdateMangaFromRemote
import mihon.domain.upcoming.interactor.GetUpcomingManga import mihon.domain.upcoming.interactor.GetUpcomingManga
import tachiyomi.data.category.CategoryRepositoryImpl import tachiyomi.data.category.CategoryRepositoryImpl
import tachiyomi.data.chapter.ChapterRepositoryImpl import tachiyomi.data.chapter.ChapterRepositoryImpl
@@ -204,5 +205,7 @@ class DomainModule : InjektModule {
addFactory { ToggleIncognito(get()) } addFactory { ToggleIncognito(get()) }
addFactory { GetIncognitoState(get(), get(), get()) } addFactory { GetIncognitoState(get(), get(), get()) }
addFactory { UpdateMangaFromRemote(get(), get(), get(), get(), get(), get(), get()) }
} }
} }
@@ -1,17 +1,9 @@
package eu.kanade.domain.manga.interactor 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.interactor.FetchInterval
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.repository.MangaRepository 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.Instant
import java.time.ZonedDateTime import java.time.ZonedDateTime
@@ -28,67 +20,6 @@ class UpdateManga(
return mangaRepository.updateAll(mangaUpdates) 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( suspend fun awaitUpdateFetchInterval(
manga: Manga, manga: Manga,
dateTime: ZonedDateTime = ZonedDateTime.now(), dateTime: ZonedDateTime = ZonedDateTime.now(),
@@ -10,7 +10,7 @@ import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
import eu.kanade.presentation.browse.components.GlobalSearchResultItem import eu.kanade.presentation.browse.components.GlobalSearchResultItem
import eu.kanade.presentation.browse.components.GlobalSearchToolbar 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.SearchItemResult
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
@@ -27,7 +27,7 @@ fun GlobalSearchScreen(
onChangeSearchFilter: (SourceFilter) -> Unit, onChangeSearchFilter: (SourceFilter) -> Unit,
onToggleResults: () -> Unit, onToggleResults: () -> Unit,
getManga: @Composable (Manga) -> State<Manga>, getManga: @Composable (Manga) -> State<Manga>,
onClickSource: (CatalogueSource) -> Unit, onClickSource: (Source) -> Unit,
onClickItem: (Manga) -> Unit, onClickItem: (Manga) -> Unit,
onLongClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit,
) { ) {
@@ -62,10 +62,10 @@ fun GlobalSearchScreen(
@Composable @Composable
internal fun GlobalSearchContent( internal fun GlobalSearchContent(
items: Map<CatalogueSource, SearchItemResult>, items: Map<Source, SearchItemResult>,
contentPadding: PaddingValues, contentPadding: PaddingValues,
getManga: @Composable (Manga) -> State<Manga>, getManga: @Composable (Manga) -> State<Manga>,
onClickSource: (CatalogueSource) -> Unit, onClickSource: (Source) -> Unit,
onClickItem: (Manga) -> Unit, onClickItem: (Manga) -> Unit,
onLongClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit,
fromSourceId: Long? = null, fromSourceId: Long? = null,
@@ -3,7 +3,7 @@ package eu.kanade.presentation.browse
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import eu.kanade.presentation.browse.components.GlobalSearchToolbar 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.SearchScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@@ -19,7 +19,7 @@ fun MigrateSearchScreen(
onChangeSearchFilter: (SourceFilter) -> Unit, onChangeSearchFilter: (SourceFilter) -> Unit,
onToggleResults: () -> Unit, onToggleResults: () -> Unit,
getManga: @Composable (Manga) -> State<Manga>, getManga: @Composable (Manga) -> State<Manga>,
onClickSource: (CatalogueSource) -> Unit, onClickSource: (Source) -> Unit,
onClickItem: (Manga) -> Unit, onClickItem: (Manga) -> Unit,
onLongClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit,
) { ) {
@@ -111,7 +111,7 @@ object SettingsTrackingScreen : SearchableSettings {
.filter { it is EnhancedTracker } .filter { it is EnhancedTracker }
.partition { service -> .partition { service ->
val acceptedSources = (service as EnhancedTracker).getAcceptedSources() 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) var enhancedTrackerInfo = stringResource(MR.strings.enhanced_tracking_info)
if (enhancedTrackers.second.isNotEmpty()) { if (enhancedTrackers.second.isNotEmpty()) {
@@ -28,7 +28,7 @@ class PreferenceBackupCreator(
} }
fun createSource(includePrivatePreferences: Boolean): List<BackupSourcePreferences> { fun createSource(includePrivatePreferences: Boolean): List<BackupSourcePreferences> {
return sourceManager.getCatalogueSources() return sourceManager.getAll()
.filterIsInstance<ConfigurableSource>() .filterIsInstance<ConfigurableSource>()
.map { .map {
BackupSourcePreferences( BackupSourcePreferences(
@@ -18,10 +18,6 @@ import androidx.work.WorkInfo
import androidx.work.WorkQuery import androidx.work.WorkQuery
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.workDataOf 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.download.DownloadManager
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
@@ -41,6 +37,7 @@ import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority import logcat.LogPriority
import mihon.domain.chapter.interactor.FilterChaptersForDownload import mihon.domain.chapter.interactor.FilterChaptersForDownload
import mihon.domain.source.interactor.UpdateMangaFromRemote
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.preference.getAndSet import tachiyomi.core.common.preference.getAndSet
import tachiyomi.core.common.util.lang.withIOContext 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 sourceManager: SourceManager = Injekt.get()
private val libraryPreferences: LibraryPreferences = Injekt.get() private val libraryPreferences: LibraryPreferences = Injekt.get()
private val downloadManager: DownloadManager = Injekt.get() private val downloadManager: DownloadManager = Injekt.get()
private val coverCache: CoverCache = Injekt.get()
private val getLibraryManga: GetLibraryManga = Injekt.get() private val getLibraryManga: GetLibraryManga = Injekt.get()
private val getManga: GetManga = 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 fetchInterval: FetchInterval = Injekt.get()
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get() private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get()
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get()
private val notifier = LibraryUpdateNotifier(context) 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<Long, Long>): List<Chapter> { private suspend fun updateManga(manga: Manga, fetchWindow: Pair<Long, Long>): List<Chapter> {
val source = sourceManager.getOrStub(manga.source) val source = sourceManager.getOrStub(manga.source)
// Update manga metadata if needed val update = updateMangaFromRemote(
if (libraryPreferences.autoUpdateMetadata.get()) { source = source,
val networkManga = source.getMangaDetails(manga.toSManga()) manga = manga,
updateManga.awaitUpdateFromSource(manga, networkManga, manualFetch = false, coverCache) fetchDetails = libraryPreferences.autoUpdateMetadata.get(),
} fetchChapters = true,
fetchWindow = fetchWindow,
)
.getOrThrow()
val chapters = source.getChapterList(manga.toSManga()) return if (update.manga.favorite) update.newChapters else emptyList()
// 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)
} }
private suspend fun withUpdateNotification( private suspend fun withUpdateNotification(
@@ -10,12 +10,7 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkQuery import androidx.work.WorkQuery
import androidx.work.WorkerParameters 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.data.notification.Notifications
import eu.kanade.tachiyomi.util.prepUpdateCover
import eu.kanade.tachiyomi.util.system.isRunning import eu.kanade.tachiyomi.util.system.isRunning
import eu.kanade.tachiyomi.util.system.setForegroundSafely import eu.kanade.tachiyomi.util.system.setForegroundSafely
import eu.kanade.tachiyomi.util.system.workManager import eu.kanade.tachiyomi.util.system.workManager
@@ -27,12 +22,12 @@ import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority import logcat.LogPriority
import mihon.domain.source.interactor.UpdateMangaFromRemote
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.interactor.GetLibraryManga import tachiyomi.domain.manga.interactor.GetLibraryManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.toMangaUpdate
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -46,9 +41,8 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
CoroutineWorker(context, workerParams) { CoroutineWorker(context, workerParams) {
private val sourceManager: SourceManager = Injekt.get() private val sourceManager: SourceManager = Injekt.get()
private val coverCache: CoverCache = Injekt.get()
private val getLibraryManga: GetLibraryManga = 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) 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 val source = sourceManager.get(manga.source) ?: return@withUpdateNotification
try { try {
val networkManga = source.getMangaDetails(manga.toSManga()) updateMangaFromRemote(
val updatedManga = manga.prepUpdateCover(coverCache, networkManga, true) source = source,
.copyFrom(networkManga) manga = manga,
try { fetchDetails = true,
updateManga.await(updatedManga.toMangaUpdate()) ).getOrThrow()
} catch (e: Exception) {
logcat(LogPriority.ERROR) { "Manga doesn't exist anymore" }
}
} catch (e: Throwable) { } catch (e: Throwable) {
// Ignore errors and continue // Ignore errors and continue
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
@@ -10,7 +10,6 @@ import eu.kanade.domain.extension.interactor.TrustExtension
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.util.lang.Hash import eu.kanade.tachiyomi.util.lang.Hash
@@ -301,9 +300,7 @@ internal object ExtensionLoader {
} }
} }
val langs = sources.filterIsInstance<CatalogueSource>() val langs = sources.map { it.lang }.toSet()
.map { it.lang }
.toSet()
val lang = when (langs.size) { val lang = when (langs.size) {
0 -> "" 0 -> ""
1 -> langs.first() 1 -> langs.first()
@@ -38,9 +38,7 @@ class AndroidSourceManager(
private val stubSourcesMap = ConcurrentHashMap<Long, StubSource>() private val stubSourcesMap = ConcurrentHashMap<Long, StubSource>()
override val catalogueSources: Flow<List<CatalogueSource>> = sourcesMapFlow.map { override val sources: Flow<List<Source>> = sourcesMapFlow.map { it.values.toList() }
it.values.filterIsInstance<CatalogueSource>()
}
init { init {
scope.launchIO { scope.launchIO {
@@ -87,9 +85,9 @@ class AndroidSourceManager(
} }
} }
override fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance<HttpSource>() override fun getAll() = sourcesMapFlow.value.values.toList()
override fun getCatalogueSources() = sourcesMapFlow.value.values.filterIsInstance<CatalogueSource>() override fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance<HttpSource>()
override fun getStubSources(): List<StubSource> { override fun getStubSources(): List<StubSource> {
val onlineSourceIds = getOnlineSources().map { it.id } val onlineSourceIds = getOnlineSources().map { it.id }
@@ -120,7 +120,7 @@ class ExtensionsScreenModel(
when (extension) { when (extension) {
is Extension.Installed -> extension.sources.any { source -> is Extension.Installed -> extension.sources.any { source ->
source.name.contains(subquery, ignoreCase = true) || 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() source.id == subquery.toLongOrNull()
} }
@@ -99,7 +99,8 @@ class ExtensionDetailsScreenModel(
val urls = extension.sources val urls = extension.sources
.filterIsInstance<HttpSource>() .filterIsInstance<HttpSource>()
.mapNotNull { it.baseUrl.takeUnless { url -> url.isEmpty() } } .flatMap { listOf(it.baseUrl, it.getHomeUrl()) }
.filter { it.isNotEmpty() }
.distinct() .distinct()
val cleared = urls.sumOf { val cleared = urls.sumOf {
@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.source.service.SourcePreferences 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.SearchItemResult
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@@ -21,8 +21,8 @@ class MigrateSearchScreenModel(
private val migrationSources by lazy { sourcePreferences.migrationSources.get() } private val migrationSources by lazy { sourcePreferences.migrationSources.get() }
override val sortComparator = { map: Map<CatalogueSource, SearchItemResult> -> override val sortComparator = { map: Map<Source, SearchItemResult> ->
compareBy<CatalogueSource>( compareBy<Source>(
{ (map[it] as? SearchItemResult.Success)?.isEmpty ?: true }, { (map[it] as? SearchItemResult.Success)?.isEmpty ?: true },
{ migrationSources.indexOf(it.id) }, { migrationSources.indexOf(it.id) },
) )
@@ -41,7 +41,7 @@ class MigrateSearchScreenModel(
} }
} }
override fun getEnabledSources(): List<CatalogueSource> { override fun getEnabledSources(): List<Source> {
return migrationSources.mapNotNull { sourceManager.get(it) as? CatalogueSource } return migrationSources.mapNotNull { sourceManager.get(it) }
} }
} }
@@ -110,7 +110,7 @@ data class MigrateSourceSearchScreen(
val source = screenModel.source as? HttpSource ?: return@BrowseSourceContent val source = screenModel.source as? HttpSource ?: return@BrowseSourceContent
navigator.push( navigator.push(
WebViewScreen( WebViewScreen(
url = source.baseUrl, url = source.getHomeUrl(),
initialTitle = source.name, initialTitle = source.name,
sourceId = source.id, sourceId = source.id,
), ),
@@ -44,7 +44,6 @@ import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.presentation.manga.DuplicateMangaDialog
import eu.kanade.presentation.util.AssistContentScreen import eu.kanade.presentation.util.AssistContentScreen
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing 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 val source = screenModel.source as? HttpSource ?: return@f
navigator.push( navigator.push(
WebViewScreen( WebViewScreen(
url = source.baseUrl, url = source.getHomeUrl(),
initialTitle = source.name, initialTitle = source.name,
sourceId = source.id, sourceId = source.id,
), ),
@@ -119,7 +118,7 @@ data class BrowseSourceScreen(
} }
LaunchedEffect(screenModel.source) { LaunchedEffect(screenModel.source) {
assistUrl = (screenModel.source as? HttpSource)?.baseUrl assistUrl = (screenModel.source as? HttpSource)?.getHomeUrl()
} }
Scaffold( Scaffold(
@@ -166,7 +165,7 @@ data class BrowseSourceScreen(
Text(text = stringResource(MR.strings.popular)) Text(text = stringResource(MR.strings.popular))
}, },
) )
if ((screenModel.source as CatalogueSource).supportsLatest) { if (screenModel.source.supportsLatest) {
FilterChip( FilterChip(
selected = state.listing == Listing.Latest, selected = state.listing == Listing.Latest,
onClick = { onClick = {
@@ -20,7 +20,6 @@ import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.presentation.util.ioCoroutineScope import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.removeCovers
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
@@ -74,7 +73,6 @@ class BrowseSourceScreenModel(
val source = sourceManager.getOrStub(sourceId) val source = sourceManager.getOrStub(sourceId)
init { init {
if (source is CatalogueSource) {
mutableState.update { mutableState.update {
var query: String? = null var query: String? = null
var listing = it.listing var listing = it.listing
@@ -90,7 +88,6 @@ class BrowseSourceScreenModel(
toolbarQuery = query, toolbarQuery = query,
) )
} }
}
if (!getIncognitoState.await(source.id)) { if (!getIncognitoState.await(source.id)) {
sourcePreferences.lastUsedSource.set(source.id) sourcePreferences.lastUsedSource.set(source.id)
@@ -129,8 +126,6 @@ class BrowseSourceScreenModel(
} }
fun resetFilters() { fun resetFilters() {
if (source !is CatalogueSource) return
mutableState.update { it.copy(filters = source.getFilterList()) } mutableState.update { it.copy(filters = source.getFilterList()) }
} }
@@ -139,8 +134,6 @@ class BrowseSourceScreenModel(
} }
fun setFilters(filters: FilterList) { fun setFilters(filters: FilterList) {
if (source !is CatalogueSource) return
mutableState.update { mutableState.update {
it.copy( it.copy(
filters = filters, filters = filters,
@@ -149,8 +142,6 @@ class BrowseSourceScreenModel(
} }
fun search(query: String? = null, filters: FilterList? = null) { fun search(query: String? = null, filters: FilterList? = null) {
if (source !is CatalogueSource) return
val input = state.value.listing as? Listing.Search val input = state.value.listing as? Listing.Search
?: Listing.Search(query = null, filters = source.getFilterList()) ?: Listing.Search(query = null, filters = source.getFilterList())
@@ -166,8 +157,6 @@ class BrowseSourceScreenModel(
} }
fun searchGenre(genreName: String) { fun searchGenre(genreName: String) {
if (source !is CatalogueSource) return
val defaultFilters = source.getFilterList() val defaultFilters = source.getFilterList()
var genreExists = false var genreExists = false
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.ui.browse.source.globalsearch package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source
class GlobalSearchScreenModel( class GlobalSearchScreenModel(
initialQuery: String = "", initialQuery: String = "",
@@ -18,7 +18,7 @@ class GlobalSearchScreenModel(
} }
} }
override fun getEnabledSources(): List<CatalogueSource> { override fun getEnabledSources(): List<Source> {
return super.getEnabledSources() return super.getEnabledSources()
.filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources } .filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
} }
@@ -8,7 +8,7 @@ import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.util.ioCoroutineScope import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.extension.ExtensionManager 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.Job
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async import kotlinx.coroutines.async
@@ -52,8 +52,8 @@ abstract class SearchScreenModel(
protected var extensionFilter: String? = null protected var extensionFilter: String? = null
open val sortComparator = { map: Map<CatalogueSource, SearchItemResult> -> open val sortComparator = { map: Map<Source, SearchItemResult> ->
compareBy<CatalogueSource>( compareBy<Source>(
{ (map[it] as? SearchItemResult.Success)?.isEmpty ?: true }, { (map[it] as? SearchItemResult.Success)?.isEmpty ?: true },
{ "${it.id}" !in pinnedSources }, { "${it.id}" !in pinnedSources },
{ "${it.name.lowercase()} (${it.lang})" }, { "${it.name.lowercase()} (${it.lang})" },
@@ -79,8 +79,8 @@ abstract class SearchScreenModel(
} }
} }
open fun getEnabledSources(): List<CatalogueSource> { open fun getEnabledSources(): List<Source> {
return sourceManager.getCatalogueSources() return sourceManager.getAll()
.filter { it.lang in enabledLanguages && "${it.id}" !in disabledSources } .filter { it.lang in enabledLanguages && "${it.id}" !in disabledSources }
.sortedWith( .sortedWith(
compareBy( compareBy(
@@ -90,7 +90,7 @@ abstract class SearchScreenModel(
) )
} }
private fun getSelectedSources(): List<CatalogueSource> { private fun getSelectedSources(): List<Source> {
val enabledSources = getEnabledSources() val enabledSources = getEnabledSources()
val filter = extensionFilter val filter = extensionFilter
@@ -101,7 +101,6 @@ abstract class SearchScreenModel(
return extensionManager.installedExtensionsFlow.value return extensionManager.installedExtensionsFlow.value
.filter { it.pkgName == filter } .filter { it.pkgName == filter }
.flatMap { it.sources } .flatMap { it.sources }
.filterIsInstance<CatalogueSource>()
.filter { it in enabledSources } .filter { it in enabledSources }
} }
@@ -179,7 +178,7 @@ abstract class SearchScreenModel(
} }
} }
private fun updateItems(items: Map<CatalogueSource, SearchItemResult>) { private fun updateItems(items: Map<Source, SearchItemResult>) {
mutableState.update { mutableState.update {
it.copy( it.copy(
items = items 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)) updateItems(state.value.items + (source to result))
} }
@@ -209,7 +208,7 @@ abstract class SearchScreenModel(
val searchQuery: String? = null, val searchQuery: String? = null,
val sourceFilter: SourceFilter = SourceFilter.PinnedOnly, val sourceFilter: SourceFilter = SourceFilter.PinnedOnly,
val onlyShowHasResults: Boolean = false, val onlyShowHasResults: Boolean = false,
val items: Map<CatalogueSource, SearchItemResult> = mapOf(), val items: Map<Source, SearchItemResult> = mapOf(),
val dialog: Dialog? = null, val dialog: Dialog? = null,
) { ) {
val progress: Int = items.count { it.value !is SearchItemResult.Loading } val progress: Int = items.count { it.value !is SearchItemResult.Loading }
@@ -3,14 +3,13 @@ package eu.kanade.tachiyomi.ui.deeplink
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope 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.Source
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.ResolvableSource import eu.kanade.tachiyomi.source.online.ResolvableSource
import eu.kanade.tachiyomi.source.online.UriType import eu.kanade.tachiyomi.source.online.UriType
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.domain.manga.model.toDomainManga import mihon.domain.manga.model.toDomainManga
import mihon.domain.source.interactor.UpdateMangaFromRemote
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
@@ -25,12 +24,12 @@ class DeepLinkScreenModel(
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(), private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(),
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(),
) : StateScreenModel<DeepLinkScreenModel.State>(State.Loading) { ) : StateScreenModel<DeepLinkScreenModel.State>(State.Loading) {
init { init {
screenModelScope.launchIO { screenModelScope.launchIO {
val source = sourceManager.getCatalogueSources() val source = sourceManager.getAll()
.filterIsInstance<ResolvableSource>() .filterIsInstance<ResolvableSource>()
.firstOrNull { it.getUriType(query) != UriType.Unknown } .firstOrNull { it.getUriType(query) != UriType.Unknown }
@@ -61,13 +60,11 @@ class DeepLinkScreenModel(
private suspend fun getChapterFromSChapter(sChapter: SChapter, manga: Manga, source: Source): Chapter? { private suspend fun getChapterFromSChapter(sChapter: SChapter, manga: Manga, source: Source): Chapter? {
val localChapter = getChapterByUrlAndMangaId.await(sChapter.url, manga.id) val localChapter = getChapterByUrlAndMangaId.await(sChapter.url, manga.id)
return if (localChapter == null) { return localChapter
val sourceChapters = source.getChapterList(manga.toSManga()) ?: updateMangaFromRemote(manga, fetchChapters = true)
val newChapters = syncChaptersWithSource.await(sourceChapters, manga, source, false) .getOrElse { return null }
newChapters.find { it.url == sChapter.url } .newChapters
} else { .find { it.url == sChapter.url }
localChapter
}
} }
sealed interface State { sealed interface State {
@@ -16,13 +16,11 @@ import eu.kanade.core.util.addOrRemove
import eu.kanade.core.util.insertSeparators import eu.kanade.core.util.insertSeparators
import eu.kanade.domain.chapter.interactor.GetAvailableScanlators import eu.kanade.domain.chapter.interactor.GetAvailableScanlators
import eu.kanade.domain.chapter.interactor.SetReadStatus 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.GetExcludedScanlators
import eu.kanade.domain.manga.interactor.SetExcludedScanlators import eu.kanade.domain.manga.interactor.SetExcludedScanlators
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.chaptersFiltered import eu.kanade.domain.manga.model.chaptersFiltered
import eu.kanade.domain.manga.model.downloadedFilter 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.AddTracks
import eu.kanade.domain.track.interactor.RefreshTracks import eu.kanade.domain.track.interactor.RefreshTracks
import eu.kanade.domain.track.interactor.TrackChapter 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.download.model.Download
import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.EnhancedTracker
import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.network.HttpException
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.chapter.getNextUnread import eu.kanade.tachiyomi.util.chapter.getNextUnread
import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.async import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@@ -54,13 +50,13 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import mihon.domain.chapter.interactor.FilterChaptersForDownload import mihon.domain.chapter.interactor.FilterChaptersForDownload
import mihon.domain.source.interactor.UpdateMangaFromRemote
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.preference.CheckboxState import tachiyomi.core.common.preference.CheckboxState
import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.preference.TriState
import tachiyomi.core.common.preference.mapAsCheckboxState import tachiyomi.core.common.preference.mapAsCheckboxState
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable 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.lang.withUIContext
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
@@ -111,13 +107,13 @@ class MangaScreenModel(
private val setReadStatus: SetReadStatus = Injekt.get(), private val setReadStatus: SetReadStatus = Injekt.get(),
private val updateChapter: UpdateChapter = Injekt.get(), private val updateChapter: UpdateChapter = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(),
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(), private val getTracks: GetTracks = Injekt.get(),
private val addTracks: AddTracks = Injekt.get(), private val addTracks: AddTracks = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val mangaRepository: MangaRepository = Injekt.get(), private val mangaRepository: MangaRepository = Injekt.get(),
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get(), private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get(),
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(), val snackbarHostState: SnackbarHostState = SnackbarHostState(),
) : StateScreenModel<MangaScreenModel.State>(State.Loading) { ) : StateScreenModel<MangaScreenModel.State>(State.Loading) {
@@ -237,11 +233,11 @@ class MangaScreenModel(
// Fetch info-chapters when needed // Fetch info-chapters when needed
if (screenModelScope.isActive) { if (screenModelScope.isActive) {
val fetchFromSourceTasks = listOf( fetchAllFromSource(
async { if (needRefreshInfo) fetchMangaFromSource() }, manualFetch = false,
async { if (needRefreshChapter) fetchChaptersFromSource() }, fetchDetails = needRefreshInfo,
fetchChapters = needRefreshChapter,
) )
fetchFromSourceTasks.awaitAll()
} }
// Initial loading finished // Initial loading finished
@@ -252,38 +248,54 @@ class MangaScreenModel(
fun fetchAllFromSource(manualFetch: Boolean = true) { fun fetchAllFromSource(manualFetch: Boolean = true) {
screenModelScope.launch { screenModelScope.launch {
updateSuccessState { it.copy(isRefreshingData = true) } updateSuccessState { it.copy(isRefreshingData = true) }
val fetchFromSourceTasks = listOf( fetchAllFromSource(
async { fetchMangaFromSource(manualFetch) }, manualFetch = manualFetch,
async { fetchChaptersFromSource(manualFetch) }, fetchDetails = true,
fetchChapters = true,
) )
fetchFromSourceTasks.awaitAll()
updateSuccessState { it.copy(isRefreshingData = false) } updateSuccessState { it.copy(isRefreshingData = false) }
} }
} }
// Manga info - start private suspend fun fetchAllFromSource(
manualFetch: Boolean,
/** fetchDetails: Boolean,
* Fetch manga information from source. fetchChapters: Boolean,
*/ ) {
private suspend fun fetchMangaFromSource(manualFetch: Boolean = false) {
val state = successState ?: return val state = successState ?: return
try { try {
withIOContext { withUIContext {
val networkManga = state.source.getMangaDetails(state.manga.toSManga()) val update = updateMangaFromRemote(
updateManga.awaitUpdateFromSource(state.manga, networkManga, manualFetch) source = state.source,
} manga = state.manga,
} catch (e: Throwable) { fetchDetails = fetchDetails,
// Ignore early hints "errors" that aren't handled by OkHttp fetchChapters = fetchChapters,
if (e is HttpException && e.code == 103) return 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) logcat(LogPriority.ERROR, e)
with(context) { e.formattedMessage }
}
screenModelScope.launch { screenModelScope.launch {
snackbarHostState.showSnackbar(message = with(context) { e.formattedMessage }) snackbarHostState.showSnackbar(message = message)
} }
} }
} }
// Manga info - start
fun toggleFavorite() { fun toggleFavorite() {
toggleFavorite( toggleFavorite(
onRemoved = { 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] * @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled]
*/ */
@@ -13,33 +13,6 @@ import uy.kohesive.injekt.api.get
import java.io.InputStream import java.io.InputStream
import java.time.Instant 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 { fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Manga {
if (isLocal()) return this if (isLocal()) return this
return if (coverCache.deleteFromCache(this, true) > 0) { return if (coverCache.deleteFromCache(this, true) > 0) {
@@ -1,9 +1,7 @@
package mihon.domain.migration.usecases package mihon.domain.migration.usecases
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.hasCustomCover import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadManager 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 eu.kanade.tachiyomi.data.track.TrackerManager
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import mihon.domain.migration.models.MigrationFlag import mihon.domain.migration.models.MigrationFlag
import mihon.domain.source.interactor.UpdateMangaFromRemote
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
@@ -30,13 +29,13 @@ class MigrateMangaUseCase(
private val downloadManager: DownloadManager, private val downloadManager: DownloadManager,
private val updateManga: UpdateManga, private val updateManga: UpdateManga,
private val getChaptersByMangaId: GetChaptersByMangaId, private val getChaptersByMangaId: GetChaptersByMangaId,
private val syncChaptersWithSource: SyncChaptersWithSource,
private val updateChapter: UpdateChapter, private val updateChapter: UpdateChapter,
private val getCategories: GetCategories, private val getCategories: GetCategories,
private val setMangaCategories: SetMangaCategories, private val setMangaCategories: SetMangaCategories,
private val getTracks: GetTracks, private val getTracks: GetTracks,
private val insertTrack: InsertTrack, private val insertTrack: InsertTrack,
private val coverCache: CoverCache, private val coverCache: CoverCache,
private val updateMangaFromRemote: UpdateMangaFromRemote,
) { ) {
private val enhancedServices by lazy { trackerManager.trackers.filterIsInstance<EnhancedTracker>() } private val enhancedServices by lazy { trackerManager.trackers.filterIsInstance<EnhancedTracker>() }
@@ -46,13 +45,7 @@ class MigrateMangaUseCase(
val flags = sourcePreferences.migrationFlags.get() val flags = sourcePreferences.migrationFlags.get()
try { try {
val chapters = targetSource.getChapterList(target.toSManga()) updateMangaFromRemote(target, fetchChapters = true).getOrThrow()
try {
syncChaptersWithSource.await(chapters, target, targetSource)
} catch (_: Exception) {
// Worst case, chapters won't be synced
}
// Update chapters read, bookmark and dateFetch // Update chapters read, bookmark and dateFetch
if (MigrationFlag.CHAPTER in flags) { if (MigrationFlag.CHAPTER in flags) {
@@ -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<Long, Long> = Pair(0, 0),
): Result<RemoteMangaUpdate> {
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<Long, Long> = Pair(0, 0),
): Result<RemoteMangaUpdate> {
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
}
}
@@ -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<Chapter>,
)
@@ -344,7 +344,7 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
val includedSources = sourcePreferences.migrationSources.get() val includedSources = sourcePreferences.migrationSources.get()
val disabledSources = sourcePreferences.disabledSources.get() val disabledSources = sourcePreferences.disabledSources.get()
.mapNotNull { it.toLongOrNull() } .mapNotNull { it.toLongOrNull() }
val sources = sourceManager.getCatalogueSources() val sources = sourceManager.getAll()
.asSequence() .asSequence()
.filterIsInstance<HttpSource>() .filterIsInstance<HttpSource>()
.filter { it.lang in languages } .filter { it.lang in languages }
@@ -4,10 +4,8 @@ import androidx.annotation.FloatRange
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource 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.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.getNameForMangaInfo import eu.kanade.tachiyomi.source.getNameForMangaInfo
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -24,6 +22,7 @@ import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority import logcat.LogPriority
import mihon.domain.migration.usecases.MigrateMangaUseCase 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
import mihon.feature.migration.list.models.MigratingManga.SearchResult import mihon.feature.migration.list.models.MigratingManga.SearchResult
import mihon.feature.migration.list.search.SmartSourceSearchEngine import mihon.feature.migration.list.search.SmartSourceSearchEngine
@@ -45,10 +44,10 @@ class MigrationListScreenModel(
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
private val migrateManga: MigrateMangaUseCase = Injekt.get(), private val migrateManga: MigrateMangaUseCase = Injekt.get(),
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(),
) : StateScreenModel<MigrationListScreenModel.State>(State()) { ) : StateScreenModel<MigrationListScreenModel.State>(State()) {
private val smartSearchEngine = SmartSourceSearchEngine(extraSearchQuery) private val smartSearchEngine = SmartSourceSearchEngine(extraSearchQuery)
@@ -110,7 +109,7 @@ class MigrationListScreenModel(
val deepSearchMode = preferences.migrationDeepSearchMode.get() val deepSearchMode = preferences.migrationDeepSearchMode.get()
val sources = preferences.migrationSources.get() val sources = preferences.migrationSources.get()
.mapNotNull { sourceManager.get(it) as? CatalogueSource } .mapNotNull { sourceManager.get(it) }
for (manga in mangas) { for (manga in mangas) {
if (!currentCoroutineContext().isActive) break if (!currentCoroutineContext().isActive) break
@@ -148,8 +147,7 @@ class MigrationListScreenModel(
if (result != null && result.first.thumbnailUrl == null) { if (result != null && result.first.thumbnailUrl == null) {
try { try {
val newManga = sourceManager.getOrStub(result.first.source).getMangaDetails(result.first.toSManga()) updateMangaFromRemote(result.first, fetchDetails = true, manualFetch = true).getOrThrow().manga
updateManga.awaitUpdateFromSource(result.first, newManga, true)
} catch (e: CancellationException) { } catch (e: CancellationException) {
throw e throw e
} catch (_: Exception) { } catch (_: Exception) {
@@ -174,7 +172,7 @@ class MigrationListScreenModel(
private suspend fun searchSource( private suspend fun searchSource(
manga: Manga, manga: Manga,
source: CatalogueSource, source: Source,
deepSearchMode: Boolean, deepSearchMode: Boolean,
): Pair<Manga, ChapterInfo>? { ): Pair<Manga, ChapterInfo>? {
return try { return try {
@@ -188,8 +186,7 @@ class MigrationListScreenModel(
val localManga = networkToLocalManga(searchResult) val localManga = networkToLocalManga(searchResult)
try { try {
val chapters = source.getChapterList(localManga.toSManga()) updateMangaFromRemote(localManga, fetchChapters = true).getOrThrow()
syncChaptersWithSource.await(chapters, localManga, source)
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
} }
@@ -224,12 +221,10 @@ class MigrationListScreenModel(
val manga = getManga.await(target) ?: return@async null val manga = getManga.await(target) ?: return@async null
try { try {
val source = sourceManager.get(manga.source)!! val source = sourceManager.get(manga.source)!!
val chapters = source.getChapterList(manga.toSManga()) updateMangaFromRemote(source = source, manga = manga, fetchChapters = true).getOrThrow().manga
syncChaptersWithSource.await(chapters, manga, source)
} catch (_: Exception) { } catch (_: Exception) {
return@async null null
} }
manga
} }
.await() .await()
@@ -239,13 +234,6 @@ class MigrationListScreenModel(
return@launchIO 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() migratingManga.searchResult.value = result.toSuccessSearchResult()
updateMigrationProgress() updateMigrationProgress()
} }
@@ -1,6 +1,6 @@
package mihon.feature.migration.list.search 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 eu.kanade.tachiyomi.source.model.SManga
import mihon.domain.manga.model.toDomainManga import mihon.domain.manga.model.toDomainManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@@ -9,19 +9,19 @@ class SmartSourceSearchEngine(extraSearchParams: String?) : BaseSmartSearchEngin
override fun getTitle(result: SManga) = result.title 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 { return regularSearch(makeSearchAction(source), title).let {
it?.toDomainManga(source.id) 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 { return deepSearch(makeSearchAction(source), title).let {
it?.toDomainManga(source.id) it?.toDomainManga(source.id)
} }
} }
private fun makeSearchAction(source: CatalogueSource): SearchAction<SManga> = { query -> private fun makeSearchAction(source: Source): SearchAction<SManga> = { query ->
source.getSearchManga(1, query, source.getFilterList()).mangas source.getSearchManga(1, query, source.getFilterList()).mangas
} }
} }
@@ -1,7 +1,7 @@
package tachiyomi.data.source package tachiyomi.data.source
import androidx.paging.PagingState 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.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import mihon.domain.manga.model.toDomainManga import mihon.domain.manga.model.toDomainManga
@@ -13,7 +13,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class SourceSearchPagingSource( class SourceSearchPagingSource(
source: CatalogueSource, source: Source,
private val query: String, private val query: String,
private val filters: FilterList, private val filters: FilterList,
) : BaseSourcePagingSource(source) { ) : 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 { override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source.getPopularManga(currentPage) return source.getPopularManga(currentPage)
} }
} }
class SourceLatestPagingSource(source: CatalogueSource) : BaseSourcePagingSource(source) { class SourceLatestPagingSource(source: Source) : BaseSourcePagingSource(source) {
override suspend fun requestNextPage(currentPage: Int): MangasPage { override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source.getLatestUpdates(currentPage) return source.getLatestUpdates(currentPage)
} }
} }
abstract class BaseSourcePagingSource( abstract class BaseSourcePagingSource(
protected val source: CatalogueSource, protected val source: Source,
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
) : SourcePagingSource() { ) : SourcePagingSource() {
@@ -1,6 +1,5 @@
package tachiyomi.data.source package tachiyomi.data.source
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
@@ -22,7 +21,7 @@ class SourceRepositoryImpl(
) : SourceRepository { ) : SourceRepository {
override fun getSources(): Flow<List<DomainSource>> { override fun getSources(): Flow<List<DomainSource>> {
return sourceManager.catalogueSources.map { sources -> return sourceManager.sources.map { sources ->
sources.map { sources.map {
mapSourceToDomainSource(it).copy( mapSourceToDomainSource(it).copy(
supportsLatest = it.supportsLatest, supportsLatest = it.supportsLatest,
@@ -32,7 +31,7 @@ class SourceRepositoryImpl(
} }
override fun getOnlineSources(): Flow<List<DomainSource>> { override fun getOnlineSources(): Flow<List<DomainSource>> {
return sourceManager.catalogueSources.map { sources -> return sourceManager.sources.map { sources ->
sources sources
.filterIsInstance<HttpSource>() .filterIsInstance<HttpSource>()
.map(::mapSourceToDomainSource) .map(::mapSourceToDomainSource)
@@ -43,7 +42,7 @@ class SourceRepositoryImpl(
val sourceIdWithFavoriteCountFlow = database.mangasQueries val sourceIdWithFavoriteCountFlow = database.mangasQueries
.getSourceIdWithFavoriteCount() .getSourceIdWithFavoriteCount()
.subscribeToList() .subscribeToList()
return combine(sourceIdWithFavoriteCountFlow, sourceManager.catalogueSources) { sourceIdWithFavoriteCount, _ -> return combine(sourceIdWithFavoriteCountFlow, sourceManager.sources) { sourceIdWithFavoriteCount, _ ->
sourceIdWithFavoriteCount sourceIdWithFavoriteCount
} }
.map { .map {
@@ -77,18 +76,15 @@ class SourceRepositoryImpl(
query: String, query: String,
filterList: FilterList, filterList: FilterList,
): SourcePagingSource { ): SourcePagingSource {
val source = sourceManager.get(sourceId) as CatalogueSource return SourceSearchPagingSource(sourceManager.getOrStub(sourceId), query, filterList)
return SourceSearchPagingSource(source, query, filterList)
} }
override fun getPopular(sourceId: Long): SourcePagingSource { override fun getPopular(sourceId: Long): SourcePagingSource {
val source = sourceManager.get(sourceId) as CatalogueSource return SourcePopularPagingSource(sourceManager.getOrStub(sourceId))
return SourcePopularPagingSource(source)
} }
override fun getLatest(sourceId: Long): SourcePagingSource { override fun getLatest(sourceId: Long): SourcePagingSource {
val source = sourceManager.get(sourceId) as CatalogueSource return SourceLatestPagingSource(sourceManager.getOrStub(sourceId))
return SourceLatestPagingSource(source)
} }
private fun mapSourceToDomainSource(source: Source): DomainSource = DomainSource( private fun mapSourceToDomainSource(source: Source): DomainSource = DomainSource(
@@ -1,9 +1,12 @@
package tachiyomi.domain.source.model package tachiyomi.domain.source.model
import eu.kanade.tachiyomi.source.Source 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.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.SMangaUpdate
class StubSource( class StubSource(
override val id: Long, override val id: Long,
@@ -13,11 +16,22 @@ class StubSource(
private val isInvalid: Boolean = name.isBlank() || lang.isBlank() 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() throw SourceNotInstalledException()
override suspend fun getChapterList(manga: SManga): List<SChapter> = override suspend fun getMangaUpdate(
throw SourceNotInstalledException() manga: SManga,
chapters: List<SChapter>,
fetchDetails: Boolean,
fetchChapters: Boolean,
): SMangaUpdate = throw SourceNotInstalledException()
override suspend fun getPageList(chapter: SChapter): List<Page> = override suspend fun getPageList(chapter: SChapter): List<Page> =
throw SourceNotInstalledException() throw SourceNotInstalledException()
@@ -1,6 +1,5 @@
package tachiyomi.domain.source.service package tachiyomi.domain.source.service
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -11,15 +10,15 @@ interface SourceManager {
val isInitialized: StateFlow<Boolean> val isInitialized: StateFlow<Boolean>
val catalogueSources: Flow<List<CatalogueSource>> val sources: Flow<List<Source>>
fun get(sourceKey: Long): Source? fun get(sourceKey: Long): Source?
fun getOrStub(sourceKey: Long): Source fun getOrStub(sourceKey: Long): Source
fun getOnlineSources(): List<HttpSource> fun getAll(): List<Source>
fun getCatalogueSources(): List<CatalogueSource> fun getOnlineSources(): List<HttpSource>
fun getStubSources(): List<StubSource> fun getStubSources(): List<StubSource>
} }
@@ -2,6 +2,12 @@ package eu.kanade.tachiyomi.source
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage 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 rx.Observable
import tachiyomi.core.common.util.lang.awaitSingle import tachiyomi.core.common.util.lang.awaitSingle
@@ -12,69 +18,61 @@ interface CatalogueSource : Source {
*/ */
override val lang: String 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") @Suppress("DEPRECATION")
suspend fun getPopularManga(page: Int): MangasPage { override suspend fun getPopularManga(page: Int): MangasPage = fetchPopularManga(page).awaitSingle()
return 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<SChapter>,
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<Page> = 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<MangasPage> = 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 page the page number to retrieve.
* @param query the search query. * @param query the search query.
* @param filters the list of filters to apply. * @param filters the list of filters to apply.
*/ */
@Suppress("DEPRECATION") @Deprecated("Use the suspend API instead", ReplaceWith("getSearchManga"))
suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { fun fetchSearchManga(
return fetchSearchManga(page, query, filters).awaitSingle() page: Int,
} query: String,
filters: FilterList,
): Observable<MangasPage> = 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. * @param page the page number to retrieve.
*/ */
@Suppress("DEPRECATION") @Deprecated("Use the suspend API instead", ReplaceWith("getLatestUpdates"))
suspend fun getLatestUpdates(page: Int): MangasPage { fun fetchLatestUpdates(page: Int): Observable<MangasPage> = throw UnsupportedOperationException()
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<MangasPage> =
throw IllegalStateException("Not used")
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getSearchManga"),
)
fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
throw IllegalStateException("Not used")
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getLatestUpdates"),
)
fun fetchLatestUpdates(page: Int): Observable<MangasPage> =
throw IllegalStateException("Not used")
} }
@@ -1,13 +1,15 @@
package eu.kanade.tachiyomi.source 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.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.awaitSingle import eu.kanade.tachiyomi.source.model.SMangaUpdate
import rx.Observable 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 { interface Source {
@@ -25,60 +27,80 @@ interface Source {
get() = "" get() = ""
/** /**
* Get the updated details for a manga. * Whether the source has support for latest updates.
*
* @since extensions-lib 1.5
* @param manga the manga to update.
* @return the updated manga.
*/ */
@Suppress("DEPRECATION") val supportsLatest: Boolean
suspend fun getMangaDetails(manga: SManga): SManga {
return fetchMangaDetails(manga).awaitSingle()
}
/** /**
* Get all the available chapters for a manga. * Returns the list of filters for the source.
*
* @since extensions-lib 1.5
* @param manga the manga to update.
* @return the chapters for the manga.
*/ */
@Suppress("DEPRECATION") fun getFilterList(): FilterList = FilterList()
suspend fun getChapterList(manga: SManga): List<SChapter> {
return fetchChapterList(manga).awaitSingle() /**
} * 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<SChapter>,
fetchDetails: Boolean,
fetchChapters: Boolean,
): SMangaUpdate
/** /**
* Get the list of pages a chapter has. Pages should be returned * Get the list of pages a chapter has. Pages should be returned
* in the expected order; the index is ignored. * in the expected order; the index is ignored.
* *
* @since extensions-lib 1.5 * @since tachiyomix 1.6
* @param chapter the chapter. * @param chapter the chapter.
* @return the pages for the chapter. * @return the pages for the chapter.
*/ */
@Suppress("DEPRECATION") suspend fun getPageList(chapter: SChapter): List<Page>
suspend fun getPageList(chapter: SChapter): List<Page> {
return fetchPageList(chapter).awaitSingle() @Deprecated("Use the combined suspend API instead", ReplaceWith("getMangaUpdate"))
} fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw UnsupportedOperationException()
@Deprecated( @Deprecated("Use the combined suspend API instead", ReplaceWith("getMangaUpdate"))
"Use the non-RxJava API instead", fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw UnsupportedOperationException()
ReplaceWith("getMangaDetails"),
) @Deprecated("Use the suspend API instead", ReplaceWith("getPageList"))
fun fetchMangaDetails(manga: SManga): Observable<SManga> = fun fetchPageList(chapter: SChapter): Observable<List<Page>> = throw UnsupportedOperationException()
throw IllegalStateException("Not used")
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getChapterList"),
)
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
throw IllegalStateException("Not used")
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getPageList"),
)
fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
throw IllegalStateException("Not used")
} }
@@ -0,0 +1,4 @@
package eu.kanade.tachiyomi.source.model
@Suppress("UNUSED")
class SMangaUpdate(val manga: SManga, val chapters: List<SChapter>)
@@ -25,7 +25,6 @@ import java.security.MessageDigest
/** /**
* A simple implementation for sources from a website. * A simple implementation for sources from a website.
*/ */
@Suppress("unused")
abstract class HttpSource : CatalogueSource { abstract class HttpSource : CatalogueSource {
/** /**
@@ -38,11 +37,24 @@ abstract class HttpSource : CatalogueSource {
*/ */
abstract val baseUrl: String 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 websites 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 * 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. * 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) * 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`. * 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. * Headers used for requests.
@@ -64,8 +76,7 @@ abstract class HttpSource : CatalogueSource {
/** /**
* Default network client for doing requests. * Default network client for doing requests.
*/ */
open val client: OkHttpClient open val client: OkHttpClient get() = network.client
get() = network.client
/** /**
* Generates a unique ID for the source based on the provided [name], [lang] and * 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. * 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()) add("User-Agent", network.defaultUserAgentProvider())
} }
/** /**
* Visible name of the source. * 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 * 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. * @param page the page number to retrieve.
*/ */
@Suppress("DEPRECATION") @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<MangasPage> { override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newCall(popularMangaRequest(page)) return client.newCall(popularMangaRequest(page))
.asObservableSuccess() .asObservableSuccess()
@@ -123,14 +134,22 @@ abstract class HttpSource : CatalogueSource {
* *
* @param page the page number to retrieve. * @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. * Parses the response from the site and returns a [MangasPage] object.
* *
* @param response the response from the site. * @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 * 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. * @param filters the list of filters to apply.
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga")) @Deprecated("Use the suspend API instead", ReplaceWith("getSearchManga"))
override fun fetchSearchManga( override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
page: Int, return client.newCall(searchMangaRequest(page, query, filters))
query: String, .asObservableSuccess()
filters: FilterList,
): Observable<MangasPage> {
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)
}
}
.map { response -> .map { response ->
searchMangaParse(response) searchMangaParse(response)
} }
@@ -168,18 +176,26 @@ abstract class HttpSource : CatalogueSource {
* @param query the search query. * @param query the search query.
* @param filters the list of filters to apply. * @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, page: Int,
query: String, query: String,
filters: FilterList, filters: FilterList,
): Request ): Request = throw UnsupportedOperationException()
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
* *
* @param response the response from the site. * @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. * 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. * @param page the page number to retrieve.
*/ */
@Suppress("DEPRECATION") @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<MangasPage> { override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return client.newCall(latestUpdatesRequest(page)) return client.newCall(latestUpdatesRequest(page))
.asObservableSuccess() .asObservableSuccess()
@@ -201,29 +217,31 @@ abstract class HttpSource : CatalogueSource {
* *
* @param page the page number to retrieve. * @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. * Parses the response from the site and returns a [MangasPage] object.
* *
* @param response the response from the site. * @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. * Returns an observable with the updated details for a manga. Normally it's not needed to
* Normally it's not needed to override this method. * override this method.
* *
* @param manga the manga to update. * @param manga the manga to be updated.
* @return the updated manga.
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
override suspend fun getMangaDetails(manga: SManga): SManga { @Deprecated("Use the combined suspend API instead", replaceWith = ReplaceWith("getMangaUpdate"))
return fetchMangaDetails(manga).awaitSingle()
}
@Suppress("DEPRECATION")
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
@@ -238,6 +256,10 @@ abstract class HttpSource : CatalogueSource {
* *
* @param manga the manga to be updated. * @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 { open fun mangaDetailsRequest(manga: SManga): Request {
return GET(baseUrl + manga.url, headers) return GET(baseUrl + manga.url, headers)
} }
@@ -247,22 +269,20 @@ abstract class HttpSource : CatalogueSource {
* *
* @param response the response from the site. * @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. * Returns an observable with the updated chapter list for a manga. Normally it's not needed to
* Normally it's not needed to override this method. * override this method.
* *
* @param manga the manga to update. * @param manga the manga to look for chapters.
* @return the chapters for the manga.
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
override suspend fun getChapterList(manga: SManga): List<SChapter> { @Deprecated("Use the combined suspend API instead", replaceWith = ReplaceWith("getMangaUpdate"))
return fetchChapterList(manga).awaitSingle()
}
@Suppress("DEPRECATION")
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return client.newCall(chapterListRequest(manga)) return client.newCall(chapterListRequest(manga))
.asObservableSuccess() .asObservableSuccess()
@@ -277,6 +297,10 @@ abstract class HttpSource : CatalogueSource {
* *
* @param manga the manga to look for chapters. * @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 { protected open fun chapterListRequest(manga: SManga): Request {
return GET(baseUrl + manga.url, headers) return GET(baseUrl + manga.url, headers)
} }
@@ -286,29 +310,19 @@ abstract class HttpSource : CatalogueSource {
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
protected abstract fun chapterListParse(response: Response): List<SChapter> @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<SChapter> = 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. * @param chapter the chapter whose page list has to be fetched.
*/
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.
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
override suspend fun getPageList(chapter: SChapter): List<Page> { @Deprecated("Use the suspend API instead", ReplaceWith("getPageList"))
return fetchPageList(chapter).awaitSingle()
}
@Suppress("DEPRECATION")
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return client.newCall(pageListRequest(chapter)) return client.newCall(pageListRequest(chapter))
.asObservableSuccess() .asObservableSuccess()
@@ -323,6 +337,10 @@ abstract class HttpSource : CatalogueSource {
* *
* @param chapter the chapter whose page list has to be fetched. * @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 { protected open fun pageListRequest(chapter: SChapter): Request {
return GET(baseUrl + chapter.url, headers) return GET(baseUrl + chapter.url, headers)
} }
@@ -332,34 +350,45 @@ abstract class HttpSource : CatalogueSource {
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
protected abstract fun pageListParse(response: Response): List<Page> @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<Page> = throw UnsupportedOperationException()
/** /**
* Returns an observable with the page containing the source url of the image. If there's any * 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. * 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. * @param page the page whose source image has to be fetched.
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
open suspend fun getImageUrl(page: Page): String { @Deprecated("Use the suspend API instead", ReplaceWith("getImageUrl"))
return fetchImageUrl(page).awaitSingle()
}
@Suppress("DEPRECATION")
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl"))
open fun fetchImageUrl(page: Page): Observable<String> { open fun fetchImageUrl(page: Page): Observable<String> {
return client.newCall(imageUrlRequest(page)) return client.newCall(imageUrlRequest(page))
.asObservableSuccess() .asObservableSuccess()
.map { imageUrlParse(it) } .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 * 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. * override the url, send different headers or request method like POST.
* *
* @param page the chapter whose page list has to be fetched * @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 { protected open fun imageUrlRequest(page: Page): Request {
return GET(page.url, headers) return GET(page.url, headers)
} }
@@ -369,16 +398,13 @@ abstract class HttpSource : CatalogueSource {
* *
* @param response the response from the site. * @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()
/** suspend fun getImage(page: Page): Response {
* 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 {
return client.newCachelessCallWithProgress(imageRequest(page), page) return client.newCachelessCallWithProgress(imageRequest(page), page)
.awaitSuccess() .awaitSuccess()
} }
@@ -399,6 +425,7 @@ abstract class HttpSource : CatalogueSource {
* *
* @param url the full url to the chapter. * @param url the full url to the chapter.
*/ */
@Suppress("Unused")
fun SChapter.setUrlWithoutDomain(url: String) { fun SChapter.setUrlWithoutDomain(url: String) {
this.url = getUrlWithoutDomain(url) this.url = getUrlWithoutDomain(url)
} }
@@ -409,6 +436,7 @@ abstract class HttpSource : CatalogueSource {
* *
* @param url the full url to the manga. * @param url the full url to the manga.
*/ */
@Suppress("Unused")
fun SManga.setUrlWithoutDomain(url: String) { fun SManga.setUrlWithoutDomain(url: String) {
this.url = getUrlWithoutDomain(url) this.url = getUrlWithoutDomain(url)
} }
@@ -429,7 +457,7 @@ abstract class HttpSource : CatalogueSource {
out += "#" + uri.fragment out += "#" + uri.fragment
} }
out out
} catch (e: URISyntaxException) { } catch (_: URISyntaxException) {
orig orig
} }
} }
@@ -441,6 +469,7 @@ abstract class HttpSource : CatalogueSource {
* @param manga the manga * @param manga the manga
* @return url of the manga * @return url of the manga
*/ */
@Suppress("DEPRECATION")
open fun getMangaUrl(manga: SManga): String { open fun getMangaUrl(manga: SManga): String {
return mangaDetailsRequest(manga).url.toString() return mangaDetailsRequest(manga).url.toString()
} }
@@ -452,6 +481,7 @@ abstract class HttpSource : CatalogueSource {
* @param chapter the chapter * @param chapter the chapter
* @return url of the chapter * @return url of the chapter
*/ */
@Suppress("DEPRECATION")
open fun getChapterUrl(chapter: SChapter): String { open fun getChapterUrl(chapter: SChapter): String {
return pageListRequest(chapter).url.toString() return pageListRequest(chapter).url.toString()
} }
@@ -463,10 +493,6 @@ abstract class HttpSource : CatalogueSource {
* @param chapter the chapter to be added. * @param chapter the chapter to be added.
* @param manga the manga of the chapter. * @param manga the manga of the chapter.
*/ */
@Deprecated("All modifications should be done when constructing the chapter")
open fun prepareNewChapter(chapter: SChapter, manga: SManga) {} open fun prepareNewChapter(chapter: SChapter, manga: SManga) {}
/**
* Returns the list of filters for the source.
*/
override fun getFilterList() = FilterList()
} }
@@ -12,7 +12,10 @@ import org.jsoup.nodes.Element
/** /**
* A simple implementation for sources from a website using Jsoup, an HTML parser. * 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() { abstract class ParsedHttpSource : HttpSource() {
/** /**
@@ -20,6 +23,9 @@ abstract class ParsedHttpSource : HttpSource() {
* *
* @param response the response from the site. * @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 { override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup() val document = response.asJsoup()
@@ -58,6 +64,9 @@ abstract class ParsedHttpSource : HttpSource() {
* *
* @param response the response from the site. * @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 { override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup() val document = response.asJsoup()
@@ -96,6 +105,9 @@ abstract class ParsedHttpSource : HttpSource() {
* *
* @param response the response from the site. * @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 { override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup() val document = response.asJsoup()
@@ -134,6 +146,9 @@ abstract class ParsedHttpSource : HttpSource() {
* *
* @param response the response from the site. * @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 { override fun mangaDetailsParse(response: Response): SManga {
return mangaDetailsParse(response.asJsoup()) return mangaDetailsParse(response.asJsoup())
} }
@@ -150,6 +165,9 @@ abstract class ParsedHttpSource : HttpSource() {
* *
* @param response the response from the site. * @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<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup() val document = response.asJsoup()
return document.select(chapterListSelector()).map { chapterFromElement(it) } return document.select(chapterListSelector()).map { chapterFromElement(it) }
@@ -172,6 +190,9 @@ abstract class ParsedHttpSource : HttpSource() {
* *
* @param response the response from the site. * @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<Page> { override fun pageListParse(response: Response): List<Page> {
return pageListParse(response.asJsoup()) return pageListParse(response.asJsoup())
} }
@@ -188,6 +209,9 @@ abstract class ParsedHttpSource : HttpSource() {
* *
* @param response the response from the site. * @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 { override fun imageUrlParse(response: Response): String {
return imageUrlParse(response.asJsoup()) return imageUrlParse(response.asJsoup())
} }
@@ -2,7 +2,6 @@ package tachiyomi.source.local
import android.content.Context import android.content.Context
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.model.FilterList 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.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.SMangaUpdate
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.supervisorScope
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import logcat.LogPriority import logcat.LogPriority
@@ -50,7 +51,7 @@ actual class LocalSource(
private val context: Context, private val context: Context,
private val fileSystem: LocalSourceFileSystem, private val fileSystem: LocalSourceFileSystem,
private val coverManager: LocalCoverManager, private val coverManager: LocalCoverManager,
) : CatalogueSource, UnmeteredSource { ) : Source, UnmeteredSource {
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val xml: XML by injectLazy() private val xml: XML by injectLazy()
@@ -138,8 +139,19 @@ actual class LocalSource(
MangasPage(mangas, false) MangasPage(mangas, false)
} }
override suspend fun getMangaUpdate(
manga: SManga,
chapters: List<SChapter>,
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 // Manga details related
override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext { private suspend fun getMangaDetails(manga: SManga): SManga = withIOContext {
coverManager.find(manga.url)?.let { coverManager.find(manga.url)?.let {
manga.thumbnail_url = it.uri.toString() manga.thumbnail_url = it.uri.toString()
} }
@@ -253,7 +265,7 @@ actual class LocalSource(
} }
// Chapters // Chapters
override suspend fun getChapterList(manga: SManga): List<SChapter> = withIOContext { private suspend fun getChapterList(manga: SManga): List<SChapter> = withIOContext {
val chapters = fileSystem.getFilesInMangaDirectory(manga.url) val chapters = fileSystem.getFilesInMangaDirectory(manga.url)
// Only keep supported formats // Only keep supported formats
.filterNot { it.name.orEmpty().startsWith('.') } .filterNot { it.name.orEmpty().startsWith('.') }
@@ -1,6 +1,6 @@
package tachiyomi.source.local package tachiyomi.source.local
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.UnmeteredSource
expect class LocalSource : CatalogueSource, UnmeteredSource expect class LocalSource : Source, UnmeteredSource