Adapt source implementation for TachiyomiX 1.6 (#3243)
This commit is contained in:
@@ -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,
|
||||
) {
|
||||
|
||||
+1
-1
@@ -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()) {
|
||||
|
||||
+1
-1
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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 {
|
||||
|
||||
+5
-5
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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 = {
|
||||
|
||||
-11
@@ -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
|
||||
|
||||
|
||||
+2
-2
@@ -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 }
|
||||
}
|
||||
|
||||
+9
-10
@@ -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 website’s home page URL. Defaults to [baseUrl].
|
||||
*/
|
||||
open fun getHomeUrl(): String = baseUrl
|
||||
|
||||
/**
|
||||
* Version id used to generate the source id. If the site completely changes and urls are
|
||||
* incompatible, you may increase this value and it'll be considered as a new source.
|
||||
*/
|
||||
open val versionId = 1
|
||||
open val versionId: Int = 1
|
||||
|
||||
/**
|
||||
* ID of the source. By default it uses a generated id using the first 16 characters (64 bits)
|
||||
@@ -54,7 +66,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* Note: the generated ID sets the sign bit to `0`.
|
||||
*/
|
||||
override val id by lazy { generateId(name, lang, versionId) }
|
||||
override val id: Long by lazy { generateId(name, lang, versionId) }
|
||||
|
||||
/**
|
||||
* Headers used for requests.
|
||||
@@ -64,8 +76,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
/**
|
||||
* Default network client for doing requests.
|
||||
*/
|
||||
open val client: OkHttpClient
|
||||
get() = network.client
|
||||
open val client: OkHttpClient get() = network.client
|
||||
|
||||
/**
|
||||
* Generates a unique ID for the source based on the provided [name], [lang] and
|
||||
@@ -93,14 +104,14 @@ abstract class HttpSource : CatalogueSource {
|
||||
/**
|
||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||
*/
|
||||
protected open fun headersBuilder() = Headers.Builder().apply {
|
||||
protected open fun headersBuilder(): Headers.Builder = Headers.Builder().apply {
|
||||
add("User-Agent", network.defaultUserAgentProvider())
|
||||
}
|
||||
|
||||
/**
|
||||
* Visible name of the source.
|
||||
*/
|
||||
override fun toString() = "$name (${lang.uppercase()})"
|
||||
override fun toString(): String = "$name (${lang.uppercase()})"
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||
@@ -109,7 +120,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga"))
|
||||
@Deprecated("Use the suspend API instead", ReplaceWith("getPopularManga"))
|
||||
override fun fetchPopularManga(page: Int): Observable<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()
|
||||
}
|
||||
|
||||
+25
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user