mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-03 02:44:34 -05:00
add Local Source
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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]),
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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?,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
BIN
server/src/main/resources/icon/localSource.png
Normal file
BIN
server/src/main/resources/icon/localSource.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
Reference in New Issue
Block a user