mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-07 21:04:34 -05:00
Switch to a new Ktlint Formatter (#705)
* Switch to new Ktlint plugin * Add ktlintCheck to PR builds * Run formatter * Put ktlint version in libs toml * Fix lint * Use Zip4Java from libs.toml
This commit is contained in:
@@ -6,7 +6,6 @@ import rx.Observable
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
||||
|
||||
interface CatalogueSource : Source {
|
||||
|
||||
/**
|
||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
||||
*/
|
||||
@@ -37,7 +36,11 @@ interface CatalogueSource : Source {
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage {
|
||||
suspend fun getSearchManga(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): MangasPage {
|
||||
return fetchSearchManga(page, query, filters).awaitSingle()
|
||||
}
|
||||
|
||||
@@ -59,22 +62,23 @@ interface CatalogueSource : Source {
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getPopularManga")
|
||||
ReplaceWith("getPopularManga"),
|
||||
)
|
||||
fun fetchPopularManga(page: Int): Observable<MangasPage> =
|
||||
throw IllegalStateException("Not used")
|
||||
fun fetchPopularManga(page: Int): Observable<MangasPage> = throw IllegalStateException("Not used")
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getSearchManga")
|
||||
ReplaceWith("getSearchManga"),
|
||||
)
|
||||
fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
throw IllegalStateException("Not used")
|
||||
fun fetchSearchManga(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): Observable<MangasPage> = throw IllegalStateException("Not used")
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getLatestUpdates")
|
||||
ReplaceWith("getLatestUpdates"),
|
||||
)
|
||||
fun fetchLatestUpdates(page: Int): Observable<MangasPage> =
|
||||
throw IllegalStateException("Not used")
|
||||
fun fetchLatestUpdates(page: Int): Observable<MangasPage> = throw IllegalStateException("Not used")
|
||||
}
|
||||
|
||||
@@ -8,14 +8,12 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
interface ConfigurableSource : Source {
|
||||
|
||||
/**
|
||||
* Gets instance of [SharedPreferences] scoped to the specific source.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
*/
|
||||
fun getSourcePreferences(): SharedPreferences =
|
||||
Injekt.get<Application>().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE)
|
||||
fun getSourcePreferences(): SharedPreferences = Injekt.get<Application>().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE)
|
||||
|
||||
fun setupPreferenceScreen(screen: PreferenceScreen)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
||||
* A basic interface for creating a source. It could be an online source, a local source, etc...
|
||||
*/
|
||||
interface Source {
|
||||
|
||||
/**
|
||||
* Id for the source. Must be unique.
|
||||
*/
|
||||
@@ -60,19 +59,19 @@ interface Source {
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getMangaDetails")
|
||||
ReplaceWith("getMangaDetails"),
|
||||
)
|
||||
fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw IllegalStateException("Not used")
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getChapterList")
|
||||
ReplaceWith("getChapterList"),
|
||||
)
|
||||
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw IllegalStateException("Not used")
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getPageList")
|
||||
ReplaceWith("getPageList"),
|
||||
)
|
||||
fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.empty()
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package eu.kanade.tachiyomi.source.local
|
||||
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
@@ -55,9 +57,8 @@ import com.github.junrar.Archive as JunrarArchive
|
||||
|
||||
class LocalSource(
|
||||
private val fileSystem: LocalSourceFileSystem,
|
||||
private val coverManager: LocalCoverManager
|
||||
private val coverManager: LocalCoverManager,
|
||||
) : CatalogueSource, UnmeteredSource {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
private val xml: XML by injectLazy()
|
||||
|
||||
@@ -79,56 +80,64 @@ class LocalSource(
|
||||
|
||||
override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", LATEST_FILTERS)
|
||||
|
||||
override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage {
|
||||
override suspend fun getSearchManga(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): MangasPage {
|
||||
val baseDirsFiles = fileSystem.getFilesInBaseDirectories()
|
||||
val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L }
|
||||
var mangaDirs = baseDirsFiles
|
||||
// Filter out files that are hidden and is not a folder
|
||||
.filter { it.isDirectory && !it.name.startsWith('.') }
|
||||
.distinctBy { it.name }
|
||||
.filter { // Filter by query or last modified
|
||||
if (lastModifiedLimit == 0L) {
|
||||
it.name.contains(query, ignoreCase = true)
|
||||
} else {
|
||||
it.lastModified() >= lastModifiedLimit
|
||||
var mangaDirs =
|
||||
baseDirsFiles
|
||||
// Filter out files that are hidden and is not a folder
|
||||
.filter { it.isDirectory && !it.name.startsWith('.') }
|
||||
.distinctBy { it.name }
|
||||
.filter { // Filter by query or last modified
|
||||
if (lastModifiedLimit == 0L) {
|
||||
it.name.contains(query, ignoreCase = true)
|
||||
} else {
|
||||
it.lastModified() >= lastModifiedLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is OrderBy.Popular -> {
|
||||
mangaDirs = if (filter.state!!.ascending) {
|
||||
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
} else {
|
||||
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
}
|
||||
mangaDirs =
|
||||
if (filter.state!!.ascending) {
|
||||
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
} else {
|
||||
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
}
|
||||
}
|
||||
is OrderBy.Latest -> {
|
||||
mangaDirs = if (filter.state!!.ascending) {
|
||||
mangaDirs.sortedBy(File::lastModified)
|
||||
} else {
|
||||
mangaDirs.sortedByDescending(File::lastModified)
|
||||
}
|
||||
mangaDirs =
|
||||
if (filter.state!!.ascending) {
|
||||
mangaDirs.sortedBy(File::lastModified)
|
||||
} else {
|
||||
mangaDirs.sortedByDescending(File::lastModified)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
/* Do nothing */
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transform mangaDirs to list of SManga
|
||||
val mangas = mangaDirs.map { mangaDir ->
|
||||
SManga.create().apply {
|
||||
title = mangaDir.name
|
||||
url = mangaDir.name
|
||||
val mangas =
|
||||
mangaDirs.map { mangaDir ->
|
||||
SManga.create().apply {
|
||||
title = mangaDir.name
|
||||
url = mangaDir.name
|
||||
|
||||
// Try to find the cover
|
||||
coverManager.find(mangaDir.name)
|
||||
?.takeIf(File::exists)
|
||||
?.let { thumbnail_url = it.absolutePath }
|
||||
// Try to find the cover
|
||||
coverManager.find(mangaDir.name)
|
||||
?.takeIf(File::exists)
|
||||
?.let { thumbnail_url = it.absolutePath }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch chapters of all the manga
|
||||
mangas.forEach { manga ->
|
||||
@@ -156,67 +165,75 @@ class LocalSource(
|
||||
}
|
||||
|
||||
// Manga details related
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga = withContext(Dispatchers.IO) {
|
||||
coverManager.find(manga.url)?.let {
|
||||
manga.thumbnail_url = it.absolutePath
|
||||
}
|
||||
|
||||
// Augment manga details based on metadata files
|
||||
try {
|
||||
val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).toList()
|
||||
|
||||
val comicInfoFile = mangaDirFiles
|
||||
.firstOrNull { it.name == COMIC_INFO_FILE }
|
||||
val noXmlFile = mangaDirFiles
|
||||
.firstOrNull { it.name == ".noxml" }
|
||||
val legacyJsonDetailsFile = mangaDirFiles
|
||||
.firstOrNull { it.extension == "json" }
|
||||
|
||||
when {
|
||||
// Top level ComicInfo.xml
|
||||
comicInfoFile != null -> {
|
||||
noXmlFile?.delete()
|
||||
setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga)
|
||||
}
|
||||
|
||||
// TODO: automatically convert these to ComicInfo.xml
|
||||
legacyJsonDetailsFile != null -> {
|
||||
json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.inputStream()).run {
|
||||
title?.let { manga.title = it }
|
||||
author?.let { manga.author = it }
|
||||
artist?.let { manga.artist = it }
|
||||
description?.let { manga.description = it }
|
||||
genre?.let { manga.genre = it.joinToString() }
|
||||
status?.let { manga.status = it }
|
||||
}
|
||||
}
|
||||
|
||||
// Copy ComicInfo.xml from chapter archive to top level if found
|
||||
noXmlFile == null -> {
|
||||
val chapterArchives = mangaDirFiles
|
||||
.filter(Archive::isSupported)
|
||||
.toList()
|
||||
|
||||
val mangaDir = fileSystem.getMangaDirectory(manga.url)
|
||||
val folderPath = mangaDir?.absolutePath
|
||||
|
||||
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
|
||||
if (copiedFile != null) {
|
||||
setMangaDetailsFromComicInfoFile(copiedFile.inputStream(), manga)
|
||||
} else {
|
||||
// Avoid re-scanning
|
||||
File("$folderPath/.noxml").createNewFile()
|
||||
}
|
||||
}
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga =
|
||||
withContext(Dispatchers.IO) {
|
||||
coverManager.find(manga.url)?.let {
|
||||
manga.thumbnail_url = it.absolutePath
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logger.error(e) { "Error setting manga details from local metadata for ${manga.title}" }
|
||||
|
||||
// Augment manga details based on metadata files
|
||||
try {
|
||||
val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).toList()
|
||||
|
||||
val comicInfoFile =
|
||||
mangaDirFiles
|
||||
.firstOrNull { it.name == COMIC_INFO_FILE }
|
||||
val noXmlFile =
|
||||
mangaDirFiles
|
||||
.firstOrNull { it.name == ".noxml" }
|
||||
val legacyJsonDetailsFile =
|
||||
mangaDirFiles
|
||||
.firstOrNull { it.extension == "json" }
|
||||
|
||||
when {
|
||||
// Top level ComicInfo.xml
|
||||
comicInfoFile != null -> {
|
||||
noXmlFile?.delete()
|
||||
setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga)
|
||||
}
|
||||
|
||||
// TODO: automatically convert these to ComicInfo.xml
|
||||
legacyJsonDetailsFile != null -> {
|
||||
json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.inputStream()).run {
|
||||
title?.let { manga.title = it }
|
||||
author?.let { manga.author = it }
|
||||
artist?.let { manga.artist = it }
|
||||
description?.let { manga.description = it }
|
||||
genre?.let { manga.genre = it.joinToString() }
|
||||
status?.let { manga.status = it }
|
||||
}
|
||||
}
|
||||
|
||||
// Copy ComicInfo.xml from chapter archive to top level if found
|
||||
noXmlFile == null -> {
|
||||
val chapterArchives =
|
||||
mangaDirFiles
|
||||
.filter(Archive::isSupported)
|
||||
.toList()
|
||||
|
||||
val mangaDir = fileSystem.getMangaDirectory(manga.url)
|
||||
val folderPath = mangaDir?.absolutePath
|
||||
|
||||
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
|
||||
if (copiedFile != null) {
|
||||
setMangaDetailsFromComicInfoFile(copiedFile.inputStream(), manga)
|
||||
} else {
|
||||
// Avoid re-scanning
|
||||
File("$folderPath/.noxml").createNewFile()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logger.error(e) { "Error setting manga details from local metadata for ${manga.title}" }
|
||||
}
|
||||
|
||||
return@withContext manga
|
||||
}
|
||||
|
||||
return@withContext manga
|
||||
}
|
||||
|
||||
private fun copyComicInfoFileFromArchive(chapterArchives: List<File>, folderPath: String?): File? {
|
||||
private fun copyComicInfoFileFromArchive(
|
||||
chapterArchives: List<File>,
|
||||
folderPath: String?,
|
||||
): File? {
|
||||
for (chapter in chapterArchives) {
|
||||
when (Format.valueOf(chapter)) {
|
||||
is Format.Zip -> {
|
||||
@@ -243,7 +260,10 @@ class LocalSource(
|
||||
return null
|
||||
}
|
||||
|
||||
private fun copyComicInfoFile(comicInfoFileStream: InputStream, folderPath: String?): File {
|
||||
private fun copyComicInfoFile(
|
||||
comicInfoFileStream: InputStream,
|
||||
folderPath: String?,
|
||||
): File {
|
||||
return File("$folderPath/$COMIC_INFO_FILE").apply {
|
||||
outputStream().use { outputStream ->
|
||||
comicInfoFileStream.use { it.copyTo(outputStream) }
|
||||
@@ -251,10 +271,14 @@ class LocalSource(
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMangaDetailsFromComicInfoFile(stream: InputStream, manga: SManga) {
|
||||
val comicInfo = KtXmlReader(stream, StandardCharsets.UTF_8.name()).use {
|
||||
xml.decodeFromReader<ComicInfo>(it)
|
||||
}
|
||||
private fun setMangaDetailsFromComicInfoFile(
|
||||
stream: InputStream,
|
||||
manga: SManga,
|
||||
) {
|
||||
val comicInfo =
|
||||
KtXmlReader(stream, StandardCharsets.UTF_8.name()).use {
|
||||
xml.decodeFromReader<ComicInfo>(it)
|
||||
}
|
||||
|
||||
manga.copyFromComicInfo(comicInfo)
|
||||
}
|
||||
@@ -267,15 +291,17 @@ class LocalSource(
|
||||
.map { chapterFile ->
|
||||
SChapter.create().apply {
|
||||
url = "${manga.url}/${chapterFile.name}"
|
||||
name = if (chapterFile.isDirectory) {
|
||||
chapterFile.name
|
||||
} else {
|
||||
chapterFile.nameWithoutExtension
|
||||
}
|
||||
name =
|
||||
if (chapterFile.isDirectory) {
|
||||
chapterFile.name
|
||||
} else {
|
||||
chapterFile.nameWithoutExtension
|
||||
}
|
||||
date_upload = chapterFile.lastModified()
|
||||
chapter_number = ChapterRecognition
|
||||
.parseChapterNumber(manga.title, this.name, this.chapter_number.toDouble())
|
||||
.toFloat()
|
||||
chapter_number =
|
||||
ChapterRecognition
|
||||
.parseChapterNumber(manga.title, this.name, this.chapter_number.toDouble())
|
||||
.toFloat()
|
||||
|
||||
val format = Format.valueOf(chapterFile)
|
||||
if (format is Format.Epub) {
|
||||
@@ -305,7 +331,7 @@ class LocalSource(
|
||||
.mapIndexed { index, page ->
|
||||
Page(
|
||||
index,
|
||||
imageUrl = applicationDirs.localMangaRoot + "/" + chapter.url + "/" + page.name
|
||||
imageUrl = applicationDirs.localMangaRoot + "/" + chapter.url + "/" + page.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -347,39 +373,46 @@ class LocalSource(
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCover(chapter: SChapter, manga: SManga): File? {
|
||||
private fun updateCover(
|
||||
chapter: SChapter,
|
||||
manga: SManga,
|
||||
): File? {
|
||||
return try {
|
||||
when (val format = getFormat(chapter)) {
|
||||
is Format.Directory -> {
|
||||
val entry = format.file.listFiles()
|
||||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
val entry =
|
||||
format.file.listFiles()
|
||||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
|
||||
entry?.let { coverManager.update(manga, it.inputStream()) }
|
||||
}
|
||||
is Format.Zip -> {
|
||||
ZipFile(format.file).use { zip ->
|
||||
val entry = zip.entries.toList()
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
val entry =
|
||||
zip.entries.toList()
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
|
||||
entry?.let { coverManager.update(manga, zip.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
is Format.Rar -> {
|
||||
JunrarArchive(format.file).use { archive ->
|
||||
val entry = archive.fileHeaders
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||
val entry =
|
||||
archive.fileHeaders
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||
|
||||
entry?.let { coverManager.update(manga, archive.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
is Format.Epub -> {
|
||||
EpubFile(format.file).use { epub ->
|
||||
val entry = epub.getImagesFromPages()
|
||||
.firstOrNull()
|
||||
?.let { epub.getEntry(it) }
|
||||
val entry =
|
||||
epub.getImagesFromPages()
|
||||
.firstOrNull()
|
||||
?.let { epub.getEntry(it) }
|
||||
|
||||
entry?.let { coverManager.update(manga, epub.getInputStream(it)) }
|
||||
}
|
||||
@@ -412,16 +445,17 @@ class LocalSource(
|
||||
|
||||
if (sourceRecord == null) {
|
||||
// must do this to avoid database integrity errors
|
||||
val extensionId = ExtensionTable.insertAndGetId {
|
||||
it[apkName] = "localSource"
|
||||
it[name] = EXTENSION_NAME
|
||||
it[pkgName] = LocalSource::class.java.`package`.name
|
||||
it[versionName] = "1.2"
|
||||
it[versionCode] = 0
|
||||
it[lang] = LANG
|
||||
it[isNsfw] = false
|
||||
it[isInstalled] = true
|
||||
}
|
||||
val extensionId =
|
||||
ExtensionTable.insertAndGetId {
|
||||
it[apkName] = "localSource"
|
||||
it[name] = EXTENSION_NAME
|
||||
it[pkgName] = LocalSource::class.java.`package`.name
|
||||
it[versionName] = "1.2"
|
||||
it[versionCode] = 0
|
||||
it[lang] = LANG
|
||||
it[isNsfw] = false
|
||||
it[isInstalled] = true
|
||||
}
|
||||
|
||||
SourceTable.insert {
|
||||
it[id] = ID
|
||||
|
||||
@@ -5,8 +5,9 @@ import eu.kanade.tachiyomi.source.model.Filter
|
||||
sealed class OrderBy(selection: Selection) : Filter.Sort(
|
||||
"Order by",
|
||||
arrayOf("Title", "Date"),
|
||||
selection
|
||||
selection,
|
||||
) {
|
||||
class Popular() : OrderBy(Selection(0, true))
|
||||
|
||||
class Latest() : OrderBy(Selection(1, false))
|
||||
}
|
||||
|
||||
@@ -9,9 +9,8 @@ import java.io.InputStream
|
||||
private const val DEFAULT_COVER_NAME = "cover.jpg"
|
||||
|
||||
class LocalCoverManager(
|
||||
private val fileSystem: LocalSourceFileSystem
|
||||
private val fileSystem: LocalSourceFileSystem,
|
||||
) {
|
||||
|
||||
fun find(mangaUrl: String): File? {
|
||||
return fileSystem.getFilesInMangaDirectory(mangaUrl)
|
||||
// Get all file whose names start with 'cover'
|
||||
@@ -24,7 +23,7 @@ class LocalCoverManager(
|
||||
|
||||
fun update(
|
||||
manga: SManga,
|
||||
inputStream: InputStream
|
||||
inputStream: InputStream,
|
||||
): File? {
|
||||
val directory = fileSystem.getMangaDirectory(manga.url)
|
||||
if (directory == null) {
|
||||
|
||||
@@ -3,10 +3,10 @@ package eu.kanade.tachiyomi.source.local.io
|
||||
import java.io.File
|
||||
|
||||
object Archive {
|
||||
|
||||
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
|
||||
|
||||
fun isSupported(file: File): Boolean = with(file) {
|
||||
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
|
||||
}
|
||||
fun isSupported(file: File): Boolean =
|
||||
with(file) {
|
||||
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,22 +4,25 @@ import java.io.File
|
||||
|
||||
sealed interface 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
|
||||
|
||||
class UnknownFormatException : Exception()
|
||||
|
||||
companion object {
|
||||
|
||||
fun valueOf(file: File) = with(file) {
|
||||
when {
|
||||
isDirectory -> Directory(this)
|
||||
extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this)
|
||||
extension.equals("rar", true) || extension.equals("cbr", true) -> Rar(this)
|
||||
extension.equals("epub", true) -> Epub(this)
|
||||
else -> throw UnknownFormatException()
|
||||
fun valueOf(file: File) =
|
||||
with(file) {
|
||||
when {
|
||||
isDirectory -> Directory(this)
|
||||
extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this)
|
||||
extension.equals("rar", true) || extension.equals("cbr", true) -> Rar(this)
|
||||
extension.equals("epub", true) -> Epub(this)
|
||||
else -> throw UnknownFormatException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@ import suwayomi.tachidesk.server.ApplicationDirs
|
||||
import java.io.File
|
||||
|
||||
class LocalSourceFileSystem(
|
||||
private val applicationDirs: ApplicationDirs
|
||||
private val applicationDirs: ApplicationDirs,
|
||||
) {
|
||||
|
||||
fun getBaseDirectories(): Sequence<File> {
|
||||
return sequenceOf(File(applicationDirs.localMangaRoot))
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import java.io.File
|
||||
* Loader used to load a chapter from a .epub file.
|
||||
*/
|
||||
class EpubPageLoader(file: File) : PageLoader {
|
||||
|
||||
private val epub = EpubFile(file)
|
||||
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
|
||||
@@ -13,7 +13,6 @@ import java.io.PipedOutputStream
|
||||
* Loader used to load a chapter from a .rar or .cbr file.
|
||||
*/
|
||||
class RarPageLoader(file: File) : PageLoader {
|
||||
|
||||
private val rar = Archive(file)
|
||||
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
@@ -35,7 +34,10 @@ class RarPageLoader(file: File) : PageLoader {
|
||||
/**
|
||||
* Returns an input stream for the given [header].
|
||||
*/
|
||||
private fun getStream(rar: Archive, header: FileHeader): InputStream {
|
||||
private fun getStream(
|
||||
rar: Archive,
|
||||
header: FileHeader,
|
||||
): InputStream {
|
||||
val pipeIn = PipedInputStream()
|
||||
val pipeOut = PipedOutputStream(pipeIn)
|
||||
synchronized(this) {
|
||||
|
||||
@@ -7,5 +7,5 @@ class ReaderPage(
|
||||
index: Int,
|
||||
url: String = "",
|
||||
imageUrl: String? = null,
|
||||
var stream: (() -> InputStream)? = null
|
||||
var stream: (() -> InputStream)? = null,
|
||||
) : Page(index, url, imageUrl, null)
|
||||
|
||||
@@ -9,7 +9,6 @@ import java.io.File
|
||||
* Loader used to load a chapter from a .zip or .cbz file.
|
||||
*/
|
||||
class ZipPageLoader(file: File) : PageLoader {
|
||||
|
||||
private val zip = ZipFile(file)
|
||||
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
|
||||
@@ -16,7 +16,7 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
|
||||
listOfNotNull(
|
||||
comicInfo.genre?.value,
|
||||
comicInfo.tags?.value,
|
||||
comicInfo.categories?.value
|
||||
comicInfo.categories?.value,
|
||||
)
|
||||
.distinct()
|
||||
.joinToString(", ") { it.trim() }
|
||||
@@ -28,7 +28,7 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
|
||||
comicInfo.inker?.value,
|
||||
comicInfo.colorist?.value,
|
||||
comicInfo.letterer?.value,
|
||||
comicInfo.coverArtist?.value
|
||||
comicInfo.coverArtist?.value,
|
||||
)
|
||||
.flatMap { it.split(", ") }
|
||||
.distinct()
|
||||
@@ -57,7 +57,7 @@ data class ComicInfo(
|
||||
val tags: Tags?,
|
||||
val web: Web?,
|
||||
val publishingStatus: PublishingStatusTachiyomi?,
|
||||
val categories: CategoriesTachiyomi?
|
||||
val categories: CategoriesTachiyomi?,
|
||||
) {
|
||||
@Suppress("UNUSED")
|
||||
@XmlElement(false)
|
||||
@@ -71,73 +71,105 @@ data class ComicInfo(
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Title", "", "")
|
||||
data class Title(@XmlValue(true) val value: String = "")
|
||||
data class Title(
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Series", "", "")
|
||||
data class Series(@XmlValue(true) val value: String = "")
|
||||
data class Series(
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Number", "", "")
|
||||
data class Number(@XmlValue(true) val value: String = "")
|
||||
data class Number(
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Summary", "", "")
|
||||
data class Summary(@XmlValue(true) val value: String = "")
|
||||
data class Summary(
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Writer", "", "")
|
||||
data class Writer(@XmlValue(true) val value: String = "")
|
||||
data class Writer(
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Penciller", "", "")
|
||||
data class Penciller(@XmlValue(true) val value: String = "")
|
||||
data class Penciller(
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Inker", "", "")
|
||||
data class Inker(@XmlValue(true) val value: String = "")
|
||||
data class Inker(
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Colorist", "", "")
|
||||
data class Colorist(@XmlValue(true) val value: String = "")
|
||||
data class Colorist(
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Letterer", "", "")
|
||||
data class Letterer(@XmlValue(true) val value: String = "")
|
||||
data class Letterer(
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("CoverArtist", "", "")
|
||||
data class CoverArtist(@XmlValue(true) val value: String = "")
|
||||
data class CoverArtist(
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Translator", "", "")
|
||||
data class Translator(@XmlValue(true) val value: String = "")
|
||||
data class Translator(
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Genre", "", "")
|
||||
data class Genre(@XmlValue(true) val value: String = "")
|
||||
data class Genre(
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Tags", "", "")
|
||||
data class Tags(@XmlValue(true) val value: String = "")
|
||||
data class Tags(
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Web", "", "")
|
||||
data class Web(@XmlValue(true) val value: String = "")
|
||||
data class Web(
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
// The spec doesn't have a good field for this
|
||||
@Serializable
|
||||
@XmlSerialName("PublishingStatusTachiyomi", "http://www.w3.org/2001/XMLSchema", "ty")
|
||||
data class PublishingStatusTachiyomi(@XmlValue(true) val value: String = "")
|
||||
data class PublishingStatusTachiyomi(
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Categories", "http://www.w3.org/2001/XMLSchema", "ty")
|
||||
data class CategoriesTachiyomi(@XmlValue(true) val value: String = "")
|
||||
data class CategoriesTachiyomi(
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
}
|
||||
|
||||
enum class ComicInfoPublishingStatus(
|
||||
val comicInfoValue: String,
|
||||
val sMangaModelValue: Int
|
||||
val sMangaModelValue: Int,
|
||||
) {
|
||||
ONGOING("Ongoing", SManga.ONGOING),
|
||||
COMPLETED("Completed", SManga.COMPLETED),
|
||||
@@ -145,7 +177,7 @@ enum class ComicInfoPublishingStatus(
|
||||
PUBLISHING_FINISHED("Publishing finished", SManga.PUBLISHING_FINISHED),
|
||||
CANCELLED("Cancelled", SManga.CANCELLED),
|
||||
ON_HIATUS("On hiatus", SManga.ON_HIATUS),
|
||||
UNKNOWN("Unknown", SManga.UNKNOWN)
|
||||
UNKNOWN("Unknown", SManga.UNKNOWN),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -9,5 +9,5 @@ class MangaDetails(
|
||||
val artist: String? = null,
|
||||
val description: String? = null,
|
||||
val genre: List<String>? = null,
|
||||
val status: Int? = null
|
||||
val status: Int? = null,
|
||||
)
|
||||
|
||||
@@ -4,15 +4,22 @@ package eu.kanade.tachiyomi.source.model
|
||||
// sealed class Filter<T>(val name: String, var state: T) {
|
||||
open class Filter<T>(val name: String, var state: T) {
|
||||
open class Header(name: String) : Filter<Any>(name, 0)
|
||||
|
||||
open class Separator(name: String = "") : Filter<Any>(name, 0)
|
||||
|
||||
abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state) {
|
||||
val displayValues get() = values.map { it.toString() }
|
||||
}
|
||||
|
||||
abstract class Text(name: String, state: String = "") : Filter<String>(name, state)
|
||||
|
||||
abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state)
|
||||
|
||||
abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) {
|
||||
fun isIgnored() = state == STATE_IGNORE
|
||||
|
||||
fun isIncluded() = state == STATE_INCLUDE
|
||||
|
||||
fun isExcluded() = state == STATE_EXCLUDE
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
|
||||
|
||||
constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
|
||||
}
|
||||
|
||||
@@ -9,18 +9,23 @@ open class Page(
|
||||
val index: Int,
|
||||
val url: String = "",
|
||||
var imageUrl: String? = null,
|
||||
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
|
||||
// Deprecated but can't be deleted due to extensions
|
||||
@Transient var uri: Uri? = null,
|
||||
) : ProgressListener {
|
||||
|
||||
private val _progress = MutableStateFlow(0)
|
||||
val progress = _progress.asStateFlow()
|
||||
|
||||
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||
_progress.value = if (contentLength > 0) {
|
||||
(100 * bytesRead / contentLength).toInt()
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
override fun update(
|
||||
bytesRead: Long,
|
||||
contentLength: Long,
|
||||
done: Boolean,
|
||||
) {
|
||||
_progress.value =
|
||||
if (contentLength > 0) {
|
||||
(100 * bytesRead / contentLength).toInt()
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
interface SChapter : Serializable {
|
||||
|
||||
var url: String
|
||||
|
||||
var name: String
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
class SChapterImpl : SChapter {
|
||||
|
||||
override lateinit var url: String
|
||||
|
||||
override lateinit var name: String
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
interface SManga : Serializable {
|
||||
|
||||
var url: String
|
||||
|
||||
var title: String
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
class SMangaImpl : SManga {
|
||||
|
||||
override lateinit var url: String
|
||||
|
||||
override lateinit var title: String
|
||||
|
||||
@@ -2,5 +2,5 @@ package eu.kanade.tachiyomi.source.model
|
||||
|
||||
enum class UpdateStrategy {
|
||||
ALWAYS_UPDATE,
|
||||
ONLY_FETCH_ONCE
|
||||
ONLY_FETCH_ONCE,
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import okhttp3.Response
|
||||
import rx.Observable
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
// import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.security.MessageDigest
|
||||
@@ -28,7 +27,6 @@ import java.security.MessageDigest
|
||||
* A simple implementation for sources from a website.
|
||||
*/
|
||||
abstract class HttpSource : CatalogueSource {
|
||||
|
||||
/**
|
||||
* Network service.
|
||||
*/
|
||||
@@ -91,7 +89,11 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param versionId [Int] the version ID of the source
|
||||
* @return a unique ID for the source
|
||||
*/
|
||||
protected fun generateId(name: String, lang: String, versionId: Int): Long {
|
||||
protected fun generateId(
|
||||
name: String,
|
||||
lang: String,
|
||||
versionId: Int,
|
||||
): Long {
|
||||
val key = "${name.lowercase()}/$lang/$versionId"
|
||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||
return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||
@@ -100,9 +102,10 @@ abstract class HttpSource : CatalogueSource {
|
||||
/**
|
||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||
*/
|
||||
protected open fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", DEFAULT_USER_AGENT)
|
||||
}
|
||||
protected open fun headersBuilder() =
|
||||
Headers.Builder().apply {
|
||||
add("User-Agent", DEFAULT_USER_AGENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Visible name of the source.
|
||||
@@ -147,7 +150,11 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
override fun fetchSearchManga(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): Observable<MangasPage> {
|
||||
return client.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
@@ -162,7 +169,11 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
protected abstract fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request
|
||||
protected abstract fun searchMangaRequest(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): Request
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
@@ -450,7 +461,10 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param chapter the chapter to be added.
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
open fun prepareNewChapter(chapter: SChapter, manga: SManga) {}
|
||||
open fun prepareNewChapter(
|
||||
chapter: SChapter,
|
||||
manga: SManga,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
|
||||
@@ -13,7 +13,6 @@ import org.jsoup.nodes.Element
|
||||
* A simple implementation for sources from a website using Jsoup, an HTML parser.
|
||||
*/
|
||||
abstract class ParsedHttpSource : HttpSource() {
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
@@ -22,13 +21,15 @@ abstract class ParsedHttpSource : HttpSource() {
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(popularMangaSelector()).map { element ->
|
||||
popularMangaFromElement(element)
|
||||
}
|
||||
val mangas =
|
||||
document.select(popularMangaSelector()).map { element ->
|
||||
popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
val hasNextPage =
|
||||
popularMangaNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
@@ -60,13 +61,15 @@ abstract class ParsedHttpSource : HttpSource() {
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(searchMangaSelector()).map { element ->
|
||||
searchMangaFromElement(element)
|
||||
}
|
||||
val mangas =
|
||||
document.select(searchMangaSelector()).map { element ->
|
||||
searchMangaFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
val hasNextPage =
|
||||
searchMangaNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
@@ -98,13 +101,15 @@ abstract class ParsedHttpSource : HttpSource() {
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(latestUpdatesSelector()).map { element ->
|
||||
latestUpdatesFromElement(element)
|
||||
}
|
||||
val mangas =
|
||||
document.select(latestUpdatesSelector()).map { element ->
|
||||
latestUpdatesFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
val hasNextPage =
|
||||
latestUpdatesNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||
*/
|
||||
@Suppress("unused")
|
||||
interface ResolvableSource : Source {
|
||||
|
||||
/**
|
||||
* Whether this source may potentially handle the given URI.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user