package eu.kanade.tachiyomi.source import android.content.Context import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.source.online.all.NHentai import eu.kanade.tachiyomi.source.online.english.EightMuses import eu.kanade.tachiyomi.source.online.english.HBrowse import eu.kanade.tachiyomi.source.online.english.Pururin import eu.kanade.tachiyomi.source.online.english.Tsumino import exh.log.xLogD import exh.source.BlacklistedSources import exh.source.DelegatedHttpSource import exh.source.EH_SOURCE_ID import exh.source.EIGHTMUSES_SOURCE_ID import exh.source.EXH_SOURCE_ID import exh.source.EnhancedHttpSource import exh.source.HBROWSE_SOURCE_ID import exh.source.MERGED_SOURCE_ID import exh.source.PURURIN_SOURCE_ID import exh.source.TSUMINO_SOURCE_ID import exh.source.handleSourceLibrary import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import tachiyomi.domain.UnsortedPreferences import tachiyomi.domain.source.model.SourceData import tachiyomi.domain.source.model.StubSource import tachiyomi.domain.source.repository.SourceDataRepository import tachiyomi.domain.source.service.SourceManager import tachiyomi.source.local.LocalSource import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.util.concurrent.ConcurrentHashMap import kotlin.reflect.KClass class AndroidSourceManager( private val context: Context, private val extensionManager: ExtensionManager, private val sourceRepository: SourceDataRepository, ) : SourceManager { private val downloadManager: DownloadManager by injectLazy() private val scope = CoroutineScope(Job() + Dispatchers.IO) private val sourcesMapFlow = MutableStateFlow(ConcurrentHashMap()) private val stubSourcesMap = ConcurrentHashMap() override val catalogueSources: Flow> = sourcesMapFlow.map { it.values.filterIsInstance() } // SY --> private val preferences: UnsortedPreferences by injectLazy() // SY <-- init { scope.launch { extensionManager.installedExtensionsFlow // SY --> .combine(preferences.enableExhentai().changes()) { extensions, enableExhentai -> extensions to enableExhentai } // SY <-- .collectLatest { (extensions, enableExhentai) -> val mutableMap = ConcurrentHashMap( mapOf( LocalSource.ID to LocalSource( context, Injekt.get(), Injekt.get(), // SY --> preferences.allowLocalSourceHiddenFolders()::get, // SY <-- ), ), ).apply { // SY --> put(EH_SOURCE_ID, EHentai(EH_SOURCE_ID, false, context)) if (enableExhentai) { put(EXH_SOURCE_ID, EHentai(EXH_SOURCE_ID, true, context)) } put(MERGED_SOURCE_ID, MergedSource()) // SY <-- } extensions.forEach { extension -> extension.sources.mapNotNull { it.toInternalSource() }.forEach { mutableMap[it.id] = it registerStubSource(it.toSourceData()) } } sourcesMapFlow.value = mutableMap } } scope.launch { sourceRepository.subscribeAll() .collectLatest { sources -> val mutableMap = stubSourcesMap.toMutableMap() sources.forEach { mutableMap[it.id] = StubSource(it) } } } } private fun Source.toInternalSource(): Source? { // EXH --> val sourceQName = this::class.qualifiedName val factories = DELEGATED_SOURCES.entries.filter { it.value.factory }.map { it.value.originalSourceQualifiedClassName } val delegate = if (sourceQName != null) { val matched = factories.find { sourceQName.startsWith(it) } if (matched != null) { DELEGATED_SOURCES[matched] } else { DELEGATED_SOURCES[sourceQName] } } else { null } val newSource = if (this is HttpSource && delegate != null) { xLogD("Delegating source: %s -> %s!", sourceQName, delegate.newSourceClass.qualifiedName) val enhancedSource = EnhancedHttpSource( this, delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(this, context), ) currentDelegatedSources[enhancedSource.originalSource.id] = DelegatedSource( enhancedSource.originalSource.name, enhancedSource.originalSource.id, enhancedSource.originalSource::class.qualifiedName ?: delegate.originalSourceQualifiedClassName, (enhancedSource.enhancedSource as DelegatedHttpSource)::class, delegate.factory, ) enhancedSource } else { this } return if (id in BlacklistedSources.BLACKLISTED_EXT_SOURCES) { xLogD("Removing blacklisted source: (id: %s, name: %s, lang: %s)!", id, name, (this as? CatalogueSource)?.lang) null } else { newSource } // EXH <-- } override fun get(sourceKey: Long): Source? { return sourcesMapFlow.value[sourceKey] } override fun getOrStub(sourceKey: Long): Source { return sourcesMapFlow.value[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) { runBlocking { createStubSource(sourceKey) } } } override fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance() override fun getCatalogueSources() = sourcesMapFlow.value.values.filterIsInstance() override fun getStubSources(): List { val onlineSourceIds = getOnlineSources().map { it.id } return stubSourcesMap.values.filterNot { it.id in onlineSourceIds } } // SY --> override fun getVisibleOnlineSources() = sourcesMapFlow.value.values.filterIsInstance().filter { it.id !in BlacklistedSources.HIDDEN_SOURCES } override fun getVisibleCatalogueSources() = sourcesMapFlow.value.values.filterIsInstance().filter { it.id !in BlacklistedSources.HIDDEN_SOURCES } fun getDelegatedCatalogueSources() = sourcesMapFlow.value.values.filterIsInstance().mapNotNull { enhancedHttpSource -> enhancedHttpSource.enhancedSource as? DelegatedHttpSource } // SY <-- private fun registerStubSource(sourceData: SourceData) { scope.launch { val (id, lang, name) = sourceData val dbSourceData = sourceRepository.getSourceData(id) if (dbSourceData == sourceData) return@launch sourceRepository.upsertSourceData(id, lang, name) if (dbSourceData != null) { downloadManager.renameSource(StubSource(dbSourceData), StubSource(sourceData)) } } } private suspend fun createStubSource(id: Long): StubSource { sourceRepository.getSourceData(id)?.let { return StubSource(it) } extensionManager.getSourceData(id)?.let { registerStubSource(it) return StubSource(it) } return StubSource(SourceData(id, "", "")) } // SY --> companion object { private const val fillInSourceId = Long.MAX_VALUE val DELEGATED_SOURCES = listOf( DelegatedSource( "Pururin", PURURIN_SOURCE_ID, "eu.kanade.tachiyomi.extension.en.pururin.Pururin", Pururin::class, ), DelegatedSource( "Tsumino", TSUMINO_SOURCE_ID, "eu.kanade.tachiyomi.extension.en.tsumino.Tsumino", Tsumino::class, ), DelegatedSource( "MangaDex", fillInSourceId, "eu.kanade.tachiyomi.extension.all.mangadex", MangaDex::class, true, ), DelegatedSource( "HBrowse", HBROWSE_SOURCE_ID, "eu.kanade.tachiyomi.extension.en.hbrowse.HBrowse", HBrowse::class, ), DelegatedSource( "8Muses", EIGHTMUSES_SOURCE_ID, "eu.kanade.tachiyomi.extension.en.eightmuses.EightMuses", EightMuses::class, ), DelegatedSource( "NHentai", fillInSourceId, "eu.kanade.tachiyomi.extension.all.nhentai.NHentai", NHentai::class, true, ), ).associateBy { it.originalSourceQualifiedClassName } val currentDelegatedSources: MutableMap = ListenMutableMap(mutableMapOf(), ::handleSourceLibrary) data class DelegatedSource( val sourceName: String, val sourceId: Long, val originalSourceQualifiedClassName: String, val newSourceClass: KClass, val factory: Boolean = false, ) } private class ListenMutableMap( private val internalMap: MutableMap, private val listener: () -> Unit, ) : MutableMap by internalMap { override fun clear() { val clearResult = internalMap.clear() listener() return clearResult } override fun put(key: K, value: V): V? { val putResult = internalMap.put(key, value) if (putResult == null) { listener() } return putResult } override fun putAll(from: Map) { internalMap.putAll(from) listener() } override fun remove(key: K): V? { val removeResult = internalMap.remove(key) if (removeResult != null) { listener() } return removeResult } } // SY <-- }