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

@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.source.local package eu.kanade.tachiyomi.source.local
import com.github.junrar.Archive import com.github.junrar.Archive
import eu.kanade.tachiyomi.source.local.FileSystemInterceptor.fakeUrlFrom import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.local.LocalSource.Format.Directory import eu.kanade.tachiyomi.source.local.LocalSource.Format.Directory
import eu.kanade.tachiyomi.source.local.LocalSource.Format.Epub import eu.kanade.tachiyomi.source.local.LocalSource.Format.Epub
import eu.kanade.tachiyomi.source.local.LocalSource.Format.Rar import eu.kanade.tachiyomi.source.local.LocalSource.Format.Rar
@@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.EpubFile import eu.kanade.tachiyomi.util.storage.EpubFile
@@ -27,15 +26,6 @@ import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import mu.KotlinLogging import mu.KotlinLogging
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.asResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import okio.buffer
import okio.source
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
@@ -51,14 +41,12 @@ import suwayomi.tachidesk.server.ApplicationDirs
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.InputStream import java.io.InputStream
import java.net.URLDecoder
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.zip.ZipFile import java.util.zip.ZipFile
class LocalSource : HttpSource() { class LocalSource : CatalogueSource {
companion object { companion object {
const val ID = 0L const val ID = 0L
const val LANG = "localsourcelang" const val LANG = "localsourcelang"
@@ -133,13 +121,8 @@ class LocalSource : HttpSource() {
override val id = ID override val id = ID
override val name = NAME override val name = NAME
override val lang = LANG override val lang = LANG
override val baseUrl: String = ""
override val supportsLatest = true override val supportsLatest = true
override val client: OkHttpClient = super.client.newBuilder()
.addInterceptor(FileSystemInterceptor)
.build()
private val json: Json by injectLazy() private val json: Json by injectLazy()
override fun toString() = name override fun toString() = name
@@ -181,7 +164,7 @@ class LocalSource : HttpSource() {
// Try to find the cover // Try to find the cover
val cover = getCoverFile(File("${applicationDirs.localMangaRoot}/$url")) val cover = getCoverFile(File("${applicationDirs.localMangaRoot}/$url"))
if (cover != null && cover.exists()) { if (cover != null && cover.exists()) {
thumbnail_url = fakeUrlFrom(cover.absolutePath) thumbnail_url = cover.absolutePath
} }
val chapters = fetchChapterList(this).toBlocking().first() val chapters = fetchChapterList(this).toBlocking().first()
@@ -197,8 +180,7 @@ class LocalSource : HttpSource() {
// Copy the cover from the first chapter found. // Copy the cover from the first chapter found.
if (thumbnail_url == null) { if (thumbnail_url == null) {
try { try {
val dest = updateCover(chapter, this) thumbnail_url = updateCover(chapter, this)?.absolutePath
thumbnail_url = dest?.absolutePath?.let { fakeUrlFrom(it) }
} catch (e: Exception) { } catch (e: Exception) {
logger.error { e } logger.error { e }
} }
@@ -311,7 +293,7 @@ class LocalSource : HttpSource() {
chapterFile.listFiles().orEmpty().sortedBy { it.name }.mapIndexed { index, page -> chapterFile.listFiles().orEmpty().sortedBy { it.name }.mapIndexed { index, page ->
Page( Page(
index, index,
imageUrl = fakeUrlFrom(applicationDirs.localMangaRoot + "/" + chapter.url + "/" + page.name) imageUrl = applicationDirs.localMangaRoot + "/" + chapter.url + "/" + page.name
) )
} }
) )
@@ -412,66 +394,4 @@ class LocalSource : HttpSource() {
data class Rar(val file: File) : Format() data class Rar(val file: File) : Format()
data class Epub(val file: File) : Format() data class Epub(val file: File) : Format()
} }
// ///////////////////// Not used ///////////////////// //
override fun mangaDetailsParse(response: Response): SManga = throw Exception("Not used")
override fun chapterListParse(response: Response): List<SChapter> = throw Exception("Not used")
override fun pageListParse(response: Response): List<Page> = throw Exception("Not used")
override fun imageUrlParse(response: Response): String = throw Exception("Not used")
override fun popularMangaRequest(page: Int): Request = throw Exception("Not used")
override fun popularMangaParse(response: Response): MangasPage = throw Exception("Not used")
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
throw Exception("Not used")
override fun searchMangaParse(response: Response): MangasPage = throw Exception("Not used")
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
override fun latestUpdatesParse(response: Response): MangasPage = throw Exception("Not used")
}
private object FileSystemInterceptor : Interceptor {
fun fakeUrlFrom(path: String): String = "http://$path"
private fun restoreFilePath(url: String): String {
val path = URLDecoder.decode(url.replaceFirst("http://", ""), "UTF-8")
// Windows
if (System.getProperty("os.name").lowercase().startsWith("win")) {
// convert paths like "c/Users/..." to "c:/Users/..."
return StringBuilder(path).insert(1, ":").toString()
}
return "/$path"
}
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val url = request.url
val filePath = restoreFilePath(url.toString())
return try {
Response.Builder()
.body(File(filePath).source().buffer().asResponseBody())
.code(200)
.message("Some file")
.protocol(Protocol.HTTP_1_0)
.request(request)
.build()
} catch (e: FileNotFoundException) {
Response.Builder()
.body("".toResponseBody())
.code(404)
.message(e.message ?: "File not found ($filePath)")
.protocol(Protocol.HTTP_1_0)
.request(request)
.build()
}
}
} }

View File

@@ -9,6 +9,7 @@ package suwayomi.tachidesk.manga.impl
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.SortOrder
@@ -20,7 +21,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.Manga.getManga import suwayomi.tachidesk.manga.impl.Manga.getManga
import suwayomi.tachidesk.manga.impl.Page.getPageName 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.getChapterDir
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
@@ -53,7 +54,7 @@ object Chapter {
private suspend fun getSourceChapters(mangaId: Int): List<ChapterDataClass> { private suspend fun getSourceChapters(mangaId: Int): List<ChapterDataClass> {
val manga = getManga(mangaId) val manga = getManga(mangaId)
val source = getHttpSource(manga.sourceId.toLong()) val source = getCatalogueSourceOrStub(manga.sourceId.toLong())
val sManga = SManga.create().apply { val sManga = SManga.create().apply {
title = manga.title title = manga.title
@@ -64,7 +65,7 @@ object Chapter {
// Recognize number for new chapters. // Recognize number for new chapters.
chapterList.forEach { chapterList.forEach {
source.prepareNewChapter(it, sManga) (source as? HttpSource)?.prepareNewChapter(it, sManga)
ChapterRecognition.parseChapterNumber(it, sManga) ChapterRecognition.parseChapterNumber(it, sManga)
} }
@@ -169,7 +170,7 @@ object Chapter {
} }
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } 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( val pageList = source.fetchPageList(
SChapter.create().apply { 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.network.GET 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.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
@@ -19,11 +21,12 @@ import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
import suwayomi.tachidesk.manga.impl.Source.getSource 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.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.network.await 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.clearCachedImage
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse 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.impl.util.updateMangaDownloadDir
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.dataclass.toGenreList 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.MangaStatus
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
import java.io.File
import java.io.IOException
import java.io.InputStream import java.io.InputStream
object Manga { object Manga {
@@ -68,7 +73,7 @@ object Manga {
false false
) )
} else { // initialize manga } else { // initialize manga
val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val sManga = SManga.create().apply { val sManga = SManga.create().apply {
url = mangaEntry[MangaTable.url] url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title] title = mangaEntry[MangaTable.title]
@@ -95,11 +100,9 @@ object Manga {
if (sManga.thumbnail_url != null && sManga.thumbnail_url.orEmpty().isNotEmpty()) if (sManga.thumbnail_url != null && sManga.thumbnail_url.orEmpty().isNotEmpty())
it[MangaTable.thumbnail_url] = sManga.thumbnail_url it[MangaTable.thumbnail_url] = sManga.thumbnail_url
it[MangaTable.realUrl] = try { it[MangaTable.realUrl] = runCatching {
source.mangaDetailsRequest(sManga).url.toString() (source as? HttpSource)?.mangaDetailsRequest(sManga)?.url?.toString()
} catch (e: Exception) { }.getOrNull()
null
}
} }
} }
@@ -164,25 +167,39 @@ object Manga {
val saveDir = applicationDirs.mangaThumbnailsRoot val saveDir = applicationDirs.mangaThumbnailsRoot
val fileName = mangaId.toString() 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] return when (val source = getCatalogueSourceOrStub(sourceId)) {
val source = getHttpSource(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] source.client.newCall(
?: if (!mangaEntry[MangaTable.initialized]) { GET(thumbnailUrl, source.headers)
// initialize then try again ).await()
getManga(mangaId) }
transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }[MangaTable.thumbnail_url]!! is LocalSource -> {
} else { val imageFile = mangaEntry[MangaTable.thumbnail_url]?.let {
// source provides no thumbnail url for this manga val file = File(it)
throw NullPointerException() if (file.exists()) {
} file
} else {
source.client.newCall( null
GET(thumbnailUrl, source.headers) }
).await() } ?: 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.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.manga.impl.Manga.getMangaMetaMap 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.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
@@ -27,14 +27,15 @@ object MangaList {
} }
suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass { suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
val source = getHttpSource(sourceId) val source = getCatalogueSourceOrStub(sourceId)
val mangasPage = if (popular) { val mangasPage = if (popular) {
source.fetchPopularManga(pageNum).awaitSingle() source.fetchPopularManga(pageNum).awaitSingle()
} else { } else {
if (source.supportsLatest) if (source.supportsLatest) {
source.fetchLatestUpdates(pageNum).awaitSingle() source.fetchLatestUpdates(pageNum).awaitSingle()
else } else {
throw Exception("Source $source doesn't support latest") throw Exception("Source $source doesn't support latest")
}
} }
return mangasPage.processEntries(sourceId) 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.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import org.kodein.di.instance 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.getChapterDir
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse 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.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.PageTable 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> { 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 mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val chapterEntry = transaction { val chapterEntry = transaction {
ChapterTable.select { ChapterTable.select {
(ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId) (ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId)
@@ -61,19 +62,20 @@ object Page {
) )
// we treat Local source differently // we treat Local source differently
if (mangaEntry[MangaTable.sourceReference] == LocalSource.ID) { if (source.id == LocalSource.ID) {
// is of archive format // is of archive format
if (LocalSource.pageCache.containsKey(chapterEntry[ChapterTable.url])) { if (LocalSource.pageCache.containsKey(chapterEntry[ChapterTable.url])) {
val pageStream = LocalSource.pageCache[chapterEntry[ChapterTable.url]]!![index]() val pageStream = LocalSource.pageCache[chapterEntry[ChapterTable.url]]!![index]
return pageStream to "image/jpeg" return pageStream() to (ImageUtil.findImageType { pageStream() }?.mime ?: "image/jpeg")
} }
// is of directory format // is of directory format
return ImageResponse.getNoCacheImageResponse { val imageFile = File(tachiyomiPage.imageUrl!!)
source.fetchImage(tachiyomiPage).awaitSingle() return imageFile.inputStream() to (ImageUtil.findImageType { imageFile.inputStream() }?.mime ?: "image/jpeg")
}
} }
source as HttpSource
if (pageEntry[PageTable.imageUrl] == null) { if (pageEntry[PageTable.imageUrl] == null) {
val trueImageUrl = getTrueImageUrl(tachiyomiPage, source) val trueImageUrl = getTrueImageUrl(tachiyomiPage, source)
transaction { 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.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import suwayomi.tachidesk.manga.impl.MangaList.processEntries 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.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
object Search { object Search {
suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass { 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() val searchManga = source.fetchSearchManga(pageNum, searchTerm, getFilterListOf(sourceId)).awaitSingle()
return searchManga.processEntries(sourceId) return searchManga.processEntries(sourceId)
} }
@@ -25,7 +25,7 @@ object Search {
private fun getFilterListOf(sourceId: Long, reset: Boolean = false): FilterList { private fun getFilterListOf(sourceId: Long, reset: Boolean = false): FilterList {
if (reset || !filterListCache.containsKey(sourceId)) { if (reset || !filterListCache.containsKey(sourceId)) {
filterListCache[sourceId] = getHttpSource(sourceId).getFilterList() filterListCache[sourceId] = getCatalogueSourceOrStub(sourceId).getFilterList()
} }
return filterListCache[sourceId]!! return filterListCache[sourceId]!!
} }

View File

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

View File

@@ -22,7 +22,7 @@ private val applicationDirs by DI.global.instance<ApplicationDirs>()
fun getMangaDir(mangaId: Int): String { fun getMangaDir(mangaId: Int): String {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } 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 sourceDir = source.toString()
val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title]) 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 */ /** return value says if rename/move was successful */
fun updateMangaDownloadDir(mangaId: Int, newTitle: String): Boolean { fun updateMangaDownloadDir(mangaId: Int, newTitle: String): Boolean {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } 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 sourceDir = source.toString()
val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title]) 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 * 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/. */ * 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.Source
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.local.LocalSource 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 eu.kanade.tachiyomi.source.online.HttpSource
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import rx.Observable
import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources
import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.manga.model.table.SourceTable
@@ -23,22 +30,56 @@ import suwayomi.tachidesk.server.ApplicationDirs
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
object GetHttpSource { 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>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
fun getHttpSource(sourceId: Long): HttpSource { class StubSource(override val id: Long) : CatalogueSource {
val cachedResult: HttpSource? = sourceCache[sourceId] 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) { if (cachedResult != null) {
return cachedResult return cachedResult
} }
val sourceRecord = transaction { val sourceRecord = transaction {
SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
} } ?: return null
if (sourceId == LocalSource.ID) {
return LocalSource()
}
val extensionId = sourceRecord[SourceTable.extension] val extensionId = sourceRecord[SourceTable.extension]
val extensionRecord = transaction { val extensionRecord = transaction {
@@ -60,6 +101,10 @@ object GetHttpSource {
return sourceCache[sourceId]!! return sourceCache[sourceId]!!
} }
fun getCatalogueSourceOrStub(sourceId: Long): CatalogueSource {
return getCatalogueSource(sourceId) ?: StubSource(sourceId)
}
fun invalidateSourceCache(sourceId: Long) { fun invalidateSourceCache(sourceId: Long) {
sourceCache.remove(sourceId) sourceCache.remove(sourceId)
} }