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