fix(recommendation): Fix null RecommendationSource & refactoring (#1000)

* Remove redundant condition & refactor class

* Batch networkToLocalManga

* Fix null source

* Simplify by creating RecommendationSource to match with upstream

* Add comment
This commit is contained in:
Cuong-Tran 2025-06-12 15:39:09 +07:00 committed by GitHub
parent 9a31ffcc41
commit 6c530e4fca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 87 additions and 58 deletions

View file

@ -11,7 +11,7 @@ import exh.md.service.MangaDexService
import exh.md.service.SimilarService
import exh.md.utils.MdLang
import exh.recs.sources.RecommendationPagingSource
import exh.recs.sources.SourceCatalogue
import exh.recs.sources.RecommendationSource
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import tachiyomi.data.source.NoResultsException
@ -26,12 +26,12 @@ import uy.kohesive.injekt.api.get
internal class MangaDexSimilarPagingSource(
manga: Manga,
// KMK -->
private val sourceCatalogue: SourceCatalogue,
private val recommendationSource: RecommendationSource,
// KMK <--
) : RecommendationPagingSource(
manga,
// KMK -->
sourceCatalogue.source,
recommendationSource,
// KMK <--
) {
@ -43,12 +43,12 @@ internal class MangaDexSimilarPagingSource(
override val associatedSourceId: Long
// KMK -->
get() = sourceCatalogue.sourceId
get() = recommendationSource.id
private val client by lazy { Injekt.get<NetworkHelper>().client }
private val mdLang by lazy {
sourceCatalogue.source?.lang?.let { lang ->
recommendationSource.lang.let { lang ->
MdLang.fromExt(lang)
} ?: MdLang.ENGLISH
}

View file

@ -112,7 +112,7 @@ class BrowseRecommendsScreen(
} else {
// KMK <--
val title = remember {
val recSource = screenModel.recommendationSource
val recSource = screenModel.recommendationPagingSource
"${recSource.name} (${recSource.category.getString(context)})"
}

View file

@ -4,8 +4,9 @@ import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
import exh.metadata.metadata.RaisedSearchMetadata
import exh.recs.sources.RECOMMENDS_SOURCE
import exh.recs.sources.RecommendationPagingSource
import exh.recs.sources.SourceCatalogue
import exh.recs.sources.RecommendationSource
import exh.recs.sources.StaticResultPagingSource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
@ -40,20 +41,20 @@ class BrowseRecommendsScreenModel(
}
}
val recommendationSource: RecommendationPagingSource
val recommendationPagingSource: RecommendationPagingSource
get() = when (args) {
is BrowseRecommendsScreen.Args.MergedSourceMangas -> StaticResultPagingSource(args.results)
is BrowseRecommendsScreen.Args.SingleSourceManga -> RecommendationPagingSource.createSources(
manga ?: runBlocking(Dispatchers.IO) { getManga.await(args.mangaId)!! },
// KMK -->
SourceCatalogue(sourceId),
RecommendationSource(sourceId),
// KMK <--
).first {
it::class.qualifiedName == args.recommendationSourceName
}
}
override fun createSourcePagingSource(query: String, filters: FilterList) = recommendationSource
override fun createSourcePagingSource(query: String, filters: FilterList) = recommendationPagingSource
override fun Flow<Manga>.combineMetadata(metadata: RaisedSearchMetadata?): Flow<Pair<Manga, RaisedSearchMetadata?>> {
// Overridden to prevent our custom metadata from being replaced from a cache

View file

@ -20,6 +20,7 @@ import exh.recs.RecommendsScreen.Args.MergedSourceMangas
import exh.recs.RecommendsScreen.Args.SingleSourceManga
import exh.recs.batch.RankedSearchResults
import exh.recs.components.RecommendsScreen
import exh.recs.sources.RECOMMENDS_SOURCE
import exh.recs.sources.StaticResultPagingSource
import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.sy.SYMR

View file

@ -5,8 +5,9 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.produceState
import cafe.adriel.voyager.core.model.StateScreenModel
import eu.kanade.presentation.util.ioCoroutineScope
import exh.recs.sources.RECOMMENDS_SOURCE
import exh.recs.sources.RecommendationPagingSource
import exh.recs.sources.SourceCatalogue
import exh.recs.sources.RecommendationSource
import exh.recs.sources.StaticResultPagingSource
import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.mutate
@ -55,7 +56,7 @@ open class RecommendsScreenModel(
RecommendationPagingSource.createSources(
manga,
// KMK -->
SourceCatalogue(args.sourceId),
RecommendationSource(args.sourceId),
// KMK <--
)
}
@ -163,5 +164,3 @@ sealed interface RecommendationItemResult {
return !onlyShowHasResults || (this is Success && !this.isEmpty)
}
}
const val RECOMMENDS_SOURCE = -1L

View file

@ -9,7 +9,7 @@ import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.source.model.SManga
import exh.log.xLog
import exh.recs.sources.RecommendationPagingSource
import exh.recs.sources.SourceCatalogue
import exh.recs.sources.RecommendationSource
import exh.recs.sources.TrackerRecommendationPagingSource
import exh.smartsearch.SmartLibrarySearchEngine
import exh.util.ThrottleManager
@ -101,7 +101,7 @@ class RecommendationSearchHelper(val context: Context) {
val jobs = RecommendationPagingSource.createSources(
sourceManga,
// KMK -->
SourceCatalogue(sourceManga.source),
RecommendationSource(sourceManga.source),
// KMK <--
).mapNotNull { source ->
// Apply source filters
@ -198,13 +198,16 @@ class RecommendationSearchHelper(val context: Context) {
return this
}
return filterNot { manga ->
// Source recommendations can be directly resolved, if the recommendation is from the same source
recSource.associatedSourceId?.let { srcId ->
return@filterNot networkToLocalManga(manga.toDomainManga(srcId))
.let { local -> libraryManga.any { it.id == local.id } }
}
// KMK -->
// Source recommendations can be directly resolved, if the recommendation is from the same source
recSource.associatedSourceId?.let { srcId ->
return networkToLocalManga(map { it.toDomainManga(srcId) })
.filterNot { local -> libraryManga.any { it.id == local.id } }
.map { it.toSManga() }
}
// KMK <--
return filterNot { manga ->
// Tracker recommendations can be resolved by checking if the tracker is attached to the recommendation
if (recSource is TrackerRecommendationPagingSource) {
recSource.associatedTrackerId?.let { trackerId ->

View file

@ -5,7 +5,6 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.coroutines.coroutineScope
@ -23,17 +22,15 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
fun CatalogueSource.isComickSource() = name == "Comick"
internal class ComickPagingSource(
manga: Manga,
// KMK -->
private val sourceCatalogue: SourceCatalogue,
private val recommendationSource: RecommendationSource,
// KMK <--
) : RecommendationPagingSource(
manga,
// KMK -->
sourceCatalogue.source,
recommendationSource,
// KMK <--
) {
@ -45,7 +42,7 @@ internal class ComickPagingSource(
override val associatedSourceId: Long
// KMK -->
get() = sourceCatalogue.sourceId
get() = recommendationSource.id
// KMK <--
private val client by lazy { Injekt.get<NetworkHelper>().client }

View file

@ -4,13 +4,13 @@ import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import exh.md.similar.MangaDexSimilarPagingSource
import exh.pref.DelegateSourcePreferences
import exh.source.COMICK_IDS
import exh.source.MANGADEX_IDS
import exh.source.isMdBasedSource
import kotlinx.serialization.json.Json
import logcat.LogPriority
import tachiyomi.core.common.util.system.logcat
@ -29,7 +29,9 @@ import uy.kohesive.injekt.injectLazy
*/
abstract class RecommendationPagingSource(
protected val manga: Manga,
source: CatalogueSource? = null,
// KMK -->
source: RecommendationSource = RecommendationSource(),
// KMK <--
) : BaseSourcePagingSource(source) {
// Display name
abstract val name: String
@ -49,12 +51,9 @@ abstract class RecommendationPagingSource(
internal fun createSources(
manga: Manga,
// KMK -->
sourceCatalogue: SourceCatalogue,
recommendationSource: RecommendationSource,
// KMK <--
): List<RecommendationPagingSource> {
// KMK -->
val source = sourceCatalogue.source
// KMK <--
return buildList {
add(AniListPagingSource(manga))
add(MangaUpdatesCommunityPagingSource(manga))
@ -64,16 +63,14 @@ abstract class RecommendationPagingSource(
// Only include MangaDex if the delegate sources are enabled and the source is MD-based
if (
// KMK -->
sourceCatalogue.isMangaDexSource() ||
recommendationSource.isMangaDexSource()
// KMK <--
source?.isMdBasedSource() /* KMK --> */ == true /* KMK <-- */ &&
Injekt.get<DelegateSourcePreferences>().delegateSources().get()
) {
add(
MangaDexSimilarPagingSource(
manga,
// KMK -->
sourceCatalogue,
recommendationSource,
// KMK <--
),
)
@ -82,15 +79,14 @@ abstract class RecommendationPagingSource(
// Only include Comick if the source manga is from there
if (
// KMK -->
sourceCatalogue.isComickSource() ||
recommendationSource.isComickSource()
// KMK <--
source?.isComickSource() /* KMK --> */ == true /* KMK <-- */
) {
add(
ComickPagingSource(
manga,
// KMK -->
sourceCatalogue,
recommendationSource,
// KMK <--
),
)
@ -151,15 +147,45 @@ abstract class TrackerRecommendationPagingSource(
}
// KMK -->
internal class SourceCatalogue(
internal val sourceId: Long,
class RecommendationSource(
override val id: Long = RECOMMENDS_SOURCE,
sourceManager: SourceManager = Injekt.get(),
) {
val source = sourceManager.get(sourceId)
?.let { it as CatalogueSource }
) : CatalogueSource {
private val delegate by lazy {
sourceManager.get(id)
?.let { it as CatalogueSource }
}
fun isComickSource(): Boolean = sourceId in COMICK_IDS
fun isComickSource(): Boolean = id in COMICK_IDS
fun isMangaDexSource(): Boolean = id in MANGADEX_IDS
fun isMangaDexSource(): Boolean = sourceId in MANGADEX_IDS
override val name: String by lazy { delegate?.name ?: "Recommends Source" }
override val lang: String by lazy { delegate?.lang ?: "all" }
override val supportsLatest by lazy { delegate?.supportsLatest ?: false }
override suspend fun getMangaDetails(manga: SManga) =
delegate?.getMangaDetails(manga)
?: throw UnsupportedOperationException()
override suspend fun getChapterList(manga: SManga) =
delegate?.getChapterList(manga)
?: throw UnsupportedOperationException()
override suspend fun getPageList(chapter: SChapter) =
delegate?.getPageList(chapter)
?: throw UnsupportedOperationException()
override suspend fun getPopularManga(page: Int) =
delegate?.getPopularManga(page)
?: throw UnsupportedOperationException()
override suspend fun getLatestUpdates(page: Int) =
delegate?.getLatestUpdates(page)
?: throw UnsupportedOperationException()
override suspend fun getSearchManga(page: Int, query: String, filters: FilterList) =
delegate?.getSearchManga(page, query, filters)
?: throw UnsupportedOperationException()
override fun getFilterList() =
delegate?.getFilterList()
?: throw UnsupportedOperationException()
}
const val RECOMMENDS_SOURCE = -1L
// KMK <--

View file

@ -21,7 +21,7 @@ abstract class EHentaiPagingSource(
val metadata = mangasPage.mangasMetadata
val manga = mangasPage.mangas
.mapIndexed { index, sManga -> sManga.toDomainManga(source!!.id) to metadata.getOrNull(index) }
.mapIndexed { index, sManga -> sManga.toDomainManga(source.id) to metadata.getOrNull(index) }
.filter { seenManga.add(it.first.url) }
// KMK -->
.let { pairs -> networkToLocalManga(pairs.map { it.first }).zip(pairs.map { it.second }) }
@ -41,18 +41,18 @@ class EHentaiSearchPagingSource(
val filters: FilterList,
) : EHentaiPagingSource(source) {
override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source!!.getSearchManga(currentPage, query.sanitize(), filters)
return source.getSearchManga(currentPage, query.sanitize(), filters)
}
}
class EHentaiPopularPagingSource(source: CatalogueSource) : EHentaiPagingSource(source) {
override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source!!.getPopularManga(currentPage)
return source.getPopularManga(currentPage)
}
}
class EHentaiLatestPagingSource(source: CatalogueSource) : EHentaiPagingSource(source) {
override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source!!.getLatestUpdates(currentPage)
return source.getLatestUpdates(currentPage)
}
}

View file

@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
import exh.log.xLogE
import exh.metadata.metadata.RaisedSearchMetadata
import mihon.domain.manga.model.toDomainManga
import tachiyomi.core.common.util.QuerySanitizer.sanitize
@ -21,24 +22,24 @@ class SourceSearchPagingSource(
private val filters: FilterList,
) : BaseSourcePagingSource(source) {
override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source!!.getSearchManga(currentPage, query.sanitize(), filters)
return source.getSearchManga(currentPage, query.sanitize(), filters)
}
}
class SourcePopularPagingSource(source: CatalogueSource) : BaseSourcePagingSource(source) {
override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source!!.getPopularManga(currentPage)
return source.getPopularManga(currentPage)
}
}
class SourceLatestPagingSource(source: CatalogueSource) : BaseSourcePagingSource(source) {
override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source!!.getLatestUpdates(currentPage)
return source.getLatestUpdates(currentPage)
}
}
abstract class BaseSourcePagingSource(
protected val source: CatalogueSource?,
protected val source: CatalogueSource,
protected val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
) : SourcePagingSource() {
@ -62,6 +63,7 @@ abstract class BaseSourcePagingSource(
getPageLoadResult(params, mangasPage)
// SY <--
} catch (e: Exception) {
xLogE("${this::class.simpleName}: Failed to load paging source", e)
LoadResult.Error(e)
}
}
@ -83,7 +85,7 @@ abstract class BaseSourcePagingSource(
val manga = mangasPage.mangas
// SY -->
.mapIndexed { index, sManga -> sManga.toDomainManga(source!!.id) to metadata.getOrNull(index) }
.mapIndexed { index, sManga -> sManga.toDomainManga(source.id) to metadata.getOrNull(index) }
.filter { seenManga.add(it.first.url) }
// KMK -->
.let { pairs -> networkToLocalManga(pairs.map { it.first }).zip(pairs.map { it.second }) }