Improve/Refactor overall code + Fixed Filters
This commit is contained in:
@@ -103,8 +103,8 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
override val name = "${KavitaInt.KAVITA_NAME} (${preferences.getString(KavitaConstants.customSourceNamePref, suffix)})"
|
override val name = "${KavitaInt.KAVITA_NAME} (${preferences.getString(KavitaConstants.customSourceNamePref, suffix)})"
|
||||||
override val lang = "all"
|
override val lang = "all"
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
private val apiUrl: String by lazy { getPrefApiUrl().ifEmpty { "" } }
|
private val apiUrl: String by lazy { getPrefApiUrl() }
|
||||||
private val apiKey: String by lazy { getPrefApiKey().ifEmpty { "" } }
|
private val apiKey: String by lazy { getPrefApiKey() }
|
||||||
override val baseUrl by lazy { getPrefBaseUrl() }
|
override val baseUrl by lazy { getPrefBaseUrl() }
|
||||||
private val address by lazy { getPrefAddress() } // Address for the Kavita OPDS url. Should be http(s)://host:(port)/api/opds/api-key
|
private val address by lazy { getPrefAddress() } // Address for the Kavita OPDS url. Should be http(s)://host:(port)/api/opds/api-key
|
||||||
private var jwtToken = "" // * JWT Token for authentication with the server. Stored in memory.
|
private var jwtToken = "" // * JWT Token for authentication with the server. Stored in memory.
|
||||||
@@ -112,7 +112,9 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
private var isLogged = false // Used to know if login was correct and not send login requests anymore
|
private var isLogged = false // Used to know if login was correct and not send login requests anymore
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private var series = emptyList<SeriesDto>() // Acts as a cache
|
// Act as a cache
|
||||||
|
private var series = emptyList<SeriesDto>()
|
||||||
|
private val libraryTypeCache = mutableMapOf<Int, LibraryTypeEnum>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension function to parse a network [Response] as a given type [T].
|
* Extension function to parse a network [Response] as a given type [T].
|
||||||
@@ -276,103 +278,6 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readingListRequest(): Request {
|
|
||||||
return POST(
|
|
||||||
"$apiUrl/ReadingList/lists?includePromoted=true&sortByLastModified=true",
|
|
||||||
headersBuilder().build(),
|
|
||||||
"{}".toRequestBody(JSON_MEDIA_TYPE),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun readingListParse(response: Response): MangasPage {
|
|
||||||
return try {
|
|
||||||
val readingLists = response.parseAs<List<ReadingListDto>>()
|
|
||||||
cachedReadingLists = readingLists
|
|
||||||
val now = java.util.Calendar.getInstance()
|
|
||||||
val mangaList = readingLists.map { list ->
|
|
||||||
val hasDates = list.startingYear > 0 && list.endingYear > 0
|
|
||||||
val status = if (hasDates) {
|
|
||||||
val endCal = java.util.Calendar.getInstance().apply {
|
|
||||||
set(list.endingYear, list.endingMonth - 1, 1)
|
|
||||||
}
|
|
||||||
if (now.after(endCal)) SManga.PUBLISHING_FINISHED else SManga.ONGOING
|
|
||||||
} else {
|
|
||||||
SManga.COMPLETED
|
|
||||||
}
|
|
||||||
SManga.create().apply {
|
|
||||||
title = (if (list.promoted) "🔺 " else "") + list.title
|
|
||||||
artist = "${list.itemCount} items"
|
|
||||||
author = list.ownerUserName
|
|
||||||
thumbnail_url = if (!list.coverImage.isNullOrBlank()) {
|
|
||||||
"$apiUrl/image/${list.coverImage}?apiKey=$apiKey"
|
|
||||||
} else {
|
|
||||||
"$apiUrl/Image/readinglist-cover?readingListId=${list.id}&apiKey=$apiKey"
|
|
||||||
}
|
|
||||||
description = list.summary ?: "Reading List"
|
|
||||||
url = "$baseUrl/ReadingList/items?readingListId=${list.id}&source=readinglist"
|
|
||||||
initialized = true
|
|
||||||
this.status = status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MangasPage(mangaList, false)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(LOG_TAG, "Error parsing reading lists", e)
|
|
||||||
MangasPage(emptyList(), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchReadingListItems(manga: SManga): Observable<List<SChapter>> {
|
|
||||||
val readingListId = manga.url.substringAfter("readingListId=").substringBefore("&")
|
|
||||||
val request = GET("$apiUrl/ReadingList/items?readingListId=$readingListId", headersBuilder().build())
|
|
||||||
|
|
||||||
return client.newCall(request)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { parseReadingListItems(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseReadingListItems(response: Response): List<SChapter> {
|
|
||||||
val items = response.parseAs<List<ReadingListItemDto>>()
|
|
||||||
val readingListId = response.request.url.queryParameter("readingListId")
|
|
||||||
|
|
||||||
return items.map { item ->
|
|
||||||
SChapter.create().apply {
|
|
||||||
url = when {
|
|
||||||
item.chapterId != null && item.chapterId > 0 ->
|
|
||||||
"/Chapter/${item.chapterId}?readingListId=$readingListId&seriesId=${item.seriesId}&chapterId=${item.chapterId}"
|
|
||||||
item.volumeId != null && item.volumeId > 0 ->
|
|
||||||
"/Volume/${item.volumeId}?readingListId=$readingListId&seriesId=${item.seriesId}&volumeId=${item.volumeId}"
|
|
||||||
else ->
|
|
||||||
"/Series/${item.seriesId}?readingListId=$readingListId&seriesId=${item.seriesId}&order=${item.order}"
|
|
||||||
}
|
|
||||||
name = buildString {
|
|
||||||
append("${item.order + 1}. ")
|
|
||||||
when {
|
|
||||||
item.volumeNumber != null && item.volumeNumber != KavitaConstants.UNNUMBERED_VOLUME_STR -> {
|
|
||||||
append("Volume ${item.volumeNumber}")
|
|
||||||
}
|
|
||||||
item.chapterNumber != null && item.chapterNumber != KavitaConstants.UNNUMBERED_VOLUME_STR -> {
|
|
||||||
val libraryType = getLibraryType(item.seriesId)
|
|
||||||
when (libraryType) {
|
|
||||||
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> append("Issue #${item.chapterNumber}")
|
|
||||||
else -> append("Chapter ${item.chapterNumber}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
append("Item ${item.order + 1}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
date_upload = if (preferences.RdDate && !item.releaseDate.isNullOrBlank()) {
|
|
||||||
parseDateSafe(item.releaseDate)
|
|
||||||
} else {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
chapter_number = item.order.toFloat()
|
|
||||||
scanlator = item.seriesName
|
|
||||||
}
|
|
||||||
}.sortedBy { it.chapter_number }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
try {
|
try {
|
||||||
val result = response.parseAs<List<SeriesDto>>()
|
val result = response.parseAs<List<SeriesDto>>()
|
||||||
@@ -401,8 +306,17 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
sortOptions = SortOptions(SortFieldEnum.AverageRating.type, false),
|
sortOptions = SortOptions(SortFieldEnum.AverageRating.type, false),
|
||||||
statements = mutableListOf(),
|
statements = mutableListOf(),
|
||||||
)
|
)
|
||||||
val payload = json.encodeToJsonElement(filter).toString()
|
|
||||||
|
|
||||||
|
// Always exclude EPUBs unless explicitly included
|
||||||
|
if (!preferences.getBoolean(SHOW_EPUB_PREF, SHOW_EPUB_DEFAULT)) {
|
||||||
|
filter.addStatement(
|
||||||
|
FilterComparison.NotContains,
|
||||||
|
FilterField.Formats,
|
||||||
|
"3",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val payload = json.encodeToJsonElement(filter).toString()
|
||||||
return POST(
|
return POST(
|
||||||
"$apiUrl/Series/all-v2?pageNumber=$page&pageSize=20",
|
"$apiUrl/Series/all-v2?pageNumber=$page&pageSize=20",
|
||||||
headersBuilder().build(),
|
headersBuilder().build(),
|
||||||
@@ -413,8 +327,18 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
val filter = FilterV2Dto(
|
val filter = FilterV2Dto(
|
||||||
sortOptions = SortOptions(SortFieldEnum.LastChapterAdded.type, false),
|
sortOptions = SortOptions(SortFieldEnum.LastChapterAdded.type, false),
|
||||||
statements = mutableListOf(), // optionally, add statements to filter out EPUB, etc.
|
statements = mutableListOf(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Always exclude EPUBs unless explicitly included
|
||||||
|
if (!preferences.getBoolean(SHOW_EPUB_PREF, SHOW_EPUB_DEFAULT)) {
|
||||||
|
filter.addStatement(
|
||||||
|
FilterComparison.NotContains,
|
||||||
|
FilterField.Formats,
|
||||||
|
"3",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val payload = json.encodeToJsonElement(filter).toString()
|
val payload = json.encodeToJsonElement(filter).toString()
|
||||||
return prepareRequest(page, payload)
|
return prepareRequest(page, payload)
|
||||||
}
|
}
|
||||||
@@ -433,7 +357,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
// If a SmartFilter selected, apply its filter and return that
|
// If a SmartFilter selected, apply its filter and return that
|
||||||
if (smartFilterFilter?.state != 0 && smartFilterFilter != null) {
|
if (smartFilterFilter?.state != 0 && smartFilterFilter != null) {
|
||||||
val index = try {
|
val index = try {
|
||||||
smartFilterFilter?.state as Int - 1
|
smartFilterFilter.state as Int - 1
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(LOG_TAG, e.toString(), e)
|
Log.e(LOG_TAG, e.toString(), e)
|
||||||
0
|
0
|
||||||
@@ -604,6 +528,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
"Image" -> 0
|
"Image" -> 0
|
||||||
"Archive" -> 1
|
"Archive" -> 1
|
||||||
"Pdf" -> 4
|
"Pdf" -> 4
|
||||||
|
"Epub" -> 3
|
||||||
"Unknown" -> 2
|
"Unknown" -> 2
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@@ -648,12 +573,12 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
val included = filter.state
|
val included = filter.state
|
||||||
.filter { it.state == STATE_INCLUDE }
|
.filter { it.state == STATE_INCLUDE }
|
||||||
.mapNotNull { languageFilter ->
|
.mapNotNull { languageFilter ->
|
||||||
languagesListMeta?.find { it.title == languageFilter.name }?.isoCode
|
languagesListMeta.find { it.title == languageFilter.name }?.isoCode
|
||||||
}
|
}
|
||||||
val excluded = filter.state
|
val excluded = filter.state
|
||||||
.filter { it.state == STATE_EXCLUDE }
|
.filter { it.state == STATE_EXCLUDE }
|
||||||
.mapNotNull { languageFilter ->
|
.mapNotNull { languageFilter ->
|
||||||
languagesListMeta?.find { it.title == languageFilter.name }?.isoCode
|
languagesListMeta.find { it.title == languageFilter.name }?.isoCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if (included.isNotEmpty()) {
|
if (included.isNotEmpty()) {
|
||||||
@@ -693,11 +618,21 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
is PubStatusFilterGroup -> {
|
is PubStatusFilterGroup -> {
|
||||||
filter.state.forEach { pubStatusFilter ->
|
filter.state.forEach { pubStatusFilter ->
|
||||||
if ((pubStatusFilter as Filter.CheckBox).state) {
|
if ((pubStatusFilter as Filter.CheckBox).state) {
|
||||||
filterV2.addStatement(
|
val statusValue = when (pubStatusFilter.name) {
|
||||||
FilterComparison.Equal,
|
"Ongoing" -> 0
|
||||||
FilterField.PublicationStatus,
|
"Hiatus" -> 1
|
||||||
pubStatusFilter.name,
|
"Completed" -> 2
|
||||||
)
|
"Cancelled" -> 3
|
||||||
|
"Ended" -> 4
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
statusValue?.let {
|
||||||
|
filterV2.addStatement(
|
||||||
|
FilterComparison.Equal,
|
||||||
|
FilterField.PublicationStatus,
|
||||||
|
it.toString(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -964,8 +899,8 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
),
|
),
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
val rawResponse = responsePlus.body?.string()
|
val rawResponse = responsePlus.body.string()
|
||||||
if (rawResponse.isNullOrBlank() || rawResponse.trim().startsWith("<")) {
|
if (rawResponse.isBlank() || rawResponse.trim().startsWith("<")) {
|
||||||
Log.e(LOG_TAG, "Invalid response for series-detail-plus: $rawResponse")
|
Log.e(LOG_TAG, "Invalid response for series-detail-plus: $rawResponse")
|
||||||
// Optionally, show a default or skip averageScore
|
// Optionally, show a default or skip averageScore
|
||||||
""
|
""
|
||||||
@@ -1037,7 +972,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
|
|
||||||
for (volume in volumes) {
|
for (volume in volumes) {
|
||||||
// Issue: use chapter covers
|
// Issue: use chapter covers
|
||||||
if (isComicLibrary && volume.number == KavitaConstants.UNNUMBERED_VOLUME) {
|
if (isComicLibrary && volume.minNumber == KavitaConstants.UNNUMBERED_VOLUME) {
|
||||||
coverCandidates += volume.chapters
|
coverCandidates += volume.chapters
|
||||||
.filterNot { it.coverImage.isNullOrBlank() }
|
.filterNot { it.coverImage.isNullOrBlank() }
|
||||||
.map { chapter ->
|
.map { chapter ->
|
||||||
@@ -1054,7 +989,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
}
|
}
|
||||||
if (hasSingleFile && !volume.coverImage.isNullOrBlank()) {
|
if (hasSingleFile && !volume.coverImage.isNullOrBlank()) {
|
||||||
val isUnread = volume.pagesRead < volume.pages
|
val isUnread = volume.pagesRead < volume.pages
|
||||||
val volumeNumber = volume.number.toFloat()
|
val volumeNumber = volume.minNumber.toFloat()
|
||||||
coverCandidates.add(
|
coverCandidates.add(
|
||||||
Triple(
|
Triple(
|
||||||
"$apiUrl/Image/volume-cover?volumeId=${volume.id}&apiKey=$apiKey",
|
"$apiUrl/Image/volume-cover?volumeId=${volume.id}&apiKey=$apiKey",
|
||||||
@@ -1144,10 +1079,167 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
1 -> SManga.ON_HIATUS
|
1 -> SManga.ON_HIATUS
|
||||||
else -> SManga.UNKNOWN
|
else -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
Log.d(LOG_TAG, "Publication status received: ${result.publicationStatus}")
|
// Log.d(LOG_TAG, "Publication status received: ${result.publicationStatus}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
** Reading Lists
|
||||||
|
**/
|
||||||
|
private fun readingListRequest(): Request {
|
||||||
|
return POST(
|
||||||
|
"$apiUrl/ReadingList/lists?includePromoted=true&sortByLastModified=true",
|
||||||
|
headersBuilder().build(),
|
||||||
|
"{}".toRequestBody(JSON_MEDIA_TYPE),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readingListParse(response: Response): MangasPage {
|
||||||
|
return try {
|
||||||
|
val readingLists = response.parseAs<List<ReadingListDto>>()
|
||||||
|
cachedReadingLists = readingLists
|
||||||
|
val now = java.util.Calendar.getInstance()
|
||||||
|
val mangaList = readingLists.map { list ->
|
||||||
|
val hasDates = list.startingYear > 0 && list.endingYear > 0
|
||||||
|
val status = if (hasDates) {
|
||||||
|
val endCal = java.util.Calendar.getInstance().apply {
|
||||||
|
set(list.endingYear, list.endingMonth - 1, 1)
|
||||||
|
}
|
||||||
|
if (now.after(endCal)) SManga.PUBLISHING_FINISHED else SManga.ONGOING
|
||||||
|
} else {
|
||||||
|
SManga.COMPLETED
|
||||||
|
}
|
||||||
|
SManga.create().apply {
|
||||||
|
title = (if (list.promoted) "🔺 " else "") + list.title
|
||||||
|
artist = "${list.itemCount} items"
|
||||||
|
author = list.ownerUserName
|
||||||
|
thumbnail_url = if (!list.coverImage.isNullOrBlank()) {
|
||||||
|
"$apiUrl/image/${list.coverImage}?apiKey=$apiKey"
|
||||||
|
} else {
|
||||||
|
"$apiUrl/Image/readinglist-cover?readingListId=${list.id}&apiKey=$apiKey"
|
||||||
|
}
|
||||||
|
description = list.summary ?: "Reading List"
|
||||||
|
url = "$baseUrl/ReadingList/items?readingListId=${list.id}&source=readinglist"
|
||||||
|
initialized = true
|
||||||
|
this.status = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MangasPage(mangaList, false)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(LOG_TAG, "Error parsing reading lists", e)
|
||||||
|
MangasPage(emptyList(), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchReadingListItems(manga: SManga): Observable<List<SChapter>> {
|
||||||
|
val readingListId = manga.url.substringAfter("readingListId=").substringBefore("&")
|
||||||
|
val request = GET("$apiUrl/ReadingList/items?readingListId=$readingListId", headersBuilder().build())
|
||||||
|
|
||||||
|
return client.newCall(request)
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { parseReadingListItems(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseReadingListItems(response: Response): List<SChapter> {
|
||||||
|
val items = response.parseAs<List<ReadingListItemDto>>()
|
||||||
|
val readingListId = response.request.url.queryParameter("readingListId")
|
||||||
|
|
||||||
|
return items.map { item ->
|
||||||
|
SChapter.create().apply {
|
||||||
|
url = when {
|
||||||
|
item.chapterId != null && item.chapterId > 0 ->
|
||||||
|
"/Chapter/${item.chapterId}?readingListId=$readingListId&seriesId=${item.seriesId}&chapterId=${item.chapterId}"
|
||||||
|
item.volumeId != null && item.volumeId > 0 ->
|
||||||
|
"/Volume/${item.volumeId}?readingListId=$readingListId&seriesId=${item.seriesId}&volumeId=${item.volumeId}"
|
||||||
|
else ->
|
||||||
|
"/Series/${item.seriesId}?readingListId=$readingListId&seriesId=${item.seriesId}&order=${item.order}"
|
||||||
|
}
|
||||||
|
|
||||||
|
val type = when {
|
||||||
|
item.volumeId != null && item.volumeNumber != null -> {
|
||||||
|
when {
|
||||||
|
item.volumeNumber == KavitaConstants.UNNUMBERED_VOLUME_STR -> {
|
||||||
|
val libraryType = getLibraryType(item.seriesId)
|
||||||
|
when (libraryType) {
|
||||||
|
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> ChapterType.Issue
|
||||||
|
else -> ChapterType.Chapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item.chapterNumber == KavitaConstants.UNNUMBERED_VOLUME_STR -> ChapterType.SingleFileVolume
|
||||||
|
else -> ChapterType.Regular
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item.chapterId != null && item.chapterNumber != null -> {
|
||||||
|
val libraryType = getLibraryType(item.seriesId)
|
||||||
|
when (libraryType) {
|
||||||
|
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> ChapterType.Issue
|
||||||
|
else -> ChapterType.Chapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> ChapterType.Special
|
||||||
|
}
|
||||||
|
|
||||||
|
name = buildString {
|
||||||
|
append("${item.order + 1}. ")
|
||||||
|
when (type) {
|
||||||
|
ChapterType.Regular -> {
|
||||||
|
val chapterNum = item.chapterNumber?.toIntOrNull()?.let {
|
||||||
|
it.toString().padStart(2, '0')
|
||||||
|
} ?: item.chapterNumber ?: ""
|
||||||
|
when {
|
||||||
|
item.chapterTitleName?.isNotBlank() == true ->
|
||||||
|
append("$chapterNum - ${item.chapterTitleName}")
|
||||||
|
else ->
|
||||||
|
append("Vol. ${item.volumeNumber} Ch. $chapterNum")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChapterType.SingleFileVolume -> {
|
||||||
|
when {
|
||||||
|
item.chapterTitleName?.isNotBlank() == true ->
|
||||||
|
append("v${item.volumeNumber} - ${item.chapterTitleName}")
|
||||||
|
else ->
|
||||||
|
append("Volume ${item.volumeNumber}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChapterType.Issue -> {
|
||||||
|
val issueNum = item.chapterNumber?.toIntOrNull()?.let {
|
||||||
|
it.toString().padStart(3, '0')
|
||||||
|
} ?: item.chapterNumber ?: ""
|
||||||
|
when {
|
||||||
|
item.chapterTitleName?.isNotBlank() == true ->
|
||||||
|
append("${item.chapterTitleName} (#$issueNum)")
|
||||||
|
else ->
|
||||||
|
append("Issue #$issueNum")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChapterType.Chapter -> {
|
||||||
|
val chapterNum = item.chapterNumber?.toIntOrNull()?.let {
|
||||||
|
it.toString().padStart(2, '0')
|
||||||
|
} ?: item.chapterNumber ?: ""
|
||||||
|
when {
|
||||||
|
item.chapterTitleName?.isNotBlank() == true ->
|
||||||
|
append("$chapterNum - ${item.chapterTitleName}")
|
||||||
|
else ->
|
||||||
|
append("Chapter $chapterNum")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChapterType.Special -> {
|
||||||
|
append(item.chapterTitleName ?: "Item ${item.order + 1}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
date_upload = if (preferences.RdDate && !item.releaseDate.isNullOrBlank()) {
|
||||||
|
parseDateSafe(item.releaseDate)
|
||||||
|
} else {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
chapter_number = item.order.toFloat()
|
||||||
|
scanlator = item.seriesName
|
||||||
|
}
|
||||||
|
}.sortedBy { it.chapter_number }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Related Titles / Suggestions
|
* Related Titles / Suggestions
|
||||||
* This only works on Komikku
|
* This only works on Komikku
|
||||||
@@ -1181,8 +1273,8 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val rawResponse = response.body?.string()
|
val rawResponse = response.body.string()
|
||||||
if (rawResponse.isNullOrEmpty() || rawResponse.startsWith("<")) {
|
if (rawResponse.isEmpty() || rawResponse.startsWith("<")) {
|
||||||
Log.e(LOG_TAG, "Invalid response for related manga: $rawResponse")
|
Log.e(LOG_TAG, "Invalid response for related manga: $rawResponse")
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
@@ -1237,24 +1329,27 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getLibraryType(seriesId: Int): LibraryTypeEnum? {
|
private fun getLibraryType(seriesId: Int): LibraryTypeEnum? {
|
||||||
return try {
|
return libraryTypeCache[seriesId] ?: run {
|
||||||
val seriesDto = client.newCall(GET("$apiUrl/Series/$seriesId", headersBuilder().build()))
|
try {
|
||||||
.execute()
|
val seriesDto = client.newCall(GET("$apiUrl/Series/$seriesId", headersBuilder().build()))
|
||||||
.parseAs<SeriesDto>()
|
|
||||||
|
|
||||||
// First try to get library type from library metadata
|
|
||||||
libraryListMeta.find { it.id == seriesDto.libraryId }?.type?.let { typeInt ->
|
|
||||||
LibraryTypeEnum.fromInt(typeInt)
|
|
||||||
} ?: run {
|
|
||||||
// Fallback: fetch library directly
|
|
||||||
val library = client.newCall(GET("$apiUrl/Library/${seriesDto.libraryId}", headersBuilder().build()))
|
|
||||||
.execute()
|
.execute()
|
||||||
.parseAs<LibraryDto>()
|
.parseAs<SeriesDto>()
|
||||||
LibraryTypeEnum.fromInt(library.type)
|
|
||||||
|
val type = libraryListMeta.find { it.id == seriesDto.libraryId }?.type?.let { typeInt ->
|
||||||
|
LibraryTypeEnum.fromInt(typeInt)
|
||||||
|
} ?: run {
|
||||||
|
val library = client.newCall(GET("$apiUrl/Library/${seriesDto.libraryId}", headersBuilder().build()))
|
||||||
|
.execute()
|
||||||
|
.parseAs<LibraryDto>()
|
||||||
|
LibraryTypeEnum.fromInt(library.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
type?.let { libraryTypeCache[seriesId] = it }
|
||||||
|
type
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(LOG_TAG, "Error getting library type for series $seriesId", e)
|
||||||
|
null
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(LOG_TAG, "Error getting library type for series $seriesId", e)
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1267,36 +1362,53 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
val volumeItems = mutableListOf<SChapter>()
|
val volumeItems = mutableListOf<SChapter>()
|
||||||
|
|
||||||
volumes.forEach { volume ->
|
volumes.forEach { volume ->
|
||||||
volume.chapters.forEach { chapter ->
|
// This fixes volumes that are recognized as chapters in Kavita because they have numbers in their titles
|
||||||
val sChapter = helper.chapterFromVolume(chapter, volume, libraryType = libraryType)
|
if (volume.chapters.size == 1 && volume.name.isNotBlank()) {
|
||||||
when (ChapterType.of(chapter, volume, libraryType)) {
|
val chapter = volume.chapters.first()
|
||||||
ChapterType.SingleFileVolume -> {
|
val sChapter = helper.chapterFromVolume(
|
||||||
// For Case 2, use positive volume numbers
|
chapter,
|
||||||
// For Case 3, use negative numbers
|
volume,
|
||||||
sChapter.chapter_number = volume.number.toFloat()
|
singleFileVolumeNumber = volume.minNumber,
|
||||||
|
libraryType = libraryType,
|
||||||
|
)
|
||||||
|
// Force SingleFileVolume type
|
||||||
|
sChapter.name = when {
|
||||||
|
volume.name.any { it.isLetter() } -> "v${volume.minNumber} - ${volume.name}"
|
||||||
|
else -> "Volume ${volume.minNumber}"
|
||||||
|
}
|
||||||
|
sChapter.chapter_number = volume.minNumber.toFloat()
|
||||||
|
sChapter.scanlator = "Volume"
|
||||||
|
volumeItems.add(sChapter)
|
||||||
|
} else {
|
||||||
|
volume.chapters.forEach { chapter ->
|
||||||
|
val type = ChapterType.of(chapter, volume, libraryType)
|
||||||
|
val sChapter = helper.chapterFromVolume(chapter, volume, libraryType = libraryType)
|
||||||
|
if (type == ChapterType.SingleFileVolume) {
|
||||||
volumeItems.add(sChapter)
|
volumeItems.add(sChapter)
|
||||||
|
} else {
|
||||||
|
chapters.add(sChapter)
|
||||||
}
|
}
|
||||||
else -> chapters.add(sChapter)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
// Case 1: Only chapters
|
// Case 1: Only chapters exist
|
||||||
chapters.isNotEmpty() && volumeItems.isEmpty() ->
|
chapters.isNotEmpty() && volumeItems.isEmpty() ->
|
||||||
chapters.sortedByDescending { it.chapter_number }
|
chapters.sortedByDescending { it.chapter_number }
|
||||||
|
|
||||||
// Case 2: Only volumes - treat as chapters with positive numbers
|
// Case 2: Only volumes exist
|
||||||
volumeItems.isNotEmpty() && chapters.isEmpty() ->
|
volumeItems.isNotEmpty() && chapters.isEmpty() ->
|
||||||
volumeItems.sortedByDescending { it.chapter_number }
|
volumeItems.sortedByDescending { it.chapter_number }
|
||||||
|
|
||||||
// Case 3: Mixed content - chapters first, then volumes (as negative numbers)
|
// Case 3: Mixed content
|
||||||
else -> {
|
else -> {
|
||||||
// Convert volume numbers to negative for proper sorting
|
// Convert volume numbers to negative for proper sorting
|
||||||
volumeItems.forEach { it.chapter_number = -it.chapter_number }
|
volumeItems.forEach { it.chapter_number = -it.chapter_number }
|
||||||
val sortedChapters = chapters.sortedByDescending { it.chapter_number }
|
(
|
||||||
val sortedVolumes = volumeItems.sortedBy { it.chapter_number }
|
chapters.sortedByDescending { it.chapter_number } +
|
||||||
sortedChapters + sortedVolumes
|
volumeItems.sortedBy { it.chapter_number }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -1471,21 +1583,9 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
private var collectionsListMeta = emptyList<MetadataCollections>()
|
private var collectionsListMeta = emptyList<MetadataCollections>()
|
||||||
private var smartFilters = emptyList<SmartFilter>()
|
private var smartFilters = emptyList<SmartFilter>()
|
||||||
private var cachedReadingLists: List<ReadingListDto> = emptyList()
|
private var cachedReadingLists: List<ReadingListDto> = emptyList()
|
||||||
private val personRoles = listOf(
|
|
||||||
"Writer",
|
|
||||||
"Penciller",
|
|
||||||
"Inker",
|
|
||||||
"Colorist",
|
|
||||||
"Letterer",
|
|
||||||
"CoverArtist",
|
|
||||||
"Editor",
|
|
||||||
"Publisher",
|
|
||||||
"Character",
|
|
||||||
"Translator",
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the enabled filters if they are not empty so tachiyomi can show them to the user
|
* Loads the enabled filters if they are not empty so Mihon can show them to the user
|
||||||
*/
|
*/
|
||||||
override fun getFilterList(): FilterList {
|
override fun getFilterList(): FilterList {
|
||||||
val toggledFilters = getToggledFilters()
|
val toggledFilters = getToggledFilters()
|
||||||
@@ -1542,6 +1642,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
"Image",
|
"Image",
|
||||||
"Archive",
|
"Archive",
|
||||||
"Pdf",
|
"Pdf",
|
||||||
|
"Epub",
|
||||||
"Unknown",
|
"Unknown",
|
||||||
).map { FormatFilter(it) },
|
).map { FormatFilter(it) },
|
||||||
),
|
),
|
||||||
@@ -1564,10 +1665,18 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
}
|
}
|
||||||
if (pubStatusListMeta.isNotEmpty() and toggledFilters.contains("Publication Status")) {
|
if (pubStatusListMeta.isNotEmpty() and toggledFilters.contains("Publication Status")) {
|
||||||
filtersLoaded.add(
|
filtersLoaded.add(
|
||||||
PubStatusFilterGroup(pubStatusListMeta.map { PubStatusFilter(it.title) }),
|
PubStatusFilterGroup(
|
||||||
|
listOf(
|
||||||
|
PubStatusFilter("Ongoing"),
|
||||||
|
PubStatusFilter("Hiatus"),
|
||||||
|
PubStatusFilter("Completed"),
|
||||||
|
PubStatusFilter("Cancelled"),
|
||||||
|
PubStatusFilter("Ended"),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (pubStatusListMeta.isNotEmpty() and toggledFilters.contains("Rating")) {
|
if (ageRatingsListMeta.isNotEmpty() and toggledFilters.contains("Rating")) {
|
||||||
filtersLoaded.add(
|
filtersLoaded.add(
|
||||||
UserRating(),
|
UserRating(),
|
||||||
)
|
)
|
||||||
@@ -1795,14 +1904,11 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPrefBaseUrl(): String = preferences.getString("BASEURL", "")!!
|
private fun getPrefBaseUrl(): String = preferences.getString("BASEURL", "") ?: ""
|
||||||
private fun getPrefApiUrl(): String = preferences.getString("APIURL", "")!!
|
private fun getPrefApiUrl(): String = preferences.getString("APIURL", "") ?: ""
|
||||||
private fun getPrefKey(): String = preferences.getString("APIKEY", "")!!
|
private fun getPrefKey(): String = preferences.getString("APIKEY", "") ?: ""
|
||||||
private fun getToggledFilters() = preferences.getStringSet(KavitaConstants.toggledFiltersPref, KavitaConstants.defaultFilterPrefEntries)!!
|
private fun getToggledFilters() = preferences.getStringSet(KavitaConstants.toggledFiltersPref, KavitaConstants.defaultFilterPrefEntries)!!
|
||||||
|
|
||||||
private val SharedPreferences.score: Boolean
|
|
||||||
get() = getBoolean(SCORE_PREF, SCORE_DEFAULT)
|
|
||||||
|
|
||||||
private val SharedPreferences.groupTags: Boolean
|
private val SharedPreferences.groupTags: Boolean
|
||||||
get() = getBoolean(GROUP_TAGS_PREF, GROUP_TAGS_DEFAULT)
|
get() = getBoolean(GROUP_TAGS_PREF, GROUP_TAGS_DEFAULT)
|
||||||
|
|
||||||
@@ -1829,7 +1935,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
// Clear all preferences
|
// Clear all preferences
|
||||||
preferences.edit().clear().commit()
|
preferences.edit().clear().commit()
|
||||||
|
|
||||||
// Reset all in-memory state
|
// Reset all in-memory state (for debug)
|
||||||
// jwtToken = ""
|
// jwtToken = ""
|
||||||
// isLogged = false
|
// isLogged = false
|
||||||
// genresListMeta = emptyList()
|
// genresListMeta = emptyList()
|
||||||
@@ -1852,16 +1958,18 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
|
|
||||||
private fun getPrefApiKey(): String {
|
private fun getPrefApiKey(): String {
|
||||||
// http(s)://host:(port)/api/opds/api-key
|
// http(s)://host:(port)/api/opds/api-key
|
||||||
val existingKey = preferences.getString("APIKEY", "")
|
val existingKey = preferences.getString(APIKEY, "")
|
||||||
return existingKey!!.ifEmpty { preferences.getString(ADDRESS_TITLE, "")!!.split("/opds/")[1] }
|
if (!existingKey.isNullOrEmpty()) return existingKey
|
||||||
|
val address = preferences.getString(ADDRESS_TITLE, "") ?: ""
|
||||||
|
val parts = address.split("/opds/")
|
||||||
|
return if (parts.size > 1) parts[1] else ""
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ADDRESS_TITLE = "Address"
|
private const val ADDRESS_TITLE = "Address"
|
||||||
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
|
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||||
|
|
||||||
private const val SCORE_PREF = "Score"
|
private const val APIKEY = "APIKEY"
|
||||||
const val SCORE_DEFAULT = true
|
|
||||||
|
|
||||||
private const val LAST_VOLUME_COVER_PREF = "LastVolumeCover"
|
private const val LAST_VOLUME_COVER_PREF = "LastVolumeCover"
|
||||||
const val LAST_VOLUME_COVER_DEFAULT = false
|
const val LAST_VOLUME_COVER_DEFAULT = false
|
||||||
@@ -1876,9 +1984,6 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
const val RD_DATE_DEFAULT = false
|
const val RD_DATE_DEFAULT = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* LOGIN
|
|
||||||
**/
|
|
||||||
/**
|
/**
|
||||||
* Used to check if a url is configured already in any of the sources
|
* Used to check if a url is configured already in any of the sources
|
||||||
* This is a limitation needed for tracking.
|
* This is a limitation needed for tracking.
|
||||||
@@ -1909,6 +2014,9 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LOGIN
|
||||||
|
* **/
|
||||||
private fun setupLogin(addressFromPreference: String = "") {
|
private fun setupLogin(addressFromPreference: String = "") {
|
||||||
Log.v(LOG_TAG, "[Setup Login] Starting setup")
|
Log.v(LOG_TAG, "[Setup Login] Starting setup")
|
||||||
val validAddress = address.ifEmpty { addressFromPreference }
|
val validAddress = address.ifEmpty { addressFromPreference }
|
||||||
@@ -1969,7 +2077,6 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
init {
|
init {
|
||||||
if (apiUrl.isNotBlank()) {
|
if (apiUrl.isNotBlank()) {
|
||||||
Single.fromCallable {
|
Single.fromCallable {
|
||||||
// Login
|
|
||||||
doLogin()
|
doLogin()
|
||||||
try { // Get current version
|
try { // Get current version
|
||||||
val requestUrl = "$apiUrl/Server/server-info-slim"
|
val requestUrl = "$apiUrl/Server/server-info-slim"
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ class KavitaHelper {
|
|||||||
val json = Json {
|
val json = Json {
|
||||||
isLenient = true
|
isLenient = true
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
|
coerceInputValues = true
|
||||||
|
explicitNulls = false
|
||||||
allowSpecialFloatingPointValues = true
|
allowSpecialFloatingPointValues = true
|
||||||
useArrayPolymorphism = true
|
useArrayPolymorphism = true
|
||||||
prettyPrint = true
|
prettyPrint = true
|
||||||
@@ -37,9 +39,9 @@ class KavitaHelper {
|
|||||||
var hasNextPage = false
|
var hasNextPage = false
|
||||||
if (!paginationHeader.isNullOrEmpty()) {
|
if (!paginationHeader.isNullOrEmpty()) {
|
||||||
val paginationInfo = json.decodeFromString<PaginationInfo>(paginationHeader)
|
val paginationInfo = json.decodeFromString<PaginationInfo>(paginationHeader)
|
||||||
hasNextPage = paginationInfo.currentPage + 1 > paginationInfo.totalPages
|
hasNextPage = paginationInfo.currentPage + 1 < paginationInfo.totalPages
|
||||||
}
|
}
|
||||||
return !hasNextPage
|
return hasNextPage
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getIdFromUrl(url: String): Int {
|
fun getIdFromUrl(url: String): Int {
|
||||||
@@ -73,6 +75,9 @@ class KavitaHelper {
|
|||||||
fun chapterFromVolume(chapter: ChapterDto, volume: VolumeDto, singleFileVolumeNumber: Int? = null, libraryType: LibraryTypeEnum? = null): SChapter =
|
fun chapterFromVolume(chapter: ChapterDto, volume: VolumeDto, singleFileVolumeNumber: Int? = null, libraryType: LibraryTypeEnum? = null): SChapter =
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
val type = ChapterType.of(chapter, volume, libraryType)
|
val type = ChapterType.of(chapter, volume, libraryType)
|
||||||
|
val titleName = chapter.titleName ?: ""
|
||||||
|
val title = chapter.title ?: ""
|
||||||
|
val range = chapter.range ?: ""
|
||||||
|
|
||||||
name = when (type) {
|
name = when (type) {
|
||||||
ChapterType.Regular -> {
|
ChapterType.Regular -> {
|
||||||
@@ -80,23 +85,20 @@ class KavitaHelper {
|
|||||||
it.toString().padStart(2, '0')
|
it.toString().padStart(2, '0')
|
||||||
} ?: chapter.number
|
} ?: chapter.number
|
||||||
when {
|
when {
|
||||||
chapter.titleName.isBlank() -> "Vol.${volume.number} Ch.$chapterNum"
|
titleName.any { it.isLetter() } -> "$chapterNum - $titleName"
|
||||||
chapter.titleName.trim().matches(Regex("^\\d+$")) ->
|
else -> "Vol.${volume.minNumber} Ch.$chapterNum"
|
||||||
"Vol.${volume.number} Ch.${chapter.titleName.trim().padStart(2, '0')}"
|
|
||||||
else -> "Vol.${volume.number} Ch.$chapterNum - ${chapter.titleName}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChapterType.SingleFileVolume -> {
|
ChapterType.SingleFileVolume -> {
|
||||||
when {
|
when {
|
||||||
volume.name.isBlank() -> "Volume ${volume.number}"
|
volume.name.any { it.isLetter() } -> "v${volume.minNumber} - ${volume.name}"
|
||||||
volume.name.trim().matches(Regex("^\\d+$")) -> "Volume ${volume.name.trim().toIntOrNull()?.toString() ?: volume.name.trim()}"
|
else -> "Volume ${volume.minNumber}"
|
||||||
else -> "${volume.number} - ${volume.name}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChapterType.Special -> {
|
ChapterType.Special -> {
|
||||||
when {
|
when {
|
||||||
chapter.title.isNotBlank() -> chapter.title
|
title.isNotBlank() -> title
|
||||||
chapter.range.isNotBlank() -> chapter.range
|
range.isNotBlank() -> range
|
||||||
else -> "Special"
|
else -> "Special"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,9 +107,8 @@ class KavitaHelper {
|
|||||||
it.toString().padStart(2, '0')
|
it.toString().padStart(2, '0')
|
||||||
} ?: chapter.number
|
} ?: chapter.number
|
||||||
when {
|
when {
|
||||||
chapter.titleName.isBlank() -> "Chapter $chapterNum"
|
titleName.any { it.isLetter() } -> "$chapterNum - $titleName"
|
||||||
chapter.titleName.trim().matches(Regex("^\\d+$")) -> "Chapter ${chapter.titleName.trim().padStart(2, '0')}"
|
else -> "Chapter $chapterNum"
|
||||||
else -> "$chapterNum - ${chapter.titleName}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChapterType.Issue -> {
|
ChapterType.Issue -> {
|
||||||
@@ -115,10 +116,7 @@ class KavitaHelper {
|
|||||||
it.toString().padStart(3, '0')
|
it.toString().padStart(3, '0')
|
||||||
} ?: chapter.number
|
} ?: chapter.number
|
||||||
when {
|
when {
|
||||||
chapter.titleName.isNotBlank() && !chapter.titleName.trim().matches(Regex("^\\d+$")) ->
|
titleName.any { it.isLetter() } -> "$titleName (#$issueNum)"
|
||||||
"#$issueNum ${chapter.titleName}"
|
|
||||||
chapter.title.isNotBlank() && !chapter.title.trim().matches(Regex("^\\d+$")) ->
|
|
||||||
"#$issueNum ${chapter.title}"
|
|
||||||
else -> "Issue #$issueNum"
|
else -> "Issue #$issueNum"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,13 +126,19 @@ class KavitaHelper {
|
|||||||
type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->
|
type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->
|
||||||
singleFileVolumeNumber.toFloat()
|
singleFileVolumeNumber.toFloat()
|
||||||
|
|
||||||
|
type == ChapterType.SingleFileVolume -> {
|
||||||
|
// For standalone volumes, use positive numbers
|
||||||
|
volume.minNumber.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a regular chapter (not a volume)
|
||||||
type != ChapterType.SingleFileVolume ->
|
type != ChapterType.SingleFileVolume ->
|
||||||
chapter.number.toFloatOrNull() ?: 0f
|
chapter.number.toFloatOrNull() ?: 0f
|
||||||
|
|
||||||
// For volumes in mixed content, place them below chapter 0
|
// For volumes in mixed content, place them below chapter 0
|
||||||
else -> {
|
else -> {
|
||||||
val volumeNum = try {
|
val volumeNum = try {
|
||||||
volume.number.toString().toFloatOrNull() ?: 0f
|
volume.minNumber.toString().toFloatOrNull() ?: 0f
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
0f
|
0f
|
||||||
}
|
}
|
||||||
@@ -149,7 +153,6 @@ class KavitaHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (chapter.fileCount > 1) {
|
if (chapter.fileCount > 1) {
|
||||||
// salt/offset to recognize chapters with new merged part-chapters as new and hence unread
|
|
||||||
chapter_number += 0.001f * chapter.fileCount
|
chapter_number += 0.001f * chapter.fileCount
|
||||||
url = "${url}_${chapter.fileCount}"
|
url = "${url}_${chapter.fileCount}"
|
||||||
}
|
}
|
||||||
@@ -161,7 +164,6 @@ class KavitaHelper {
|
|||||||
ChapterType.Issue -> "Issue"
|
ChapterType.Issue -> "Issue"
|
||||||
ChapterType.Chapter -> "Chapter"
|
ChapterType.Chapter -> "Chapter"
|
||||||
ChapterType.Regular -> "Chapter"
|
ChapterType.Regular -> "Chapter"
|
||||||
else -> "Chapter"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,6 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
|
||||||
val formatterDate = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
|
||||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
|
||||||
val formatterDateTime = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
|
|
||||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
|
||||||
|
|
||||||
fun parseDateSafe(date: String?): Long {
|
fun parseDateSafe(date: String?): Long {
|
||||||
return date?.let {
|
return date?.let {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface ConvertibleToSManga {
|
|||||||
fun toSManga(baseUrl: String, apiUrl: String, apiKey: String): SManga
|
fun toSManga(baseUrl: String, apiUrl: String, apiKey: String): SManga
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable // https://github.com/Kareadita/Kavita/blob/develop/API/Entities/Enums/MangaFormat.cs
|
||||||
enum class MangaFormat(val format: Int) {
|
enum class MangaFormat(val format: Int) {
|
||||||
Image(0),
|
Image(0),
|
||||||
Archive(1),
|
Archive(1),
|
||||||
@@ -190,8 +190,8 @@ data class AuthorDto(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class VolumeDto(
|
data class VolumeDto(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val number: Int,
|
val minNumber: Int,
|
||||||
val name: String,
|
val name: String = "",
|
||||||
val pages: Int,
|
val pages: Int,
|
||||||
val pagesRead: Int,
|
val pagesRead: Int,
|
||||||
val lastModified: String,
|
val lastModified: String,
|
||||||
@@ -217,16 +217,16 @@ enum class ChapterType {
|
|||||||
fun of(chapter: ChapterDto, volume: VolumeDto, libraryType: LibraryTypeEnum? = null): ChapterType =
|
fun of(chapter: ChapterDto, volume: VolumeDto, libraryType: LibraryTypeEnum? = null): ChapterType =
|
||||||
when {
|
when {
|
||||||
// Special
|
// Special
|
||||||
volume.number == SPECIAL_NUMBER -> Special
|
volume.minNumber == SPECIAL_NUMBER -> Special
|
||||||
// Issue
|
// Issue
|
||||||
volume.number == UNNUMBERED_VOLUME_NUMBER -> when (libraryType) {
|
volume.minNumber == UNNUMBERED_VOLUME_NUMBER -> when (libraryType) {
|
||||||
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> Issue
|
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> Issue
|
||||||
else -> Chapter
|
else -> Chapter
|
||||||
}
|
}
|
||||||
// SingleFileVolume
|
// SingleFileVolume
|
||||||
chapter.number == KavitaConstants.UNNUMBERED_VOLUME_STR -> SingleFileVolume
|
chapter.number == KavitaConstants.UNNUMBERED_VOLUME_STR -> SingleFileVolume
|
||||||
// Regular
|
// Regular
|
||||||
volume.number > 0 -> Regular
|
volume.minNumber > 0 -> Regular
|
||||||
// Everything else depends on library type
|
// Everything else depends on library type
|
||||||
else -> when (libraryType) {
|
else -> when (libraryType) {
|
||||||
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> Issue
|
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> Issue
|
||||||
@@ -244,7 +244,7 @@ data class ChapterDto(
|
|||||||
val pages: Int,
|
val pages: Int,
|
||||||
val isSpecial: Boolean,
|
val isSpecial: Boolean,
|
||||||
val title: String,
|
val title: String,
|
||||||
val titleName: String,
|
val titleName: String = "",
|
||||||
val pagesRead: Int,
|
val pagesRead: Int,
|
||||||
val coverImageLocked: Boolean,
|
val coverImageLocked: Boolean,
|
||||||
val coverImage: String,
|
val coverImage: String,
|
||||||
@@ -268,13 +268,13 @@ data class ReadingListDto(
|
|||||||
val title: String,
|
val title: String,
|
||||||
val coverImage: String? = null,
|
val coverImage: String? = null,
|
||||||
val promoted: Boolean = false,
|
val promoted: Boolean = false,
|
||||||
val summary: String?,
|
val summary: String? = null,
|
||||||
val itemCount: Int,
|
val itemCount: Int,
|
||||||
val startingYear: Int,
|
val startingYear: Int,
|
||||||
val startingMonth: Int,
|
val startingMonth: Int,
|
||||||
val endingYear: Int,
|
val endingYear: Int,
|
||||||
val endingMonth: Int,
|
val endingMonth: Int,
|
||||||
val ownerUserName: String?,
|
val ownerUserName: String? = null,
|
||||||
@SerialName("items") val items: List<ReadingListItemDto> = emptyList(),
|
@SerialName("items") val items: List<ReadingListItemDto> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -287,11 +287,11 @@ data class ReadingListItemDto(
|
|||||||
val seriesName: String,
|
val seriesName: String,
|
||||||
val chapterNumber: String?,
|
val chapterNumber: String?,
|
||||||
val volumeNumber: String?,
|
val volumeNumber: String?,
|
||||||
val chapterTitleName: String?,
|
val chapterTitleName: String? = null,
|
||||||
val volumeId: Int?,
|
val volumeId: Int?,
|
||||||
val title: String?,
|
val title: String? = null,
|
||||||
val summary: String?,
|
val summary: String? = null,
|
||||||
val releaseDate: String?,
|
val releaseDate: String? = null,
|
||||||
val libraryName: String?,
|
val libraryName: String?,
|
||||||
@SerialName("readingListId") val readingListId: Int,
|
@SerialName("readingListId") val readingListId: Int,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user