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:
Mitchell Syer
2023-10-06 23:38:39 -04:00
committed by GitHub
parent 3cd3cb0186
commit 849acfca3d
277 changed files with 6709 additions and 5090 deletions

View File

@@ -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")
}

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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))
}

View File

@@ -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) {

View File

@@ -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
}
}

View File

@@ -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()
}
}
}
}
}

View File

@@ -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))
}

View File

@@ -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> {

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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> {

View File

@@ -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 {

View File

@@ -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,
)

View File

@@ -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 {

View File

@@ -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())
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -2,5 +2,5 @@ package eu.kanade.tachiyomi.source.model
enum class UpdateStrategy {
ALWAYS_UPDATE,
ONLY_FETCH_ONCE
ONLY_FETCH_ONCE,
}

View File

@@ -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.

View File

@@ -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)
}

View File

@@ -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.
*