add Local Source

This commit is contained in:
Aria Moradi
2021-09-18 00:47:50 +04:30
parent 52a064ae45
commit 1ca11fdd34
14 changed files with 455 additions and 234 deletions

View File

@@ -62,6 +62,8 @@ dependencies {
implementation("com.google.code.gson:gson:2.8.7") implementation("com.google.code.gson:gson:2.8.7")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0") implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
// Sort
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
// asm for ByteCodeEditor(fixing SimpleDateFormat) (must match Dex2Jar version) // asm for ByteCodeEditor(fixing SimpleDateFormat) (must match Dex2Jar version)
implementation("org.ow2.asm:asm:9.2") implementation("org.ow2.asm:asm:9.2")

View File

@@ -1,153 +1,185 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
import android.content.Context
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 rx.Observable
// import com.github.junrar.Archive // import com.github.junrar.Archive
// import com.google.gson.JsonParser
// import eu.kanade.tachiyomi.R // import eu.kanade.tachiyomi.R
// import eu.kanade.tachiyomi.source.model.Filter
// import eu.kanade.tachiyomi.source.model.FilterList
// import eu.kanade.tachiyomi.source.model.MangasPage // 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.util.chapter.ChapterRecognition // import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
// import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
// import eu.kanade.tachiyomi.util.storage.DiskUtil // import eu.kanade.tachiyomi.util.storage.DiskUtil
// import eu.kanade.tachiyomi.util.storage.EpubFile // import eu.kanade.tachiyomi.util.storage.EpubFile
// import eu.kanade.tachiyomi.util.system.ImageUtil // import eu.kanade.tachiyomi.util.system.ImageUtil
// import rx.Observable // import rx.Observable
// import timber.log.Timber // import timber.log.Timber
// import java.io.File
// import java.io.FileInputStream // import java.io.FileInputStream
// import java.io.InputStream // import java.io.InputStream
// import java.util.Locale // import java.util.Locale
// import java.util.concurrent.TimeUnit
// import java.util.zip.ZipFile // import java.util.zip.ZipFile
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.source.model.Filter
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.util.lang.compareToCaseInsensitiveNaturalOrder
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId
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.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable
import suwayomi.tachidesk.server.ApplicationDirs
import java.io.File
import java.io.FileNotFoundException
import java.net.URL
import java.util.Locale
import java.util.concurrent.TimeUnit
class LocalSource(private val context: Context) : CatalogueSource { class LocalSource(override val baseUrl: String = "") : HttpSource() {
companion object { companion object {
const val ID = 0L const val ID = 0L
// const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/" const val LANG = "localsourcelang"
// const val NAME = "Local source"
// private const val COVER_NAME = "cover.jpg"
// private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub") const val EXTENSION_NAME = "Local Source fake extension"
//
// private val POPULAR_FILTERS = FilterList(OrderBy()) const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
// private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
// private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS) private val SUPPORTED_ARCHIVE_TYPES = setOf<String>(
// // "zip",
// fun updateCover(context: Context, manga: SManga, input: InputStream): File? { // "rar",
// "cbr",
// "cbz",
// "epub"
)
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
// fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
// val dir = getBaseDirectories(context).firstOrNull() // val dir = getBaseDirectories(context).firstOrNull()
// if (dir == null) { // if (dir == null) {
// input.close() // input.close()
// return null // return null
// } // }
// val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME) // val cover = getCoverFile(File("${dir.absolutePath}/${manga.url}"))
// //
// // It might not exist if using the external SD card // if (cover != null && cover.exists()) {
// cover.parentFile?.mkdirs() // // It might not exist if using the external SD card
// input.use { // cover.parentFile?.mkdirs()
// cover.outputStream().use { // input.use {
// input.copyTo(it) // cover.outputStream().use {
// input.copyTo(it)
// }
// } // }
// } // }
// return cover // return cover
// } // }
// //
// private fun getBaseDirectories(context: Context): List<File> { // /**
// val c = context.getString(R.string.app_name) + File.separator + "local" // * Returns valid cover file inside [parent] directory.
// return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) } // */
// private fun getCoverFile(parent: File): File? {
// return parent.listFiles()?.find { it.nameWithoutExtension == "cover" }?.takeIf {
// it.isFile && ImageUtil.isImage(it.name) { it.inputStream() }
// }
// } // }
//
private val applicationDirs by DI.global.instance<ApplicationDirs>()
fun addDbRecords() {
transaction {
val sourceRecord = SourceTable.select { SourceTable.id eq ID }.firstOrNull()
if (sourceRecord == null) {
// must do this to avoid database integrity errors
val extensionId = ExtensionTable.insertAndGetId {
it[apkName] = "localSource"
it[name] = EXTENSION_NAME
it[pkgName] = "eu.kanade.tachiyomi.source.LocalSource"
it[versionName] = "1.2"
it[versionCode] = 0
it[lang] = LANG
it[isNsfw] = false
it[isInstalled] = true
}
SourceTable.insert {
it[id] = ID
it[name] = NAME
it[lang] = LANG
it[extension] = extensionId
it[isNsfw] = false
}
}
}
}
} }
override val id = ID override val id = ID
override val name = "Local source" override val name = NAME
override val lang = "" override val lang = LANG
override val supportsLatest = true override val supportsLatest = true
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override val client: OkHttpClient = super.client.newBuilder()
TODO("Not yet implemented") .addInterceptor(FileSystemInterceptor)
} .build()
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun toString() = name
TODO("Not yet implemented")
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
TODO("Not yet implemented")
}
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
TODO("Not yet implemented")
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
TODO("Not yet implemented") val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> { var mangaDirs = File(applicationDirs.localMangaRoot).listFiles().orEmpty().toList()
TODO("Not yet implemented") .filter { it.isDirectory }
} .filterNot { it.name.startsWith('.') }
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
.distinctBy { it.name }
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
when (state?.index) {
0 -> {
mangaDirs = if (state.ascending) {
mangaDirs.sortedBy { it.name.lowercase(Locale.ENGLISH) }
} else {
mangaDirs.sortedByDescending { it.name.lowercase(Locale.ENGLISH) }
}
}
1 -> {
mangaDirs = if (state.ascending) {
mangaDirs.sortedBy(File::lastModified)
} else {
mangaDirs.sortedByDescending(File::lastModified)
}
}
}
val mangas = mangaDirs.map { mangaDir ->
SManga.create().apply {
title = mangaDir.name
url = mangaDir.name
// Try to find the cover
val cover = File("${applicationDirs.localMangaRoot}/$title/cover.jpg")
if (cover.exists()) {
thumbnail_url = "http://${cover.absolutePath}"
}
override fun getFilterList(): FilterList {
TODO("Not yet implemented")
}
//
// override fun toString() = context.getString(R.string.local_source)
//
// override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
//
// override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
// val baseDirs = getBaseDirectories(context)
//
// val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
// var mangaDirs = baseDirs
// .asSequence()
// .mapNotNull { it.listFiles()?.toList() }
// .flatten()
// .filter { it.isDirectory }
// .filterNot { it.name.startsWith('.') }
// .filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
// .distinctBy { it.name }
//
// val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
// when (state?.index) {
// 0 -> {
// mangaDirs = if (state.ascending) {
// mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
// } else {
// mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
// }
// }
// 1 -> {
// mangaDirs = if (state.ascending) {
// mangaDirs.sortedBy(File::lastModified)
// } else {
// mangaDirs.sortedByDescending(File::lastModified)
// }
// }
// }
//
// val mangas = mangaDirs.map { mangaDir ->
// SManga.create().apply {
// title = mangaDir.name
// url = mangaDir.name
//
// // Try to find the cover
// for (dir in baseDirs) {
// val cover = File("${dir.absolutePath}/$url", COVER_NAME)
// if (cover.exists()) {
// thumbnail_url = cover.absolutePath
// break
// }
// }
//
// val chapters = fetchChapterList(this).toBlocking().first() // val chapters = fetchChapterList(this).toBlocking().first()
// if (chapters.isNotEmpty()) { // if (chapters.isNotEmpty()) {
// val chapter = chapters.last() // val chapter = chapters.last()
@@ -168,117 +200,123 @@ class LocalSource(private val context: Context) : CatalogueSource {
// } // }
// } // }
// } // }
// } }
// } }
//
// return Observable.just(MangasPage(mangas.toList(), false)) return Observable.just(MangasPage(mangas.toList(), false))
// } }
//
// override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS) override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
//
// override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
// getBaseDirectories(context) File(applicationDirs.localMangaRoot, manga.url).listFiles().orEmpty().toList()
// .asSequence() .firstOrNull { it.extension == "json" }
// .mapNotNull { File(it, manga.url).listFiles()?.toList() } ?.apply {
// .flatten() val reader = this.inputStream().bufferedReader()
// .firstOrNull { it.extension == "json" } val json = JsonParser.parseReader(reader).asJsonObject
// ?.apply {
// val reader = this.inputStream().bufferedReader() manga.title = json["title"]?.asString ?: manga.title
// val json = JsonParser.parseReader(reader).asJsonObject manga.author = json["author"]?.asString ?: manga.author
// manga.artist = json["artist"]?.asString ?: manga.artist
// manga.title = json["title"]?.asString ?: manga.title manga.description = json["description"]?.asString ?: manga.description
// manga.author = json["author"]?.asString ?: manga.author manga.genre = json["genre"]?.asJsonArray?.joinToString(", ") { it.asString }
// manga.artist = json["artist"]?.asString ?: manga.artist ?: manga.genre
// manga.description = json["description"]?.asString ?: manga.description manga.status = json["status"]?.asInt ?: manga.status
// manga.genre = json["genre"]?.asJsonArray?.joinToString(", ") { it.asString } }
// ?: manga.genre
// manga.status = json["status"]?.asInt ?: manga.status return Observable.just(manga)
// } }
//
// return Observable.just(manga) override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
// } val chapters = File(applicationDirs.localMangaRoot, manga.url).listFiles().orEmpty().toList()
// .filter { it.isDirectory || isSupportedFile(it.extension) }
// override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { .map { chapterFile ->
// val chapters = getBaseDirectories(context) SChapter.create().apply {
// .asSequence() url = "${manga.url}/${chapterFile.name}"
// .mapNotNull { File(it, manga.url).listFiles()?.toList() } name = if (chapterFile.isDirectory) {
// .flatten() chapterFile.name
// .filter { it.isDirectory || isSupportedFile(it.extension) } } else {
// .map { chapterFile -> chapterFile.nameWithoutExtension
// SChapter.create().apply { }
// url = "${manga.url}/${chapterFile.name}" date_upload = chapterFile.lastModified()
// name = if (chapterFile.isDirectory) {
// chapterFile.name
// } else {
// chapterFile.nameWithoutExtension
// }
// date_upload = chapterFile.lastModified()
//
// val format = getFormat(this) // val format = getFormat(this)
// if (format is Format.Epub) { // if (format is Format.Epub) {
// EpubFile(format.file).use { epub -> // EpubFile(format.file).use { epub ->
// epub.fillChapterMetadata(this) // epub.fillChapterMetadata(this)
// } // }
// } // }
//
// val chapNameCut = stripMangaTitle(name, manga.title) val chapNameCut = stripMangaTitle(name, manga.title)
// if (chapNameCut.isNotEmpty()) name = chapNameCut if (chapNameCut.isNotEmpty()) name = chapNameCut
// ChapterRecognition.parseChapterNumber(this, manga) // ChapterRecognition.parseChapterNumber(this, manga)
// } }
// } }
// .sortedWith( .sortedWith { c1, c2 ->
// Comparator { c1, c2 -> val c = c2.chapter_number.compareTo(c1.chapter_number)
// val c = c2.chapter_number.compareTo(c1.chapter_number) if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
// if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c }
// } .toList()
// )
// .toList() return Observable.just(chapters)
// }
// return Observable.just(chapters)
// } /**
// * Strips the manga title from a chapter name, matching only based on alphanumeric and whitespace
// /** * characters.
// * Strips the manga title from a chapter name, matching only based on alphanumeric and whitespace */
// * characters. private fun stripMangaTitle(chapterName: String, mangaTitle: String): String {
// */ var chapterNameIndex = 0
// private fun stripMangaTitle(chapterName: String, mangaTitle: String): String { var mangaTitleIndex = 0
// var chapterNameIndex = 0 while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
// var mangaTitleIndex = 0 val chapterChar = chapterName[chapterNameIndex]
// while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) { val mangaChar = mangaTitle[mangaTitleIndex]
// val chapterChar = chapterName[chapterNameIndex] if (!chapterChar.equals(mangaChar, true)) {
// val mangaChar = mangaTitle[mangaTitleIndex] val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
// if (!chapterChar.equals(mangaChar, true)) { val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
// val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
// val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace() if (!invalidChapterChar && !invalidMangaChar) {
// return chapterName
// if (!invalidChapterChar && !invalidMangaChar) { }
// return chapterName
// } if (invalidChapterChar) {
// chapterNameIndex++
// if (invalidChapterChar) { }
// chapterNameIndex++
// } if (invalidMangaChar) {
// mangaTitleIndex++
// if (invalidMangaChar) { }
// mangaTitleIndex++ } else {
// } chapterNameIndex++
// } else { mangaTitleIndex++
// chapterNameIndex++ }
// mangaTitleIndex++ }
// }
// } return chapterName.substring(chapterNameIndex).trimStart(' ', '-', '_', ',', ':')
// }
// return chapterName.substring(chapterNameIndex).trimStart(' ', '-', '_', ',', ':')
// } private fun isSupportedFile(extension: String): Boolean {
// return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
// override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { }
// return Observable.error(Exception("Unused"))
// } override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
// val chapterFile = File(applicationDirs.localMangaRoot + File.separator + chapter.url)
// private fun isSupportedFile(extension: String): Boolean {
// return extension.toLowerCase() in SUPPORTED_ARCHIVE_TYPES return Observable.just(
// } if (chapterFile.isDirectory) {
// chapterFile.listFiles().sortedBy { it.name }.mapIndexed { index, page ->
Page(
index,
imageUrl = "http://" + applicationDirs.localMangaRoot + File.separator + chapter.url + File.separator + page.name
)
}
} else {
throw Exception("Archive chapters are not supported.")
}
)
}
//
// fun getFormat(chapter: SChapter): Format { // fun getFormat(chapter: SChapter): Format {
// val baseDirs = getBaseDirectories(context) // val baseDirs = getBaseDirectories(context)
// //
@@ -288,7 +326,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
// //
// return getFormat(chapFile) // return getFormat(chapFile)
// } // }
// throw Exception("Chapter not found") // throw Exception(context.getString(R.string.chapter_not_found))
// } // }
// //
// private fun getFormat(file: File): Format { // private fun getFormat(file: File): Format {
@@ -302,7 +340,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
// } else if (extension.equals("epub", true)) { // } else if (extension.equals("epub", true)) {
// Format.Epub(file) // Format.Epub(file)
// } else { // } else {
// throw Exception("Invalid chapter format") // throw Exception(context.getString(R.string.local_invalid_format))
// } // }
// } // }
// //
@@ -345,14 +383,73 @@ class LocalSource(private val context: Context) : CatalogueSource {
// } // }
// } // }
// //
// private class OrderBy : Filter.Sort("Order by", arrayOf("Title", "Date"), Selection(0, true)) override fun getFilterList() = POPULAR_FILTERS
//
// override fun getFilterList() = FilterList(OrderBy()) private val POPULAR_FILTERS = FilterList(OrderBy())
// private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
// sealed class Format {
// data class Directory(val file: File) : Format() private class OrderBy : Filter.Sort(
// data class Zip(val file: File) : Format() "Order by",
// data class Rar(val file: File) : Format() arrayOf("Title", "Date"),
// data class Epub(val file: File) : Format() Selection(0, true)
// } )
sealed class Format {
data class Directory(val file: File) : Format()
data class Zip(val file: File) : Format()
data class Rar(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 {
private fun restoreFileUrl(markedFakeHttpUrl: String): String {
return markedFakeHttpUrl.replaceFirst("http:", "file:/")
}
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val url = request.url
val fileUrl = restoreFileUrl(url.toString())
return try {
Response.Builder()
.body(URL(fileUrl).readBytes().toResponseBody())
.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 ($fileUrl)")
.protocol(Protocol.HTTP_1_0)
.request(request)
.build()
}
}
} }

View File

@@ -0,0 +1,58 @@
package eu.kanade.tachiyomi.util.lang
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
import kotlin.math.floor
/**
* Replaces the given string to have at most [count] characters using [replacement] at its end.
* If [replacement] is longer than [count] an exception will be thrown when `length > count`.
*/
fun String.chop(count: Int, replacement: String = ""): String {
return if (length > count) {
take(count - replacement.length) + replacement
} else {
this
}
}
/**
* Replaces the given string to have at most [count] characters using [replacement] near the center.
* If [replacement] is longer than [count] an exception will be thrown when `length > count`.
*/
fun String.truncateCenter(count: Int, replacement: String = "..."): String {
if (length <= count) {
return this
}
val pieceLength: Int = floor((count - replacement.length).div(2.0)).toInt()
return "${take(pieceLength)}$replacement${takeLast(pieceLength)}"
}
/**
* Case-insensitive natural comparator for strings.
*/
fun String.compareToCaseInsensitiveNaturalOrder(other: String): Int {
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
return comparator.compare(this, other)
}
/**
* Returns the size of the string as the number of bytes.
*/
fun String.byteSize(): Int {
return toByteArray(Charsets.UTF_8).size
}
/**
* Returns a string containing the first [n] bytes from this string, or the entire string if this
* string is shorter.
*/
fun String.takeBytes(n: Int): String {
val bytes = toByteArray(Charsets.UTF_8)
return if (bytes.size <= n) {
this
} else {
bytes.decodeToString(endIndex = n).replace("\uFFFD", "")
}
}

View File

@@ -75,7 +75,7 @@ object Manga {
transaction { transaction {
MangaTable.update({ MangaTable.id eq mangaId }) { MangaTable.update({ MangaTable.id eq mangaId }) {
it[MangaTable.title] = fetchedManga.title
it[MangaTable.initialized] = true it[MangaTable.initialized] = true
it[MangaTable.artist] = fetchedManga.artist it[MangaTable.artist] = fetchedManga.artist
@@ -86,7 +86,11 @@ object Manga {
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url.orEmpty().isNotEmpty()) if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url.orEmpty().isNotEmpty())
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
it[MangaTable.realUrl] = try { source.mangaDetailsRequest(sManga).url.toString() } catch (e: Exception) { null } it[MangaTable.realUrl] = try {
source.mangaDetailsRequest(sManga).url.toString()
} catch (e: Exception) {
null
}
} }
} }
@@ -151,14 +155,20 @@ object Manga {
val fileName = mangaId.toString() val fileName = mangaId.toString()
return getCachedImageResponse(saveDir, fileName) { return getCachedImageResponse(saveDir, fileName) {
getManga(mangaId) // make sure is initialized
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) val source = getHttpSource(sourceId)
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]!! val thumbnailUrl: String = 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( source.client.newCall(
GET(thumbnailUrl, source.headers) GET(thumbnailUrl, source.headers)

View File

@@ -11,6 +11,7 @@ import android.app.Application
import android.content.Context 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.LocalSource
import eu.kanade.tachiyomi.source.getPreferenceKey import eu.kanade.tachiyomi.source.getPreferenceKey
import mu.KotlinLogging import mu.KotlinLogging
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
@@ -45,7 +46,8 @@ object Source {
getExtensionIconUrl(sourceExtension[ExtensionTable.apkName]), getExtensionIconUrl(sourceExtension[ExtensionTable.apkName]),
httpSource.supportsLatest, httpSource.supportsLatest,
httpSource is ConfigurableSource, httpSource is ConfigurableSource,
it[SourceTable.isNsfw] it[SourceTable.isNsfw],
httpSource.toString(),
) )
} }
} }
@@ -53,6 +55,11 @@ 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 httpSource = source?.let { getHttpSource(sourceId) }
val extension = source?.let { val extension = source?.let {
@@ -70,7 +77,8 @@ object Source {
}, },
httpSource?.supportsLatest, httpSource?.supportsLatest,
httpSource?.let { it is ConfigurableSource }, httpSource?.let { it is ConfigurableSource },
source?.get(SourceTable.isNsfw) source?.get(SourceTable.isNsfw),
httpSource?.toString()
) )
} }
} }

