mirror of
https://github.com/keiyoushi/extensions-source.git
synced 2024-11-22 10:22:47 +01:00
Komga: README, chapter timestamp shenanigans, refactor (#1313)
* Komga: README, chapter timestamp shenanigans, refactor * Repeating by password length is probably fine * Reuse the CoroutineScope
This commit is contained in:
parent
bbeb5e0157
commit
ca3589bb80
41
src/all/komga/README.md
Normal file
41
src/all/komga/README.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Komga
|
||||||
|
|
||||||
|
Table of Content
|
||||||
|
- [FAQ](#FAQ)
|
||||||
|
- [Why do I see no manga?](#why-do-i-see-no-manga)
|
||||||
|
- [Where can I get more information about Komga?](#where-can-i-get-more-information-about-komga)
|
||||||
|
- [The Komga extension stopped working?](#the-komga-extension-stopped-working)
|
||||||
|
- [Can I add more than one Komga server or user?](#can-i-add-more-than-one-komga-server-or-user)
|
||||||
|
- [Can I test the Komga extension before setting up my own server?](#can-i-test-the-komga-extension-before-setting-up-my-own-server)
|
||||||
|
- [Guides](#Guides)
|
||||||
|
- [How do I add my Komga server to the app?](#how-do-i-add-my-komga-server-to-the-app)
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Why do I see no manga?
|
||||||
|
Komga is a self-hosted comic/manga media server.
|
||||||
|
|
||||||
|
### Where can I get more information about Komga?
|
||||||
|
You can visit the [Komga](https://komga.org/) website for for more information.
|
||||||
|
|
||||||
|
### The Komga extension stopped working?
|
||||||
|
Make sure that your Komga server and extension are on the newest version.
|
||||||
|
|
||||||
|
### Can I add more than one Komga server or user?
|
||||||
|
Yes, currently you can add up to 11 different Komga instances to the app. The number of instances
|
||||||
|
available can be customized in extension settings.
|
||||||
|
|
||||||
|
### Can I test the Komga extension before setting up my own server?
|
||||||
|
Yes, you can try it out with the DEMO server `https://demo.komga.org`, username `demo@komga.org` and
|
||||||
|
password `komga-demo`.
|
||||||
|
|
||||||
|
### Why are EPUB chapters/books missing?
|
||||||
|
Mihon/Tachiyomi does not support reading text. EPUB files containing only images will be available
|
||||||
|
if your Komga server version is [1.9.0](https://github.com/gotson/komga/releases/tag/1.9.0)
|
||||||
|
or newer.
|
||||||
|
|
||||||
|
## Guides
|
||||||
|
|
||||||
|
### How do I add my Komga server to the app?
|
||||||
|
Go into the settings of the Komga extension (under Browse -> Extensions) and fill in your server
|
||||||
|
address and login details.
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Komga'
|
extName = 'Komga'
|
||||||
extClass = '.KomgaFactory'
|
extClass = '.KomgaFactory'
|
||||||
extVersionCode = 55
|
extVersionCode = 56
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -5,15 +5,10 @@ import android.content.SharedPreferences
|
|||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.preference.EditTextPreference
|
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.MultiSelectListPreference
|
import androidx.preference.MultiSelectListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.AppInfo
|
import eu.kanade.tachiyomi.AppInfo
|
||||||
import eu.kanade.tachiyomi.extension.all.komga.KomgaUtils.addEditTextPreference
|
|
||||||
import eu.kanade.tachiyomi.extension.all.komga.KomgaUtils.isFromReadList
|
|
||||||
import eu.kanade.tachiyomi.extension.all.komga.KomgaUtils.parseAs
|
|
||||||
import eu.kanade.tachiyomi.extension.all.komga.KomgaUtils.toSManga
|
|
||||||
import eu.kanade.tachiyomi.extension.all.komga.dto.AuthorDto
|
import eu.kanade.tachiyomi.extension.all.komga.dto.AuthorDto
|
||||||
import eu.kanade.tachiyomi.extension.all.komga.dto.BookDto
|
import eu.kanade.tachiyomi.extension.all.komga.dto.BookDto
|
||||||
import eu.kanade.tachiyomi.extension.all.komga.dto.CollectionDto
|
import eu.kanade.tachiyomi.extension.all.komga.dto.CollectionDto
|
||||||
@ -23,6 +18,7 @@ import eu.kanade.tachiyomi.extension.all.komga.dto.PageWrapperDto
|
|||||||
import eu.kanade.tachiyomi.extension.all.komga.dto.ReadListDto
|
import eu.kanade.tachiyomi.extension.all.komga.dto.ReadListDto
|
||||||
import eu.kanade.tachiyomi.extension.all.komga.dto.SeriesDto
|
import eu.kanade.tachiyomi.extension.all.komga.dto.SeriesDto
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
@ -32,6 +28,11 @@ import eu.kanade.tachiyomi.source.model.Page
|
|||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Credentials
|
import okhttp3.Credentials
|
||||||
import okhttp3.Dns
|
import okhttp3.Dns
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
@ -39,15 +40,12 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Single
|
import org.apache.commons.text.StringSubstitutor
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
|
||||||
import kotlin.concurrent.read
|
|
||||||
import kotlin.concurrent.write
|
|
||||||
|
|
||||||
open class Komga(private val suffix: String = "") : ConfigurableSource, UnmeteredSource, HttpSource() {
|
open class Komga(private val suffix: String = "") : ConfigurableSource, UnmeteredSource, HttpSource() {
|
||||||
|
|
||||||
@ -57,7 +55,13 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
|
|
||||||
private val displayName by lazy { preferences.getString(PREF_DISPLAY_NAME, "")!! }
|
private val displayName by lazy { preferences.getString(PREF_DISPLAY_NAME, "")!! }
|
||||||
|
|
||||||
override val name by lazy { "Komga${displayName.ifBlank { suffix }.let { if (it.isNotBlank()) " ($it)" else "" }}" }
|
override val name by lazy {
|
||||||
|
val displayNameSuffix = displayName
|
||||||
|
.ifBlank { suffix }
|
||||||
|
.let { if (it.isNotBlank()) " ($it)" else "" }
|
||||||
|
|
||||||
|
"Komga$displayNameSuffix"
|
||||||
|
}
|
||||||
|
|
||||||
override val lang = "all"
|
override val lang = "all"
|
||||||
|
|
||||||
@ -79,6 +83,8 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
private val defaultLibraries
|
private val defaultLibraries
|
||||||
get() = preferences.getStringSet(PREF_DEFAULT_LIBRARIES, emptySet())!!
|
get() = preferences.getStringSet(PREF_DEFAULT_LIBRARIES, emptySet())!!
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.set("User-Agent", "TachiyomiKomga/${AppInfo.getVersionName()}")
|
.set("User-Agent", "TachiyomiKomga/${AppInfo.getVersionName()}")
|
||||||
|
|
||||||
@ -106,7 +112,7 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
)
|
)
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage =
|
override fun popularMangaParse(response: Response): MangasPage =
|
||||||
KomgaUtils.processSeriesPage(response, baseUrl)
|
processSeriesPage(response, baseUrl)
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request =
|
override fun latestUpdatesRequest(page: Int): Request =
|
||||||
searchMangaRequest(
|
searchMangaRequest(
|
||||||
@ -118,11 +124,9 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
)
|
)
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage =
|
override fun latestUpdatesParse(response: Response): MangasPage =
|
||||||
KomgaUtils.processSeriesPage(response, baseUrl)
|
processSeriesPage(response, baseUrl)
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
runCatching { fetchFilterOptions() }
|
|
||||||
|
|
||||||
val collectionId = (filters.find { it is CollectionSelect } as? CollectionSelect)?.let {
|
val collectionId = (filters.find { it is CollectionSelect } as? CollectionSelect)?.let {
|
||||||
it.collections[it.state].id
|
it.collections[it.state].id
|
||||||
}
|
}
|
||||||
@ -164,7 +168,17 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage =
|
override fun searchMangaParse(response: Response): MangasPage =
|
||||||
KomgaUtils.processSeriesPage(response, baseUrl)
|
processSeriesPage(response, baseUrl)
|
||||||
|
|
||||||
|
private fun processSeriesPage(response: Response, baseUrl: String): MangasPage {
|
||||||
|
val data = if (response.isFromReadList()) {
|
||||||
|
response.parseAs<PageWrapperDto<ReadListDto>>()
|
||||||
|
} else {
|
||||||
|
response.parseAs<PageWrapperDto<SeriesDto>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(data.content.map { it.toSManga(baseUrl) }, !data.last)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga) = manga.url.replace("/api/v1", "")
|
override fun getMangaUrl(manga: SManga) = manga.url.replace("/api/v1", "")
|
||||||
|
|
||||||
@ -199,10 +213,19 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
chapter_number = if (!isFromReadList) book.metadata.numberSort else index + 1F
|
chapter_number = if (!isFromReadList) book.metadata.numberSort else index + 1F
|
||||||
url = "$baseUrl/api/v1/books/${book.id}"
|
url = "$baseUrl/api/v1/books/${book.id}"
|
||||||
name = KomgaUtils.formatChapterName(book, chapterNameTemplate, isFromReadList)
|
name = book.getChapterName(chapterNameTemplate, isFromReadList)
|
||||||
scanlator = book.metadata.authors.filter { it.role == "translator" }.joinToString { it.name }
|
scanlator = book.metadata.authors
|
||||||
date_upload = book.metadata.releaseDate?.let { KomgaUtils.parseDate(it) }
|
.filter { it.role == "translator" }
|
||||||
?: KomgaUtils.parseDateTime(book.lastModified)
|
.joinToString { it.name }
|
||||||
|
date_upload = when {
|
||||||
|
book.metadata.releaseDate != null -> parseDate(book.metadata.releaseDate)
|
||||||
|
book.created != null -> parseDateTime(book.created)
|
||||||
|
|
||||||
|
// XXX: `Book.fileLastModified` actually uses the server's running timezone,
|
||||||
|
// not UTC, even if the timestamp ends with a Z! We cannot determine the
|
||||||
|
// server's timezone, which is why this is a last resort option.
|
||||||
|
else -> parseDateTime(book.fileLastModified)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sortedByDescending { it.chapter_number }
|
.sortedByDescending { it.chapter_number }
|
||||||
@ -215,7 +238,7 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
|
|
||||||
return pages.map {
|
return pages.map {
|
||||||
val url = "${response.request.url}/${it.number}" +
|
val url = "${response.request.url}/${it.number}" +
|
||||||
if (!supportedImageTypes.contains(it.mediaType)) {
|
if (!SUPPORTED_IMAGE_TYPES.contains(it.mediaType)) {
|
||||||
"?convert=png"
|
"?convert=png"
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
@ -228,6 +251,8 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||||
|
|
||||||
override fun getFilterList(): FilterList {
|
override fun getFilterList(): FilterList {
|
||||||
|
fetchFilterOptions()
|
||||||
|
|
||||||
val filters = mutableListOf<Filter<*>>(
|
val filters = mutableListOf<Filter<*>>(
|
||||||
UnreadFilter(),
|
UnreadFilter(),
|
||||||
InProgressFilter(),
|
InProgressFilter(),
|
||||||
@ -265,8 +290,14 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
publishers.map { UriMultiSelectOption(it) },
|
publishers.map { UriMultiSelectOption(it) },
|
||||||
),
|
),
|
||||||
).apply {
|
).apply {
|
||||||
if (collections.isEmpty() && libraries.isEmpty() && genres.isEmpty() && tags.isEmpty() && publishers.isEmpty()) {
|
if (fetchFilterStatus != FetchFilterStatus.FETCHED) {
|
||||||
add(0, Filter.Header("Press 'Reset' to show filtering options"))
|
val message = if (fetchFilterStatus == FetchFilterStatus.NOT_FETCHED && fetchFiltersAttempts >= 3) {
|
||||||
|
"Failed to fetch filtering options from the server"
|
||||||
|
} else {
|
||||||
|
"Press 'Reset' to show filtering options"
|
||||||
|
}
|
||||||
|
|
||||||
|
add(0, Filter.Header(message))
|
||||||
add(1, Filter.Separator())
|
add(1, Filter.Separator())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,6 +309,8 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
fetchFilterOptions()
|
||||||
|
|
||||||
if (suffix.isEmpty()) {
|
if (suffix.isEmpty()) {
|
||||||
ListPreference(screen.context).apply {
|
ListPreference(screen.context).apply {
|
||||||
key = PREF_EXTRA_SOURCES_COUNT
|
key = PREF_EXTRA_SOURCES_COUNT
|
||||||
@ -305,9 +338,10 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
title = "Address",
|
title = "Address",
|
||||||
default = "",
|
default = "",
|
||||||
summary = baseUrl.ifBlank { "The server address" },
|
summary = baseUrl.ifBlank { "The server address" },
|
||||||
|
dialogMessage = "The address must not end with a forward slash.",
|
||||||
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI,
|
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI,
|
||||||
validate = { it.toHttpUrlOrNull() != null },
|
validate = { it.toHttpUrlOrNull() != null && !it.endsWith("/") },
|
||||||
validationMessage = "The URL is invalid or malformed",
|
validationMessage = "The URL is invalid, malformed, or ends with a slash",
|
||||||
key = PREF_ADDRESS,
|
key = PREF_ADDRESS,
|
||||||
restartRequired = true,
|
restartRequired = true,
|
||||||
)
|
)
|
||||||
@ -334,7 +368,7 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
append("Show content from selected libraries by default.")
|
append("Show content from selected libraries by default.")
|
||||||
|
|
||||||
if (libraries.isEmpty()) {
|
if (libraries.isEmpty()) {
|
||||||
append(" Browse the source to load available options.")
|
append(" Exit and enter the settings menu to load options.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entries = libraries.map { it.name }.toTypedArray()
|
entries = libraries.map { it.name }.toTypedArray()
|
||||||
@ -342,10 +376,25 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
setDefaultValue(emptySet<String>())
|
setDefaultValue(emptySet<String>())
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
EditTextPreference(screen.context).apply {
|
val values = hashMapOf(
|
||||||
key = PREF_CHAPTER_NAME_TEMPLATE
|
"title" to "",
|
||||||
title = "Chapter title format"
|
"seriesTitle" to "",
|
||||||
summary = "Customize how chapter names appear. Chapters in read lists will always be prefixed by the series' name."
|
"number" to "",
|
||||||
|
"createdDate" to "",
|
||||||
|
"releaseDate" to "",
|
||||||
|
"size" to "",
|
||||||
|
"sizeBytes" to "",
|
||||||
|
)
|
||||||
|
val stringSubstitutor = StringSubstitutor(values, "{", "}").apply {
|
||||||
|
isEnableUndefinedVariableException = true
|
||||||
|
}
|
||||||
|
|
||||||
|
screen.addEditTextPreference(
|
||||||
|
key = PREF_CHAPTER_NAME_TEMPLATE,
|
||||||
|
title = "Chapter title format",
|
||||||
|
summary = "Customize how chapter names appear. Chapters in read lists will always be prefixed by the series' name.",
|
||||||
|
inputType = InputType.TYPE_CLASS_TEXT,
|
||||||
|
default = PREF_CHAPTER_NAME_TEMPLATE_DEFAULT,
|
||||||
dialogMessage = """
|
dialogMessage = """
|
||||||
|Supported placeholders:
|
|Supported placeholders:
|
||||||
|- {title}: Chapter name
|
|- {title}: Chapter name
|
||||||
@ -355,10 +404,19 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
|- {releaseDate}: Chapter release date
|
|- {releaseDate}: Chapter release date
|
||||||
|- {size}: Chapter file size (formatted)
|
|- {size}: Chapter file size (formatted)
|
||||||
|- {sizeBytes}: Chapter file size (in bytes)
|
|- {sizeBytes}: Chapter file size (in bytes)
|
||||||
""".trimMargin()
|
|If you wish to place some text between curly brackets, place the escape character "$"
|
||||||
|
|before the opening curly bracket, e.g. ${'$'}{series}.
|
||||||
setDefaultValue(PREF_CHAPTER_NAME_TEMPLATE_DEFAULT)
|
""".trimMargin(),
|
||||||
}.also(screen::addPreference)
|
validate = {
|
||||||
|
try {
|
||||||
|
stringSubstitutor.replace(it)
|
||||||
|
true
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validationMessage = "Invalid chapter title format",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var libraries = emptyList<LibraryDto>()
|
private var libraries = emptyList<LibraryDto>()
|
||||||
@ -368,72 +426,72 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
private var publishers = emptySet<String>()
|
private var publishers = emptySet<String>()
|
||||||
private var authors = emptyMap<String, List<AuthorDto>>() // roles to list of authors
|
private var authors = emptyMap<String, List<AuthorDto>>() // roles to list of authors
|
||||||
|
|
||||||
private var fetchFiltersFailed = false
|
private var fetchFilterStatus = FetchFilterStatus.NOT_FETCHED
|
||||||
|
|
||||||
private var fetchFiltersAttempts = 0
|
private var fetchFiltersAttempts = 0
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
private val fetchFiltersLock = ReentrantReadWriteLock()
|
|
||||||
|
|
||||||
private fun fetchFilterOptions() {
|
private fun fetchFilterOptions() {
|
||||||
if (baseUrl.isBlank()) {
|
if (baseUrl.isBlank() || fetchFilterStatus != FetchFilterStatus.NOT_FETCHED || fetchFiltersAttempts >= 3) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Single.fromCallable {
|
fetchFilterStatus = FetchFilterStatus.FETCHING
|
||||||
fetchFiltersLock.read {
|
fetchFiltersAttempts++
|
||||||
if (fetchFiltersAttempts > 3 || (fetchFiltersAttempts > 0 && !fetchFiltersFailed)) {
|
|
||||||
return@fromCallable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchFiltersLock.write {
|
scope.launch {
|
||||||
fetchFiltersFailed = try {
|
try {
|
||||||
libraries = client.newCall(GET("$baseUrl/api/v1/libraries")).execute().parseAs()
|
libraries = client.newCall(GET("$baseUrl/api/v1/libraries")).await().parseAs()
|
||||||
collections = client
|
collections = client
|
||||||
.newCall(GET("$baseUrl/api/v1/collections?unpaged=true"))
|
.newCall(GET("$baseUrl/api/v1/collections?unpaged=true"))
|
||||||
.execute()
|
.await()
|
||||||
.parseAs<PageWrapperDto<CollectionDto>>()
|
.parseAs<PageWrapperDto<CollectionDto>>()
|
||||||
.content
|
.content
|
||||||
genres = client.newCall(GET("$baseUrl/api/v1/genres")).execute().parseAs()
|
genres = client.newCall(GET("$baseUrl/api/v1/genres")).await().parseAs()
|
||||||
tags = client.newCall(GET("$baseUrl/api/v1/tags")).execute().parseAs()
|
tags = client.newCall(GET("$baseUrl/api/v1/tags")).await().parseAs()
|
||||||
publishers = client.newCall(GET("$baseUrl/api/v1/publishers")).execute().parseAs()
|
publishers = client.newCall(GET("$baseUrl/api/v1/publishers")).await().parseAs()
|
||||||
authors = client
|
authors = client
|
||||||
.newCall(GET("$baseUrl/api/v1/authors"))
|
.newCall(GET("$baseUrl/api/v1/authors"))
|
||||||
.execute()
|
.await()
|
||||||
.parseAs<List<AuthorDto>>()
|
.parseAs<List<AuthorDto>>()
|
||||||
.groupBy { it.role }
|
.groupBy { it.role }
|
||||||
false
|
fetchFilterStatus = FetchFilterStatus.FETCHED
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(logTag, "Could not fetch filter options", e)
|
fetchFilterStatus = FetchFilterStatus.NOT_FETCHED
|
||||||
true
|
Log.e(logTag, "Failed to fetch filtering options", e)
|
||||||
}
|
|
||||||
|
|
||||||
fetchFiltersAttempts++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(Schedulers.io())
|
|
||||||
.subscribe()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val logTag = "komga${if (suffix.isNotBlank()) ".$suffix" else ""}"
|
fun Response.isFromReadList() = request.url.toString().contains("/api/v1/readlists")
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T =
|
||||||
|
json.decodeFromString(body.string())
|
||||||
|
|
||||||
|
private val logTag by lazy { "komga${if (suffix.isNotBlank()) ".$suffix" else ""}" }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal const val PREF_EXTRA_SOURCES_COUNT = "Number of extra sources"
|
internal const val PREF_EXTRA_SOURCES_COUNT = "Number of extra sources"
|
||||||
internal const val PREF_EXTRA_SOURCES_DEFAULT = "2"
|
internal const val PREF_EXTRA_SOURCES_DEFAULT = "2"
|
||||||
private val PREF_EXTRA_SOURCES_ENTRIES = (0..10).map { it.toString() }.toTypedArray()
|
|
||||||
|
|
||||||
private const val PREF_DISPLAY_NAME = "Source display name"
|
|
||||||
private const val PREF_ADDRESS = "Address"
|
|
||||||
private const val PREF_USERNAME = "Username"
|
|
||||||
private const val PREF_PASSWORD = "Password"
|
|
||||||
private const val PREF_DEFAULT_LIBRARIES = "Default libraries"
|
|
||||||
private const val PREF_CHAPTER_NAME_TEMPLATE = "Chapter name template"
|
|
||||||
private const val PREF_CHAPTER_NAME_TEMPLATE_DEFAULT = "{number} - {title} ({size})"
|
|
||||||
|
|
||||||
private val supportedImageTypes = listOf("image/jpeg", "image/png", "image/gif", "image/webp", "image/jxl", "image/heif", "image/avif")
|
|
||||||
|
|
||||||
internal const val TYPE_SERIES = "Series"
|
internal const val TYPE_SERIES = "Series"
|
||||||
internal const val TYPE_READLISTS = "Read lists"
|
internal const val TYPE_READLISTS = "Read lists"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum class FetchFilterStatus {
|
||||||
|
NOT_FETCHED,
|
||||||
|
FETCHING,
|
||||||
|
FETCHED,
|
||||||
|
}
|
||||||
|
|
||||||
|
private val PREF_EXTRA_SOURCES_ENTRIES = (0..10).map { it.toString() }.toTypedArray()
|
||||||
|
|
||||||
|
private const val PREF_DISPLAY_NAME = "Source display name"
|
||||||
|
private const val PREF_ADDRESS = "Address"
|
||||||
|
private const val PREF_USERNAME = "Username"
|
||||||
|
private const val PREF_PASSWORD = "Password"
|
||||||
|
private const val PREF_DEFAULT_LIBRARIES = "Default libraries"
|
||||||
|
private const val PREF_CHAPTER_NAME_TEMPLATE = "Chapter name template"
|
||||||
|
private const val PREF_CHAPTER_NAME_TEMPLATE_DEFAULT = "{number} - {title} ({size})"
|
||||||
|
|
||||||
|
private val SUPPORTED_IMAGE_TYPES = listOf("image/jpeg", "image/png", "image/gif", "image/webp", "image/jxl", "image/heif", "image/avif")
|
||||||
|
@ -6,9 +6,17 @@ import eu.kanade.tachiyomi.source.SourceFactory
|
|||||||
class KomgaFactory : SourceFactory {
|
class KomgaFactory : SourceFactory {
|
||||||
override fun createSources(): List<Source> {
|
override fun createSources(): List<Source> {
|
||||||
val firstKomga = Komga("")
|
val firstKomga = Komga("")
|
||||||
val komgaCount = firstKomga.preferences.getString(Komga.PREF_EXTRA_SOURCES_COUNT, Komga.PREF_EXTRA_SOURCES_DEFAULT)!!.toInt()
|
val komgaCount = firstKomga.preferences
|
||||||
|
.getString(Komga.PREF_EXTRA_SOURCES_COUNT, Komga.PREF_EXTRA_SOURCES_DEFAULT)!!
|
||||||
|
.toInt()
|
||||||
|
|
||||||
// Komga(""), Komga("2"), Komga("3"), ...
|
// Komga(""), Komga("2"), Komga("3"), ...
|
||||||
return listOf(firstKomga) + (0 until komgaCount).map { Komga("${it + 2}") }
|
return buildList(komgaCount) {
|
||||||
|
add(firstKomga)
|
||||||
|
|
||||||
|
for (i in 0 until komgaCount) {
|
||||||
|
add(Komga("${i + 2}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,6 +98,8 @@ internal class AuthorGroup(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class CollectionSelect(val collections: List<CollectionFilterEntry>) : Filter.Select<String>("Collection", collections.map { it.name }.toTypedArray())
|
internal class CollectionSelect(
|
||||||
|
val collections: List<CollectionFilterEntry>,
|
||||||
|
) : Filter.Select<String>("Collection", collections.map { it.name }.toTypedArray())
|
||||||
|
|
||||||
internal data class CollectionFilterEntry(val name: String, val id: String? = null)
|
internal data class CollectionFilterEntry(val name: String, val id: String? = null)
|
||||||
|
@ -6,176 +6,89 @@ import android.widget.Button
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.preference.EditTextPreference
|
import androidx.preference.EditTextPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.extension.all.komga.KomgaUtils.toSManga
|
import java.text.ParseException
|
||||||
import eu.kanade.tachiyomi.extension.all.komga.dto.BookDto
|
|
||||||
import eu.kanade.tachiyomi.extension.all.komga.dto.PageWrapperDto
|
|
||||||
import eu.kanade.tachiyomi.extension.all.komga.dto.ReadListDto
|
|
||||||
import eu.kanade.tachiyomi.extension.all.komga.dto.SeriesDto
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.apache.commons.text.StringSubstitutor
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
|
||||||
internal object KomgaUtils {
|
val formatterDate = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||||
private val json: Json by injectLazy()
|
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||||
|
val formatterDateTime = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
|
||||||
|
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||||
|
|
||||||
val formatterDate = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
fun parseDate(date: String): Long = try {
|
||||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
formatterDate.parse(date)!!.time
|
||||||
val formatterDateTime = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
|
} catch (_: ParseException) {
|
||||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
0L
|
||||||
val formatterDateTimeMilli = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.S", Locale.US)
|
}
|
||||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
|
||||||
|
fun parseDateTime(date: String) = try {
|
||||||
fun parseDate(date: String?): Long = runCatching {
|
formatterDateTime.parse(date)!!.time
|
||||||
formatterDate.parse(date!!)!!.time
|
} catch (_: ParseException) {
|
||||||
}.getOrDefault(0L)
|
0L
|
||||||
|
}
|
||||||
fun parseDateTime(date: String?) = if (date == null) {
|
|
||||||
0L
|
fun PreferenceScreen.addEditTextPreference(
|
||||||
} else {
|
title: String,
|
||||||
runCatching {
|
default: String,
|
||||||
formatterDateTime.parse(date)!!.time
|
summary: String,
|
||||||
}
|
dialogMessage: String? = null,
|
||||||
.getOrElse {
|
inputType: Int? = null,
|
||||||
formatterDateTimeMilli.parse(date)?.time ?: 0L
|
validate: ((String) -> Boolean)? = null,
|
||||||
}
|
validationMessage: String? = null,
|
||||||
}
|
key: String = title,
|
||||||
|
restartRequired: Boolean = false,
|
||||||
fun Response.isFromReadList() = request.url.toString().contains("/api/v1/readlists")
|
) {
|
||||||
|
EditTextPreference(context).apply {
|
||||||
fun processSeriesPage(response: Response, baseUrl: String): MangasPage {
|
this.key = key
|
||||||
return if (response.isFromReadList()) {
|
this.title = title
|
||||||
val data = response.parseAs<PageWrapperDto<ReadListDto>>()
|
this.summary = summary
|
||||||
|
this.setDefaultValue(default)
|
||||||
MangasPage(data.content.map { it.toSManga(baseUrl) }, !data.last)
|
dialogTitle = title
|
||||||
} else {
|
this.dialogMessage = dialogMessage
|
||||||
val data = response.parseAs<PageWrapperDto<SeriesDto>>()
|
|
||||||
|
setOnBindEditTextListener { editText ->
|
||||||
MangasPage(data.content.map { it.toSManga(baseUrl) }, !data.last)
|
if (inputType != null) {
|
||||||
}
|
editText.inputType = inputType
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatChapterName(book: BookDto, chapterNameTemplate: String, isFromReadList: Boolean): String {
|
if (validate != null) {
|
||||||
val values = hashMapOf(
|
editText.addTextChangedListener(
|
||||||
"title" to book.metadata.title,
|
object : TextWatcher {
|
||||||
"seriesTitle" to book.seriesTitle,
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
"number" to book.metadata.number,
|
|
||||||
"createdDate" to book.created,
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
"releaseDate" to book.metadata.releaseDate,
|
|
||||||
"size" to book.size,
|
override fun afterTextChanged(editable: Editable?) {
|
||||||
"sizeBytes" to book.sizeBytes.toString(),
|
requireNotNull(editable)
|
||||||
)
|
|
||||||
val sub = StringSubstitutor(values, "{", "}")
|
val text = editable.toString()
|
||||||
|
|
||||||
return buildString {
|
val isValid = text.isBlank() || validate(text)
|
||||||
if (isFromReadList) {
|
|
||||||
append(book.seriesTitle)
|
editText.error = if (!isValid) validationMessage else null
|
||||||
append(" ")
|
editText.rootView.findViewById<Button>(android.R.id.button1)
|
||||||
}
|
?.isEnabled = editText.error == null
|
||||||
|
}
|
||||||
append(sub.replace(chapterNameTemplate))
|
},
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fun SeriesDto.toSManga(baseUrl: String): SManga =
|
|
||||||
SManga.create().apply {
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
title = metadata.title
|
try {
|
||||||
url = "$baseUrl/api/v1/series/$id"
|
val text = newValue as String
|
||||||
thumbnail_url = "$url/thumbnail"
|
val result = text.isBlank() || validate?.invoke(text) ?: true
|
||||||
status = when {
|
|
||||||
metadata.status == "ENDED" && metadata.totalBookCount != null && booksCount < metadata.totalBookCount -> SManga.PUBLISHING_FINISHED
|
if (restartRequired && result) {
|
||||||
metadata.status == "ENDED" -> SManga.COMPLETED
|
Toast.makeText(context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show()
|
||||||
metadata.status == "ONGOING" -> SManga.ONGOING
|
}
|
||||||
metadata.status == "ABANDONED" -> SManga.CANCELLED
|
|
||||||
metadata.status == "HIATUS" -> SManga.ON_HIATUS
|
result
|
||||||
else -> SManga.UNKNOWN
|
} catch (e: Exception) {
|
||||||
}
|
e.printStackTrace()
|
||||||
genre = (metadata.genres + metadata.tags + booksMetadata.tags).distinct().joinToString(", ")
|
false
|
||||||
description = metadata.summary.ifBlank { booksMetadata.summary }
|
}
|
||||||
booksMetadata.authors.groupBy({ it.role }, { it.name }).let { map ->
|
}
|
||||||
author = map["writer"]?.distinct()?.joinToString()
|
}.also(::addPreference)
|
||||||
artist = map["penciller"]?.distinct()?.joinToString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ReadListDto.toSManga(baseUrl: String): SManga =
|
|
||||||
SManga.create().apply {
|
|
||||||
title = name
|
|
||||||
description = summary
|
|
||||||
url = "$baseUrl/api/v1/readlists/$id"
|
|
||||||
thumbnail_url = "$url/thumbnail"
|
|
||||||
status = SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
fun PreferenceScreen.addEditTextPreference(
|
|
||||||
title: String,
|
|
||||||
default: String,
|
|
||||||
summary: String,
|
|
||||||
inputType: Int? = null,
|
|
||||||
validate: ((String) -> Boolean)? = null,
|
|
||||||
validationMessage: String? = null,
|
|
||||||
key: String = title,
|
|
||||||
restartRequired: Boolean = false,
|
|
||||||
) {
|
|
||||||
EditTextPreference(context).apply {
|
|
||||||
this.key = key
|
|
||||||
this.title = title
|
|
||||||
this.summary = summary
|
|
||||||
this.setDefaultValue(default)
|
|
||||||
dialogTitle = title
|
|
||||||
|
|
||||||
setOnBindEditTextListener { editText ->
|
|
||||||
if (inputType != null) {
|
|
||||||
editText.inputType = inputType
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validate != null) {
|
|
||||||
editText.addTextChangedListener(
|
|
||||||
object : TextWatcher {
|
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
|
||||||
|
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
|
||||||
|
|
||||||
override fun afterTextChanged(editable: Editable?) {
|
|
||||||
requireNotNull(editable)
|
|
||||||
|
|
||||||
val text = editable.toString()
|
|
||||||
|
|
||||||
val isValid = text.isBlank() || validate(text)
|
|
||||||
|
|
||||||
editText.error = if (!isValid) validationMessage else null
|
|
||||||
editText.rootView.findViewById<Button>(android.R.id.button1)
|
|
||||||
?.isEnabled = editText.error == null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, _ ->
|
|
||||||
try {
|
|
||||||
val result = text.isBlank() || validate?.invoke(text) ?: true
|
|
||||||
|
|
||||||
if (restartRequired && result) {
|
|
||||||
Toast.makeText(context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.also(::addPreference)
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T> Response.parseAs(): T {
|
|
||||||
return json.decodeFromString(body.string())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.komga.dto
|
package eu.kanade.tachiyomi.extension.all.komga.dto
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.apache.commons.text.StringSubstitutor
|
||||||
|
|
||||||
|
interface ConvertibleToSManga {
|
||||||
|
fun toSManga(baseUrl: String): SManga
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LibraryDto(
|
class LibraryDto(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SeriesDto(
|
class SeriesDto(
|
||||||
val id: String,
|
val id: String,
|
||||||
val libraryId: String,
|
val libraryId: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
@ -19,10 +25,30 @@ data class SeriesDto(
|
|||||||
val booksCount: Int,
|
val booksCount: Int,
|
||||||
val metadata: SeriesMetadataDto,
|
val metadata: SeriesMetadataDto,
|
||||||
val booksMetadata: BookMetadataAggregationDto,
|
val booksMetadata: BookMetadataAggregationDto,
|
||||||
)
|
) : ConvertibleToSManga {
|
||||||
|
override fun toSManga(baseUrl: String) = SManga.create().apply {
|
||||||
|
title = metadata.title
|
||||||
|
url = "$baseUrl/api/v1/series/$id"
|
||||||
|
thumbnail_url = "$url/thumbnail"
|
||||||
|
status = when {
|
||||||
|
metadata.status == "ENDED" && metadata.totalBookCount != null && booksCount < metadata.totalBookCount -> SManga.PUBLISHING_FINISHED
|
||||||
|
metadata.status == "ENDED" -> SManga.COMPLETED
|
||||||
|
metadata.status == "ONGOING" -> SManga.ONGOING
|
||||||
|
metadata.status == "ABANDONED" -> SManga.CANCELLED
|
||||||
|
metadata.status == "HIATUS" -> SManga.ON_HIATUS
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
genre = (metadata.genres + metadata.tags + booksMetadata.tags).distinct().joinToString(", ")
|
||||||
|
description = metadata.summary.ifBlank { booksMetadata.summary }
|
||||||
|
booksMetadata.authors.groupBy({ it.role }, { it.name }).let { map ->
|
||||||
|
author = map["writer"]?.distinct()?.joinToString()
|
||||||
|
artist = map["penciller"]?.distinct()?.joinToString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SeriesMetadataDto(
|
class SeriesMetadataDto(
|
||||||
val status: String,
|
val status: String,
|
||||||
val created: String?,
|
val created: String?,
|
||||||
val lastModified: String?,
|
val lastModified: String?,
|
||||||
@ -46,7 +72,7 @@ data class SeriesMetadataDto(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BookMetadataAggregationDto(
|
class BookMetadataAggregationDto(
|
||||||
val authors: List<AuthorDto> = emptyList(),
|
val authors: List<AuthorDto> = emptyList(),
|
||||||
val tags: Set<String> = emptySet(),
|
val tags: Set<String> = emptySet(),
|
||||||
val releaseDate: String?,
|
val releaseDate: String?,
|
||||||
@ -58,7 +84,7 @@ data class BookMetadataAggregationDto(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BookDto(
|
class BookDto(
|
||||||
val id: String,
|
val id: String,
|
||||||
val seriesId: String,
|
val seriesId: String,
|
||||||
val seriesTitle: String,
|
val seriesTitle: String,
|
||||||
@ -71,10 +97,32 @@ data class BookDto(
|
|||||||
val size: String,
|
val size: String,
|
||||||
val media: MediaDto,
|
val media: MediaDto,
|
||||||
val metadata: BookMetadataDto,
|
val metadata: BookMetadataDto,
|
||||||
)
|
) {
|
||||||
|
fun getChapterName(template: String, isFromReadList: Boolean): String {
|
||||||
|
val values = hashMapOf(
|
||||||
|
"title" to metadata.title,
|
||||||
|
"seriesTitle" to seriesTitle,
|
||||||
|
"number" to metadata.number,
|
||||||
|
"createdDate" to created,
|
||||||
|
"releaseDate" to metadata.releaseDate,
|
||||||
|
"size" to size,
|
||||||
|
"sizeBytes" to sizeBytes.toString(),
|
||||||
|
)
|
||||||
|
val sub = StringSubstitutor(values, "{", "}")
|
||||||
|
|
||||||
|
return buildString {
|
||||||
|
if (isFromReadList) {
|
||||||
|
append(seriesTitle)
|
||||||
|
append(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
append(sub.replace(template))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MediaDto(
|
class MediaDto(
|
||||||
val status: String,
|
val status: String,
|
||||||
val mediaType: String,
|
val mediaType: String,
|
||||||
val pagesCount: Int,
|
val pagesCount: Int,
|
||||||
@ -83,14 +131,14 @@ data class MediaDto(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PageDto(
|
class PageDto(
|
||||||
val number: Int,
|
val number: Int,
|
||||||
val fileName: String,
|
val fileName: String,
|
||||||
val mediaType: String,
|
val mediaType: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BookMetadataDto(
|
class BookMetadataDto(
|
||||||
val title: String,
|
val title: String,
|
||||||
val titleLock: Boolean,
|
val titleLock: Boolean,
|
||||||
val summary: String,
|
val summary: String,
|
||||||
@ -106,13 +154,13 @@ data class BookMetadataDto(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AuthorDto(
|
class AuthorDto(
|
||||||
val name: String,
|
val name: String,
|
||||||
val role: String,
|
val role: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CollectionDto(
|
class CollectionDto(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val ordered: Boolean,
|
val ordered: Boolean,
|
||||||
@ -123,7 +171,7 @@ data class CollectionDto(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ReadListDto(
|
class ReadListDto(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val summary: String,
|
val summary: String,
|
||||||
@ -131,4 +179,12 @@ data class ReadListDto(
|
|||||||
val createdDate: String,
|
val createdDate: String,
|
||||||
val lastModifiedDate: String,
|
val lastModifiedDate: String,
|
||||||
val filtered: Boolean,
|
val filtered: Boolean,
|
||||||
)
|
) : ConvertibleToSManga {
|
||||||
|
override fun toSManga(baseUrl: String) = SManga.create().apply {
|
||||||
|
title = name
|
||||||
|
description = summary
|
||||||
|
url = "$baseUrl/api/v1/readlists/$id"
|
||||||
|
thumbnail_url = "$url/thumbnail"
|
||||||
|
status = SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.extension.all.komga.dto
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PageWrapperDto<T>(
|
class PageWrapperDto<T>(
|
||||||
val content: List<T>,
|
val content: List<T>,
|
||||||
val empty: Boolean,
|
val empty: Boolean,
|
||||||
val first: Boolean,
|
val first: Boolean,
|
||||||
|
Loading…
Reference in New Issue
Block a user