Support using a CatalogueSource instead of only HttpSources (#219)

This commit is contained in:
Mitchell Syer
2021-10-10 16:31:04 -04:00
committed by GitHub
parent fd715a3f92
commit 45808cd530
9 changed files with 136 additions and 155 deletions

View File

@@ -9,6 +9,7 @@ package suwayomi.tachidesk.manga.impl
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.SortOrder
@@ -20,7 +21,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.Manga.getManga
import suwayomi.tachidesk.manga.impl.Page.getPageName
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.getChapterDir
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
@@ -53,7 +54,7 @@ object Chapter {
private suspend fun getSourceChapters(mangaId: Int): List<ChapterDataClass> {
val manga = getManga(mangaId)
val source = getHttpSource(manga.sourceId.toLong())
val source = getCatalogueSourceOrStub(manga.sourceId.toLong())
val sManga = SManga.create().apply {
title = manga.title
@@ -64,7 +65,7 @@ object Chapter {
// Recognize number for new chapters.
chapterList.forEach {
source.prepareNewChapter(it, sManga)
(source as? HttpSource)?.prepareNewChapter(it, sManga)
ChapterRecognition.parseChapterNumber(it, sManga)
}
@@ -169,7 +170,7 @@ object Chapter {
}
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val pageList = source.fetchPageList(
SChapter.create().apply {

View File

@@ -8,7 +8,9 @@ package suwayomi.tachidesk.manga.impl
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.local.LocalSource
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
@@ -19,11 +21,12 @@ import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
import suwayomi.tachidesk.manga.impl.Source.getSource
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.network.await
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import suwayomi.tachidesk.manga.impl.util.updateMangaDownloadDir
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.dataclass.toGenreList
@@ -31,6 +34,8 @@ import suwayomi.tachidesk.manga.model.table.MangaMetaTable
import suwayomi.tachidesk.manga.model.table.MangaStatus
import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.server.ApplicationDirs
import java.io.File
import java.io.IOException
import java.io.InputStream
object Manga {
@@ -68,7 +73,7 @@ object Manga {
false
)
} else { // initialize manga
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val sManga = SManga.create().apply {
url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title]
@@ -95,11 +100,9 @@ object Manga {
if (sManga.thumbnail_url != null && sManga.thumbnail_url.orEmpty().isNotEmpty())
it[MangaTable.thumbnail_url] = sManga.thumbnail_url
it[MangaTable.realUrl] = try {
source.mangaDetailsRequest(sManga).url.toString()
} catch (e: Exception) {
null
}
it[MangaTable.realUrl] = runCatching {
(source as? HttpSource)?.mangaDetailsRequest(sManga)?.url?.toString()
}.getOrNull()
}
}
@@ -164,25 +167,39 @@ object Manga {
val saveDir = applicationDirs.mangaThumbnailsRoot
val fileName = mangaId.toString()
return getImageResponse(saveDir, fileName, useCache) {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val sourceId = mangaEntry[MangaTable.sourceReference]
val sourceId = mangaEntry[MangaTable.sourceReference]
val source = getHttpSource(sourceId)
return when (val source = getCatalogueSourceOrStub(sourceId)) {
is HttpSource -> getImageResponse(saveDir, fileName, useCache) {
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
?: if (!mangaEntry[MangaTable.initialized]) {
// initialize then try again
getManga(mangaId)
transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }[MangaTable.thumbnail_url]!!
} else {
// source provides no thumbnail url for this manga
throw NullPointerException()
}
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
?: if (!mangaEntry[MangaTable.initialized]) {
// initialize then try again
getManga(mangaId)
transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }[MangaTable.thumbnail_url]!!
} else {
// source provides no thumbnail url for this manga
throw NullPointerException()
}
source.client.newCall(
GET(thumbnailUrl, source.headers)
).await()
source.client.newCall(
GET(thumbnailUrl, source.headers)
).await()
}
is LocalSource -> {
val imageFile = mangaEntry[MangaTable.thumbnail_url]?.let {
val file = File(it)
if (file.exists()) {
file
} else {
null
}
} ?: throw IOException("Thumbnail does not exist")
val contentType = ImageUtil.findImageType { imageFile.inputStream() }?.mime
?: "image/jpeg"
imageFile.inputStream() to contentType
}
else -> throw IllegalArgumentException("Unknown source")
}
}

View File

@@ -13,7 +13,7 @@ import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.manga.impl.Manga.getMangaMetaMap
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
@@ -27,14 +27,15 @@ object MangaList {
}
suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
val source = getHttpSource(sourceId)
val source = getCatalogueSourceOrStub(sourceId)
val mangasPage = if (popular) {
source.fetchPopularManga(pageNum).awaitSingle()
} else {
if (source.supportsLatest)
if (source.supportsLatest) {
source.fetchLatestUpdates(pageNum).awaitSingle()
else
} else {
throw Exception("Source $source doesn't support latest")
}
}
return mangasPage.processEntries(sourceId)
}

View File

@@ -17,11 +17,12 @@ import org.jetbrains.exposed.sql.update
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.getChapterDir
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.PageTable
@@ -43,7 +44,7 @@ object Page {
suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int, useCache: Boolean = true): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val chapterEntry = transaction {
ChapterTable.select {
(ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId)
@@ -61,19 +62,20 @@ object Page {
)
// we treat Local source differently
if (mangaEntry[MangaTable.sourceReference] == LocalSource.ID) {
if (source.id == LocalSource.ID) {
// is of archive format
if (LocalSource.pageCache.containsKey(chapterEntry[ChapterTable.url])) {
val pageStream = LocalSource.pageCache[chapterEntry[ChapterTable.url]]!![index]()
return pageStream to "image/jpeg"
val pageStream = LocalSource.pageCache[chapterEntry[ChapterTable.url]]!![index]
return pageStream() to (ImageUtil.findImageType { pageStream() }?.mime ?: "image/jpeg")
}
// is of directory format
return ImageResponse.getNoCacheImageResponse {
source.fetchImage(tachiyomiPage).awaitSingle()
}
val imageFile = File(tachiyomiPage.imageUrl!!)
return imageFile.inputStream() to (ImageUtil.findImageType { imageFile.inputStream() }?.mime ?: "image/jpeg")
}
source as HttpSource
if (pageEntry[PageTable.imageUrl] == null) {
val trueImageUrl = getTrueImageUrl(tachiyomiPage, source)
transaction {

View File

@@ -10,13 +10,13 @@ package suwayomi.tachidesk.manga.impl
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import suwayomi.tachidesk.manga.impl.MangaList.processEntries
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
object Search {
suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
val source = getHttpSource(sourceId)
val source = getCatalogueSourceOrStub(sourceId)
val searchManga = source.fetchSearchManga(pageNum, searchTerm, getFilterListOf(sourceId)).awaitSingle()
return searchManga.processEntries(sourceId)
}
@@ -25,7 +25,7 @@ object Search {
private fun getFilterListOf(sourceId: Long, reset: Boolean = false): FilterList {
if (reset || !filterListCache.containsKey(sourceId)) {
filterListCache[sourceId] = getHttpSource(sourceId).getFilterList()
filterListCache[sourceId] = getCatalogueSourceOrStub(sourceId).getFilterList()
}
return filterListCache[sourceId]!!
}

View File

@@ -12,7 +12,6 @@ import android.content.Context
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.getPreferenceKey
import eu.kanade.tachiyomi.source.local.LocalSource
import mu.KotlinLogging
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
@@ -21,7 +20,8 @@ import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIconUrl
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getCatalogueSource
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.invalidateSourceCache
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
import suwayomi.tachidesk.manga.model.table.ExtensionTable
@@ -36,7 +36,7 @@ object Source {
fun getSourceList(): List<SourceDataClass> {
return transaction {
SourceTable.selectAll().map {
val httpSource = getHttpSource(it[SourceTable.id].value)
val catalogueSource = getCatalogueSourceOrStub(it[SourceTable.id].value)
val sourceExtension = ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()
SourceDataClass(
@@ -44,10 +44,10 @@ object Source {
it[SourceTable.name],
it[SourceTable.lang],
getExtensionIconUrl(sourceExtension[ExtensionTable.apkName]),
httpSource.supportsLatest,
httpSource is ConfigurableSource,
catalogueSource.supportsLatest,
catalogueSource is ConfigurableSource,
it[SourceTable.isNsfw],
httpSource.toString(),
catalogueSource.toString(),
)
}
}
@@ -55,13 +55,8 @@ object Source {
fun getSource(sourceId: Long): SourceDataClass { // all the data extracted fresh form the source instance
return transaction {
if (sourceId == LocalSource.ID) {
// initialize local source
getHttpSource(sourceId)
}
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
val httpSource = source?.let { getHttpSource(sourceId) }
val catalogueSource = source?.let { getCatalogueSource(sourceId) }
val extension = source?.let {
ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()
}
@@ -75,10 +70,10 @@ object Source {
extension!![ExtensionTable.apkName]
)
},
httpSource?.supportsLatest,
httpSource?.let { it is ConfigurableSource },
catalogueSource?.supportsLatest,
catalogueSource?.let { it is ConfigurableSource },
source?.get(SourceTable.isNsfw),
httpSource?.toString()
catalogueSource?.toString()
)
}
}
@@ -103,7 +98,7 @@ object Source {
* Gets a source's PreferenceScreen, puts the result into [preferenceScreenMap]
*/
fun getSourcePreferences(sourceId: Long): List<PreferenceObject> {
val source = getHttpSource(sourceId)
val source = getCatalogueSourceOrStub(sourceId)
if (source is ConfigurableSource) {
val sourceShardPreferences =

View File

@@ -22,7 +22,7 @@ private val applicationDirs by DI.global.instance<ApplicationDirs>()
fun getMangaDir(mangaId: Int): String {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = GetHttpSource.getHttpSource(mangaEntry[MangaTable.sourceReference])
val source = GetHttpSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val sourceDir = source.toString()
val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title])
@@ -46,7 +46,7 @@ fun getChapterDir(mangaId: Int, chapterId: Int): String {
/** return value says if rename/move was successful */
fun updateMangaDownloadDir(mangaId: Int, newTitle: String): Boolean {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = GetHttpSource.getHttpSource(mangaEntry[MangaTable.sourceReference])
val source = GetHttpSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val sourceDir = source.toString()
val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title])

View File

@@ -7,15 +7,22 @@ package suwayomi.tachidesk.manga.impl.util
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.local.LocalSource
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.online.HttpSource
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import rx.Observable
import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources
import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable
@@ -23,22 +30,56 @@ import suwayomi.tachidesk.server.ApplicationDirs
import java.util.concurrent.ConcurrentHashMap
object GetHttpSource {
private val sourceCache = ConcurrentHashMap<Long, HttpSource>()
private val sourceCache = ConcurrentHashMap<Long, CatalogueSource>(
mapOf(LocalSource.ID to LocalSource())
)
private val applicationDirs by DI.global.instance<ApplicationDirs>()
fun getHttpSource(sourceId: Long): HttpSource {
val cachedResult: HttpSource? = sourceCache[sourceId]
class StubSource(override val id: Long) : CatalogueSource {
override val lang: String = "other"
override val supportsLatest: Boolean = false
override val name: String
get() = id.toString()
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return Observable.error(getSourceNotInstalledException())
}
override fun getFilterList(): FilterList {
return FilterList()
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return Observable.error(getSourceNotInstalledException())
}
override fun toString(): String {
return name
}
private fun getSourceNotInstalledException(): SourceNotInstalledException {
return SourceNotInstalledException(id)
}
inner class SourceNotInstalledException(val id: Long) :
Exception("Source not installed: $id")
}
fun getCatalogueSource(sourceId: Long): CatalogueSource? {
val cachedResult: CatalogueSource? = sourceCache[sourceId]
if (cachedResult != null) {
return cachedResult
}
val sourceRecord = transaction {
SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!!
}
if (sourceId == LocalSource.ID) {
return LocalSource()
}
SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
} ?: return null
val extensionId = sourceRecord[SourceTable.extension]
val extensionRecord = transaction {
@@ -60,6 +101,10 @@ object GetHttpSource {
return sourceCache[sourceId]!!
}
fun getCatalogueSourceOrStub(sourceId: Long): CatalogueSource {
return getCatalogueSource(sourceId) ?: StubSource(sourceId)
}
fun invalidateSourceCache(sourceId: Long) {
sourceCache.remove(sourceId)
}