View File

@@ -268,8 +268,8 @@ object Extension {
} }
suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> { suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
val iconUrl = val iconUrl = if (apkName == "localSource") ""
transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.first() }[ExtensionTable.iconUrl] else transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.first() }[ExtensionTable.iconUrl]
val saveDir = "${applicationDirs.extensionsRoot}/icon" val saveDir = "${applicationDirs.extensionsRoot}/icon"

View File

@@ -7,6 +7,7 @@ package suwayomi.tachidesk.manga.impl.extension
* 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.LocalSource
import mu.KotlinLogging import mu.KotlinLogging
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
@@ -46,7 +47,7 @@ object ExtensionsList {
} }
fun extensionTableAsDataClass() = transaction { fun extensionTableAsDataClass() = transaction {
ExtensionTable.selectAll().map { ExtensionTable.selectAll().filter { it[ExtensionTable.name] != LocalSource.EXTENSION_NAME }.map {
ExtensionDataClass( ExtensionDataClass(
it[ExtensionTable.apkName], it[ExtensionTable.apkName],
getExtensionIconUrl(it[ExtensionTable.apkName]), getExtensionIconUrl(it[ExtensionTable.apkName]),

View File

@@ -7,6 +7,7 @@ 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.LocalSource
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.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
@@ -35,6 +36,10 @@ object GetHttpSource {
SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!!
} }
if (sourceId == LocalSource.ID) {
return LocalSource()
}
val extensionId = sourceRecord[SourceTable.extension] val extensionId = sourceRecord[SourceTable.extension]
val extensionRecord = transaction { val extensionRecord = transaction {
ExtensionTable.select { ExtensionTable.id eq extensionId }.first() ExtensionTable.select { ExtensionTable.id eq extensionId }.first()

View File

@@ -23,4 +23,6 @@ data class SourceDataClass(
/** The Source class has a @Nsfw annotation */ /** The Source class has a @Nsfw annotation */
val isNsfw: Boolean?, val isNsfw: Boolean?,
val displayName: String?,
) )

View File

@@ -20,7 +20,7 @@ object ExtensionTable : IntIdTable() {
val pkgName = varchar("pkg_name", 128) val pkgName = varchar("pkg_name", 128)
val versionName = varchar("version_name", 16) val versionName = varchar("version_name", 16)
val versionCode = integer("version_code") val versionCode = integer("version_code")
val lang = varchar("lang", 10) val lang = varchar("lang", 32)
val isNsfw = bool("is_nsfw") val isNsfw = bool("is_nsfw")
val isInstalled = bool("is_installed").default(false) val isInstalled = bool("is_installed").default(false)

View File

@@ -12,7 +12,7 @@ import org.jetbrains.exposed.dao.id.IdTable
object SourceTable : IdTable<Long>() { object SourceTable : IdTable<Long>() {
override val id = long("id").entityId() override val id = long("id").entityId()
val name = varchar("name", 128) val name = varchar("name", 128)
val lang = varchar("lang", 10) val lang = varchar("lang", 32)
val extension = reference("extension", ExtensionTable) val extension = reference("extension", ExtensionTable)
val isNsfw = bool("is_nsfw").default(false) val isNsfw = bool("is_nsfw").default(false)
} }

View File

@@ -8,6 +8,7 @@ package suwayomi.tachidesk.server
* 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.App import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.source.LocalSource
import mu.KotlinLogging import mu.KotlinLogging
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.bind import org.kodein.di.bind
@@ -33,6 +34,7 @@ class ApplicationDirs(
val mangaThumbnailsRoot = "$dataRoot/manga-thumbnails" val mangaThumbnailsRoot = "$dataRoot/manga-thumbnails"
val animeThumbnailsRoot = "$dataRoot/anime-thumbnails" val animeThumbnailsRoot = "$dataRoot/anime-thumbnails"
val mangaRoot = "$dataRoot/manga" val mangaRoot = "$dataRoot/manga"
val localMangaRoot = "$dataRoot/manga-local"
val webUIRoot = "$dataRoot/webUI" val webUIRoot = "$dataRoot/webUI"
} }
@@ -63,6 +65,8 @@ fun applicationSetup() {
applicationDirs.extensionsRoot + "/icon", applicationDirs.extensionsRoot + "/icon",
applicationDirs.mangaThumbnailsRoot, applicationDirs.mangaThumbnailsRoot,
applicationDirs.animeThumbnailsRoot, applicationDirs.animeThumbnailsRoot,
applicationDirs.mangaRoot,
applicationDirs.localMangaRoot,
).forEach { ).forEach {
File(it).mkdirs() File(it).mkdirs()
} }
@@ -96,11 +100,27 @@ fun applicationSetup() {
logger.error("Exception while creating initial server.conf:\n", e) logger.error("Exception while creating initial server.conf:\n", e)
} }
// copy local source icon
try {
val localSourceIconFile = File("${applicationDirs.extensionsRoot}/icon/localSource.png")
if (!localSourceIconFile.exists()) {
JavalinSetup::class.java.getResourceAsStream("/icon/localSource.png").use { input ->
localSourceIconFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
} catch (e: Exception) {
logger.error("Exception while creating initial server.conf:\n", e)
}
// fixes #119 , ref: https://github.com/Suwayomi/Tachidesk-Server/issues/119#issuecomment-894681292 , source Id calculation depends on String.lowercase() // fixes #119 , ref: https://github.com/Suwayomi/Tachidesk-Server/issues/119#issuecomment-894681292 , source Id calculation depends on String.lowercase()
Locale.setDefault(Locale.ENGLISH) Locale.setDefault(Locale.ENGLISH)
databaseUp() databaseUp()
LocalSource.addDbRecords()
// create system tray // create system tray
if (serverConfig.systemTrayEnabled) { if (serverConfig.systemTrayEnabled) {
try { try {

View File

@@ -0,0 +1,18 @@
package suwayomi.tachidesk.server.database.migration
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* 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 de.neonew.exposed.migrations.helpers.SQLMigration
@Suppress("ClassName", "unused")
class M0015_SourceAndExtensionLangAddLengthLimit : SQLMigration() {
override val sql = """
ALTER TABLE SOURCE ALTER COLUMN LANG VARCHAR(32);
ALTER TABLE EXTENSION ALTER COLUMN LANG VARCHAR(32);
""".trimIndent()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB