Migration to min/max and add support for range chapters/volumes + customization for Webtoons + revised parsing
This commit is contained in:
@@ -28,6 +28,7 @@ import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataLibrary
|
|||||||
import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataPeople
|
import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataPeople
|
||||||
import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataPubStatus
|
import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataPubStatus
|
||||||
import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataTag
|
import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataTag
|
||||||
|
import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataUserRatings
|
||||||
import eu.kanade.tachiyomi.extension.all.kavita.dto.PersonRole
|
import eu.kanade.tachiyomi.extension.all.kavita.dto.PersonRole
|
||||||
import eu.kanade.tachiyomi.extension.all.kavita.dto.ReadingListDto
|
import eu.kanade.tachiyomi.extension.all.kavita.dto.ReadingListDto
|
||||||
import eu.kanade.tachiyomi.extension.all.kavita.dto.ReadingListItemDto
|
import eu.kanade.tachiyomi.extension.all.kavita.dto.ReadingListItemDto
|
||||||
@@ -140,20 +141,34 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
* - API-style URLs (e.g., `/Series/123`)
|
* - API-style URLs (e.g., `/Series/123`)
|
||||||
*/
|
*/
|
||||||
override fun getMangaUrl(manga: SManga): String {
|
override fun getMangaUrl(manga: SManga): String {
|
||||||
return when {
|
return try {
|
||||||
// API-style URL (used internally)
|
when {
|
||||||
manga.url.contains("/Series/") -> {
|
// Handle both API-style and web-style URLs
|
||||||
val seriesId = helper.getIdFromUrl(manga.url)
|
manga.url.contains("/series/") || manga.url.contains("/Series/") -> {
|
||||||
val cachedSeries = series.find { it.id == seriesId }
|
// Extract series ID from URL (works for both formats)
|
||||||
val libraryId = cachedSeries?.libraryId ?: run {
|
val seriesId = manga.url
|
||||||
// Fallback: fetch from API if not cached
|
.substringAfterLast("/series/")
|
||||||
val fetched = client.newCall(GET("$apiUrl/Series/$seriesId", headersBuilder().build()))
|
.substringAfterLast("/Series/")
|
||||||
.execute()
|
.substringBefore("?")
|
||||||
.parseAs<SeriesDto>()
|
.substringBefore("/")
|
||||||
fetched.libraryId
|
.toIntOrNull()
|
||||||
|
|
||||||
|
if (seriesId == null) {
|
||||||
|
Log.w(LOG_TAG, "Couldn't extract series ID from URL: ${manga.url}")
|
||||||
|
return manga.url
|
||||||
}
|
}
|
||||||
// Return web-friendly URL for webview while keeping API URL for tracking
|
|
||||||
|
// Try to get library ID from cached series first
|
||||||
|
val cachedSeries = series.find { it.id == seriesId }
|
||||||
|
val libraryId = cachedSeries?.libraryId
|
||||||
|
|
||||||
|
if (libraryId != null) {
|
||||||
|
// Use web-friendly URL format
|
||||||
"$baseUrl/library/$libraryId/series/$seriesId"
|
"$baseUrl/library/$libraryId/series/$seriesId"
|
||||||
|
} else {
|
||||||
|
// Fallback to original URL format if we don't have libraryId
|
||||||
|
"$baseUrl/library/1/series/$seriesId"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle reading list URLs
|
// Handle reading list URLs
|
||||||
@@ -165,6 +180,10 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
// Fallback - use original URL if we don't recognize the pattern
|
// Fallback - use original URL if we don't recognize the pattern
|
||||||
else -> manga.url
|
else -> manga.url
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(LOG_TAG, "Error in getMangaUrl", e)
|
||||||
|
manga.url // Always fall back to original URL
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChapterUrl(chapter: SChapter): String {
|
override fun getChapterUrl(chapter: SChapter): String {
|
||||||
@@ -406,7 +425,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
if (filter.state > 0) {
|
if (filter.state > 0) {
|
||||||
filterV2.addStatement(
|
filterV2.addStatement(
|
||||||
FilterComparison.GreaterThanEqual,
|
FilterComparison.GreaterThanEqual,
|
||||||
FilterField.AverageRating,
|
FilterField.UserRating,
|
||||||
filter.state.toString(),
|
filter.state.toString(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -503,24 +522,32 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is AgeRatingFilterGroup -> {
|
is AgeRatingFilterGroup -> {
|
||||||
filter.state.forEach { ageFilter ->
|
val included = filter.state
|
||||||
val rating = ageRatingsListMeta.find { it.title == (ageFilter as Filter.TriState).name }
|
.filter { it.state == STATE_INCLUDE }
|
||||||
rating?.let {
|
.mapNotNull { ageFilter ->
|
||||||
when (ageFilter.state) {
|
ageRatingsListMeta.find { it.title == ageFilter.name }?.value?.toString()
|
||||||
STATE_INCLUDE -> filterV2.addStatement(
|
}
|
||||||
|
val excluded = filter.state
|
||||||
|
.filter { it.state == STATE_EXCLUDE }
|
||||||
|
.mapNotNull { ageFilter ->
|
||||||
|
ageRatingsListMeta.find { it.title == ageFilter.name }?.value?.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (included.isNotEmpty()) {
|
||||||
|
filterV2.addStatement(
|
||||||
FilterComparison.Contains,
|
FilterComparison.Contains,
|
||||||
FilterField.AgeRating,
|
FilterField.AgeRating,
|
||||||
rating.value.toString(),
|
included.joinToString(","),
|
||||||
)
|
)
|
||||||
STATE_EXCLUDE -> filterV2.addStatement(
|
}
|
||||||
|
if (excluded.isNotEmpty()) {
|
||||||
|
filterV2.addStatement(
|
||||||
FilterComparison.NotContains,
|
FilterComparison.NotContains,
|
||||||
FilterField.AgeRating,
|
FilterField.AgeRating,
|
||||||
rating.value.toString(),
|
excluded.joinToString(","),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
is FormatsFilterGroup -> {
|
is FormatsFilterGroup -> {
|
||||||
filter.state.forEach { formatFilter ->
|
filter.state.forEach { formatFilter ->
|
||||||
if ((formatFilter as Filter.CheckBox).state) {
|
if ((formatFilter as Filter.CheckBox).state) {
|
||||||
@@ -972,9 +999,9 @@ 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.minNumber == KavitaConstants.UNNUMBERED_VOLUME) {
|
if (isComicLibrary && volume.minNumber.toInt() == KavitaConstants.UNNUMBERED_VOLUME) {
|
||||||
coverCandidates += volume.chapters
|
coverCandidates += volume.chapters
|
||||||
.filterNot { it.coverImage.isNullOrBlank() }
|
.filterNot { it.coverImage.isBlank() }
|
||||||
.map { chapter ->
|
.map { chapter ->
|
||||||
Triple(
|
Triple(
|
||||||
"$apiUrl/Image/chapter-cover?chapterId=${chapter.id}&apiKey=$apiKey",
|
"$apiUrl/Image/chapter-cover?chapterId=${chapter.id}&apiKey=$apiKey",
|
||||||
@@ -987,7 +1014,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
val hasSingleFile = volume.chapters.any { chapter ->
|
val hasSingleFile = volume.chapters.any { chapter ->
|
||||||
ChapterType.of(chapter, volume) == ChapterType.SingleFileVolume
|
ChapterType.of(chapter, volume) == ChapterType.SingleFileVolume
|
||||||
}
|
}
|
||||||
if (hasSingleFile && !volume.coverImage.isNullOrBlank()) {
|
if (hasSingleFile && volume.coverImage.isNotBlank()) {
|
||||||
val isUnread = volume.pagesRead < volume.pages
|
val isUnread = volume.pagesRead < volume.pages
|
||||||
val volumeNumber = volume.minNumber.toFloat()
|
val volumeNumber = volume.minNumber.toFloat()
|
||||||
coverCandidates.add(
|
coverCandidates.add(
|
||||||
@@ -1144,6 +1171,19 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
val items = response.parseAs<List<ReadingListItemDto>>()
|
val items = response.parseAs<List<ReadingListItemDto>>()
|
||||||
val readingListId = response.request.url.queryParameter("readingListId")
|
val readingListId = response.request.url.queryParameter("readingListId")
|
||||||
|
|
||||||
|
val firstItem = items.firstOrNull()
|
||||||
|
val isWebtoon = firstItem?.let { item ->
|
||||||
|
runCatching {
|
||||||
|
val meta = client.newCall(GET("$apiUrl/series/metadata?seriesId=${item.seriesId}", headersBuilder().build()))
|
||||||
|
.execute().parseAs<SeriesDetailPlusDto>()
|
||||||
|
|
||||||
|
val genreTitles = meta.genres.map { it.title }
|
||||||
|
val tagTitles = meta.tags.map { it.title }
|
||||||
|
val typeLabels = listOfNotNull(meta.libraryName)
|
||||||
|
helper.isWebtoonOrLongStrip(genreTitles + tagTitles + typeLabels)
|
||||||
|
}.getOrElse { false }
|
||||||
|
} ?: false
|
||||||
|
|
||||||
return items.map { item ->
|
return items.map { item ->
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
url = when {
|
url = when {
|
||||||
@@ -1183,9 +1223,9 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
append("${item.order + 1}. ")
|
append("${item.order + 1}. ")
|
||||||
when (type) {
|
when (type) {
|
||||||
ChapterType.Regular -> {
|
ChapterType.Regular -> {
|
||||||
val chapterNum = item.chapterNumber?.toIntOrNull()?.let {
|
val chapterNum = item.chapterNumber?.toIntOrNull()?.toString()
|
||||||
it.toString().padStart(2, '0')
|
?.padStart(2, '0')
|
||||||
} ?: item.chapterNumber ?: ""
|
?: item.chapterNumber ?: ""
|
||||||
when {
|
when {
|
||||||
item.chapterTitleName?.isNotBlank() == true ->
|
item.chapterTitleName?.isNotBlank() == true ->
|
||||||
append("$chapterNum - ${item.chapterTitleName}")
|
append("$chapterNum - ${item.chapterTitleName}")
|
||||||
@@ -1202,9 +1242,9 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChapterType.Issue -> {
|
ChapterType.Issue -> {
|
||||||
val issueNum = item.chapterNumber?.toIntOrNull()?.let {
|
val issueNum = item.chapterNumber?.toIntOrNull()?.toString()
|
||||||
it.toString().padStart(3, '0')
|
?.padStart(3, '0')
|
||||||
} ?: item.chapterNumber ?: ""
|
?: item.chapterNumber ?: ""
|
||||||
when {
|
when {
|
||||||
item.chapterTitleName?.isNotBlank() == true ->
|
item.chapterTitleName?.isNotBlank() == true ->
|
||||||
append("${item.chapterTitleName} (#$issueNum)")
|
append("${item.chapterTitleName} (#$issueNum)")
|
||||||
@@ -1213,14 +1253,14 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChapterType.Chapter -> {
|
ChapterType.Chapter -> {
|
||||||
val chapterNum = item.chapterNumber?.toIntOrNull()?.let {
|
val chapterNum = item.chapterNumber?.toIntOrNull()?.toString()
|
||||||
it.toString().padStart(2, '0')
|
?.padStart(2, '0')
|
||||||
} ?: item.chapterNumber ?: ""
|
?: item.chapterNumber ?: ""
|
||||||
when {
|
when {
|
||||||
item.chapterTitleName?.isNotBlank() == true ->
|
item.chapterTitleName?.isNotBlank() == true ->
|
||||||
append("$chapterNum - ${item.chapterTitleName}")
|
append("$chapterNum - ${item.chapterTitleName}")
|
||||||
else ->
|
else ->
|
||||||
append("Chapter $chapterNum")
|
append(if (isWebtoon) "Episode $chapterNum" else "Chapter $chapterNum")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChapterType.Special -> {
|
ChapterType.Special -> {
|
||||||
@@ -1361,31 +1401,59 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
val chapters = mutableListOf<SChapter>()
|
val chapters = mutableListOf<SChapter>()
|
||||||
val volumeItems = mutableListOf<SChapter>()
|
val volumeItems = mutableListOf<SChapter>()
|
||||||
|
|
||||||
|
// Get the series name to use as mangaTitle
|
||||||
|
val seriesName = seriesId?.let {
|
||||||
|
runCatching {
|
||||||
|
client.newCall(GET("$apiUrl/Series/$seriesId", headersBuilder().build()))
|
||||||
|
.execute().parseAs<SeriesDto>().name
|
||||||
|
}.getOrNull()
|
||||||
|
} ?: ""
|
||||||
|
|
||||||
|
// Get series metadata to determine if it's a webtoon
|
||||||
|
val isWebtoon = seriesId?.let {
|
||||||
|
runCatching {
|
||||||
|
val meta = client.newCall(GET("$apiUrl/series/metadata?seriesId=$it", headersBuilder().build()))
|
||||||
|
.execute().parseAs<SeriesDetailPlusDto>()
|
||||||
|
val genreTitles = meta.genres.map { it.title }
|
||||||
|
val tagTitles = meta.tags.map { it.title }
|
||||||
|
val typeLabels = listOfNotNull(meta.libraryName)
|
||||||
|
helper.isWebtoonOrLongStrip(genreTitles + tagTitles + typeLabels)
|
||||||
|
}.getOrElse { false }
|
||||||
|
} ?: false
|
||||||
|
|
||||||
volumes.forEach { volume ->
|
volumes.forEach { volume ->
|
||||||
// This fixes volumes that are recognized as chapters in Kavita because they have numbers in their titles
|
if (volume.chapters.size == 1 && volume.name.isNotBlank() && volume.minNumber.toInt() != KavitaConstants.SPECIAL_NUMBER) {
|
||||||
if (volume.chapters.size == 1 && volume.name.isNotBlank()) {
|
|
||||||
val chapter = volume.chapters.first()
|
val chapter = volume.chapters.first()
|
||||||
val sChapter = helper.chapterFromVolume(
|
val sChapter = helper.chapterFromVolume(
|
||||||
chapter,
|
chapter,
|
||||||
volume,
|
volume,
|
||||||
singleFileVolumeNumber = volume.minNumber,
|
singleFileVolumeNumber = volume.minNumber,
|
||||||
libraryType = libraryType,
|
libraryType = libraryType,
|
||||||
|
isWebtoon = isWebtoon,
|
||||||
|
mangaTitle = seriesName,
|
||||||
)
|
)
|
||||||
// Force SingleFileVolume type
|
sChapter.url = "/Chapter/${chapter.id}"
|
||||||
sChapter.name = when {
|
sChapter.name = when {
|
||||||
volume.name.any { it.isLetter() } -> "v${volume.minNumber} - ${volume.name}"
|
volume.name.any { it.isLetter() } -> "v${helper.formatVolumeNumber(volume)} - ${volume.name}"
|
||||||
else -> "Volume ${volume.minNumber}"
|
else -> "Volume ${helper.formatVolumeNumber(volume)}"
|
||||||
}
|
}
|
||||||
sChapter.chapter_number = volume.minNumber.toFloat()
|
sChapter.chapter_number = volume.minNumber.toFloat()
|
||||||
sChapter.scanlator = "Volume"
|
sChapter.scanlator = if (isWebtoon) "Season" else "Volume"
|
||||||
volumeItems.add(sChapter)
|
volumeItems.add(sChapter)
|
||||||
} else {
|
} else {
|
||||||
volume.chapters.forEach { chapter ->
|
volume.chapters.forEach { chapter ->
|
||||||
val type = ChapterType.of(chapter, volume, libraryType)
|
val type = ChapterType.of(chapter, volume, libraryType)
|
||||||
val sChapter = helper.chapterFromVolume(chapter, volume, libraryType = libraryType)
|
val sChapter = helper.chapterFromVolume(
|
||||||
|
chapter,
|
||||||
|
volume,
|
||||||
|
libraryType = libraryType,
|
||||||
|
isWebtoon = isWebtoon,
|
||||||
|
mangaTitle = seriesName,
|
||||||
|
)
|
||||||
if (type == ChapterType.SingleFileVolume) {
|
if (type == ChapterType.SingleFileVolume) {
|
||||||
volumeItems.add(sChapter)
|
volumeItems.add(sChapter)
|
||||||
} else {
|
} else {
|
||||||
|
sChapter.url = "/Chapter/${chapter.id}"
|
||||||
chapters.add(sChapter)
|
chapters.add(sChapter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1393,17 +1461,13 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
}
|
}
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
// 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 exist
|
|
||||||
volumeItems.isNotEmpty() && chapters.isEmpty() ->
|
volumeItems.isNotEmpty() && chapters.isEmpty() ->
|
||||||
volumeItems.sortedByDescending { it.chapter_number }
|
volumeItems.sortedByDescending { it.chapter_number }
|
||||||
|
|
||||||
// Case 3: Mixed content
|
|
||||||
else -> {
|
else -> {
|
||||||
// Convert volume numbers to negative for proper sorting
|
|
||||||
volumeItems.forEach { it.chapter_number = -it.chapter_number }
|
volumeItems.forEach { it.chapter_number = -it.chapter_number }
|
||||||
(
|
(
|
||||||
chapters.sortedByDescending { it.chapter_number } +
|
chapters.sortedByDescending { it.chapter_number } +
|
||||||
@@ -1498,9 +1562,35 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Original handling for regular chapters
|
// For normal titles
|
||||||
val chapterId = chapter.url.substringBefore("_")
|
|
||||||
return Observable.fromCallable {
|
return Observable.fromCallable {
|
||||||
|
// Check if this is a volume URL (contains "Volume/")
|
||||||
|
if (chapter.url.contains("/Volume/")) {
|
||||||
|
val volumeId = chapter.url.substringAfter("/Volume/").substringBefore("?").toIntOrNull()
|
||||||
|
?: throw IOException("Invalid volume ID")
|
||||||
|
|
||||||
|
// Get all chapters & pages in this volume
|
||||||
|
val volumeRequest = GET("$apiUrl/Volume/$volumeId", headersBuilder().build())
|
||||||
|
val volume = client.newCall(volumeRequest).execute().parseAs<VolumeDto>()
|
||||||
|
val pages = mutableListOf<Page>()
|
||||||
|
var pageOffset = 0
|
||||||
|
|
||||||
|
volume.chapters.sortedBy { it.number.toFloatOrNull() ?: 0f }.forEach { chapterDto ->
|
||||||
|
for (i in 0 until chapterDto.pages) {
|
||||||
|
pages.add(
|
||||||
|
Page(
|
||||||
|
index = pageOffset + i,
|
||||||
|
imageUrl = "$apiUrl/Reader/image?chapterId=${chapterDto.id}&page=$i&extractPdf=true&apiKey=$apiKey",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pageOffset += chapterDto.pages
|
||||||
|
}
|
||||||
|
|
||||||
|
pages.toList()
|
||||||
|
} else {
|
||||||
|
// Original chapter handling
|
||||||
|
val chapterId = chapter.url.substringAfter("/Chapter/").substringBefore("_")
|
||||||
val chapterRequest = GET("$apiUrl/Chapter?chapterId=$chapterId", headersBuilder().build())
|
val chapterRequest = GET("$apiUrl/Chapter?chapterId=$chapterId", headersBuilder().build())
|
||||||
val response = client.newCall(chapterRequest).execute()
|
val response = client.newCall(chapterRequest).execute()
|
||||||
|
|
||||||
@@ -1508,13 +1598,6 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
throw IOException("Failed to fetch chapter details: HTTP ${response.code}")
|
throw IOException("Failed to fetch chapter details: HTTP ${response.code}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check content type to ensure we got JSON
|
|
||||||
val contentType = response.header("Content-Type") ?: ""
|
|
||||||
if (!contentType.contains("application/json")) {
|
|
||||||
val bodyPreview = response.peekBody(128).string()
|
|
||||||
throw IOException("${helper.intl["error_unexpected_response"]}: $contentType - $bodyPreview")
|
|
||||||
}
|
|
||||||
|
|
||||||
val chapterDetails = try {
|
val chapterDetails = try {
|
||||||
response.parseAs<ChapterDto>()
|
response.parseAs<ChapterDto>()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -1531,8 +1614,8 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
pages.toList()
|
pages.toList()
|
||||||
|
}
|
||||||
}.onErrorResumeNext { error ->
|
}.onErrorResumeNext { error ->
|
||||||
// Fallback to using the scanlator field if we can't get chapter details
|
|
||||||
Log.e(LOG_TAG, "Error fetching chapter details, using fallback", error)
|
Log.e(LOG_TAG, "Error fetching chapter details, using fallback", error)
|
||||||
val fallbackPageCount = chapter.scanlator?.replace(" pages", "")?.toIntOrNull() ?: 1
|
val fallbackPageCount = chapter.scanlator?.replace(" pages", "")?.toIntOrNull() ?: 1
|
||||||
val pages = mutableListOf<Page>()
|
val pages = mutableListOf<Page>()
|
||||||
@@ -1540,7 +1623,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
pages.add(
|
pages.add(
|
||||||
Page(
|
Page(
|
||||||
index = i,
|
index = i,
|
||||||
imageUrl = "$apiUrl/Reader/image?chapterId=$chapterId&page=$i&extractPdf=true&apiKey=$apiKey",
|
imageUrl = "$apiUrl/Reader/image?chapterId=${chapter.url.substringBefore("_")}&page=$i&extractPdf=true&apiKey=$apiKey",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1578,6 +1661,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
private var ageRatingsListMeta = emptyList<MetadataAgeRatings>()
|
private var ageRatingsListMeta = emptyList<MetadataAgeRatings>()
|
||||||
private var peopleListMeta = emptyList<MetadataPeople>()
|
private var peopleListMeta = emptyList<MetadataPeople>()
|
||||||
private var pubStatusListMeta = emptyList<MetadataPubStatus>()
|
private var pubStatusListMeta = emptyList<MetadataPubStatus>()
|
||||||
|
private var userRatingsListMeta = emptyList<MetadataUserRatings>()
|
||||||
private var languagesListMeta = emptyList<MetadataLanguages>()
|
private var languagesListMeta = emptyList<MetadataLanguages>()
|
||||||
private var libraryListMeta = emptyList<MetadataLibrary>()
|
private var libraryListMeta = emptyList<MetadataLibrary>()
|
||||||
private var collectionsListMeta = emptyList<MetadataCollections>()
|
private var collectionsListMeta = emptyList<MetadataCollections>()
|
||||||
@@ -1676,7 +1760,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (ageRatingsListMeta.isNotEmpty() and toggledFilters.contains("Rating")) {
|
if (userRatingsListMeta.isNotEmpty() and toggledFilters.contains("Rating")) {
|
||||||
filtersLoaded.add(
|
filtersLoaded.add(
|
||||||
UserRating(),
|
UserRating(),
|
||||||
)
|
)
|
||||||
@@ -1915,9 +1999,6 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
private val SharedPreferences.LastVolumeCover: Boolean
|
private val SharedPreferences.LastVolumeCover: Boolean
|
||||||
get() = getBoolean(LAST_VOLUME_COVER_PREF, LAST_VOLUME_COVER_DEFAULT)
|
get() = getBoolean(LAST_VOLUME_COVER_PREF, LAST_VOLUME_COVER_DEFAULT)
|
||||||
|
|
||||||
private val SharedPreferences.showEpub: Boolean
|
|
||||||
get() = getBoolean(SHOW_EPUB_PREF, SHOW_EPUB_DEFAULT)
|
|
||||||
|
|
||||||
private val SharedPreferences.RdDate: Boolean
|
private val SharedPreferences.RdDate: Boolean
|
||||||
get() = getBoolean(RD_DATE_PREF, RD_DATE_DEFAULT)
|
get() = getBoolean(RD_DATE_PREF, RD_DATE_DEFAULT)
|
||||||
|
|
||||||
@@ -2187,15 +2268,16 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
|||||||
}
|
}
|
||||||
client.newCall(GET("$apiUrl/Metadata/publication-status", headersBuilder().build()))
|
client.newCall(GET("$apiUrl/Metadata/publication-status", headersBuilder().build()))
|
||||||
.execute().use { response ->
|
.execute().use { response ->
|
||||||
pubStatusListMeta = try {
|
if (!response.isSuccessful) {
|
||||||
response.body.use { json.decodeFromString(it.string()) }
|
Log.e(LOG_TAG, "Failed to fetch publication status: ${response.code}")
|
||||||
|
pubStatusListMeta = emptyList()
|
||||||
|
return@use
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
pubStatusListMeta = response.body.use { json.decodeFromString(it.string()) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(
|
Log.e(LOG_TAG, "Error decoding publication status JSON", e)
|
||||||
LOG_TAG,
|
pubStatusListMeta = emptyList()
|
||||||
"error while decoding JSON for publicationStatusListMeta filter",
|
|
||||||
e,
|
|
||||||
)
|
|
||||||
emptyList()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
client.newCall(GET("$apiUrl/filter", headersBuilder().build()))
|
client.newCall(GET("$apiUrl/filter", headersBuilder().build()))
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ object KavitaConstants {
|
|||||||
|
|
||||||
const val UNNUMBERED_VOLUME = -100000
|
const val UNNUMBERED_VOLUME = -100000
|
||||||
const val UNNUMBERED_VOLUME_STR = "-100000"
|
const val UNNUMBERED_VOLUME_STR = "-100000"
|
||||||
|
const val SPECIAL_NUMBER = 100_000
|
||||||
|
const val VOLUME_NUMBER_OFFSET = 100000f
|
||||||
|
|
||||||
val PERSON_ROLES = listOf(
|
val PERSON_ROLES = listOf(
|
||||||
"Writer", "Penciller", "Inker", "Colorist",
|
"Writer", "Penciller", "Inker", "Colorist",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.kavita
|
package eu.kanade.tachiyomi.extension.all.kavita
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import eu.kanade.tachiyomi.extension.all.kavita.dto.ChapterDto
|
import eu.kanade.tachiyomi.extension.all.kavita.dto.ChapterDto
|
||||||
import eu.kanade.tachiyomi.extension.all.kavita.dto.ChapterType
|
import eu.kanade.tachiyomi.extension.all.kavita.dto.ChapterType
|
||||||
import eu.kanade.tachiyomi.extension.all.kavita.dto.LibraryTypeEnum
|
import eu.kanade.tachiyomi.extension.all.kavita.dto.LibraryTypeEnum
|
||||||
@@ -12,9 +13,7 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
|
||||||
|
|
||||||
class KavitaHelper {
|
class KavitaHelper {
|
||||||
val json = Json {
|
val json = Json {
|
||||||
@@ -26,13 +25,6 @@ class KavitaHelper {
|
|||||||
useArrayPolymorphism = true
|
useArrayPolymorphism = true
|
||||||
prettyPrint = true
|
prettyPrint = true
|
||||||
}
|
}
|
||||||
inline fun <reified T : Enum<T>> safeValueOf(type: String): T {
|
|
||||||
return java.lang.Enum.valueOf(T::class.java, type)
|
|
||||||
}
|
|
||||||
val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSS", Locale.US)
|
|
||||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
|
||||||
fun parseDate(dateAsString: String): Long =
|
|
||||||
dateFormatter.parse(dateAsString)?.time ?: 0
|
|
||||||
|
|
||||||
fun hasNextPage(response: Response): Boolean {
|
fun hasNextPage(response: Response): Boolean {
|
||||||
val paginationHeader = response.header("Pagination")
|
val paginationHeader = response.header("Pagination")
|
||||||
@@ -72,56 +64,98 @@ class KavitaHelper {
|
|||||||
thumbnail_url = "$baseUrl/image/series-cover?seriesId=${obj.id}&apiKey=$apiKey"
|
thumbnail_url = "$baseUrl/image/series-cover?seriesId=${obj.id}&apiKey=$apiKey"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun chapterFromVolume(chapter: ChapterDto, volume: VolumeDto, singleFileVolumeNumber: Int? = null, libraryType: LibraryTypeEnum? = null): SChapter =
|
fun chapterFromVolume(
|
||||||
|
chapter: ChapterDto,
|
||||||
|
volume: VolumeDto,
|
||||||
|
singleFileVolumeNumber: Double? = null,
|
||||||
|
libraryType: LibraryTypeEnum? = null,
|
||||||
|
isWebtoon: Boolean = false,
|
||||||
|
mangaTitle: String = "",
|
||||||
|
): 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 titleName = chapter.titleName
|
||||||
val title = chapter.title ?: ""
|
val title = chapter.title
|
||||||
val range = chapter.range ?: ""
|
val range = chapter.range
|
||||||
|
// For webtoon
|
||||||
|
val chapterLabel = if (isWebtoon) "Episode" else "Chapter"
|
||||||
|
val volumeLabel = if (isWebtoon) "Season" else "Volume"
|
||||||
|
|
||||||
name = when (type) {
|
name = when (type) {
|
||||||
ChapterType.Regular -> {
|
ChapterType.Regular -> {
|
||||||
val chapterNum = chapter.number.toIntOrNull()?.let {
|
val chapterNum = formatChapterNumber(chapter)
|
||||||
it.toString().padStart(2, '0')
|
|
||||||
} ?: chapter.number
|
|
||||||
when {
|
when {
|
||||||
titleName.any { it.isLetter() } -> "$chapterNum - $titleName"
|
titleName.isNotBlank() && titleName.any { it.isLetter() } ->
|
||||||
else -> "Vol.${volume.minNumber} Ch.$chapterNum"
|
"$chapterNum - ${cleanChapterTitle(
|
||||||
|
titleName,
|
||||||
|
ChapterTitleContext(
|
||||||
|
mangaTitle = mangaTitle,
|
||||||
|
chapterNumber = chapterNum,
|
||||||
|
volumeName = volume.name.ifBlank { title },
|
||||||
|
),
|
||||||
|
)}"
|
||||||
|
else ->
|
||||||
|
"$volumeLabel ${formatVolumeNumber(volume)} $chapterLabel $chapterNum"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChapterType.SingleFileVolume -> {
|
ChapterType.SingleFileVolume -> {
|
||||||
|
cleanChapterTitle(
|
||||||
when {
|
when {
|
||||||
volume.name.any { it.isLetter() } -> "v${volume.minNumber} - ${volume.name}"
|
volume.name.any { it.isLetter() } -> "v${volume.minNumber} - ${volume.name}"
|
||||||
else -> "Volume ${volume.minNumber}"
|
else -> "Volume ${volume.minNumber}"
|
||||||
}
|
},
|
||||||
|
ChapterTitleContext(
|
||||||
|
mangaTitle = mangaTitle,
|
||||||
|
volumeName = volume.name,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
ChapterType.Special -> {
|
ChapterType.Special -> {
|
||||||
|
cleanChapterTitle(
|
||||||
when {
|
when {
|
||||||
title.isNotBlank() -> title
|
title.isNotBlank() -> title
|
||||||
range.isNotBlank() -> range
|
range.isNotBlank() -> range
|
||||||
else -> "Special"
|
else -> "Special"
|
||||||
}
|
},
|
||||||
|
ChapterTitleContext(
|
||||||
|
mangaTitle = mangaTitle,
|
||||||
|
volumeName = volume.name,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
ChapterType.Chapter -> {
|
ChapterType.Chapter -> {
|
||||||
val chapterNum = chapter.number.toIntOrNull()?.let {
|
val chapterNum = formatChapterNumber(chapter)
|
||||||
it.toString().padStart(2, '0')
|
|
||||||
} ?: chapter.number
|
|
||||||
when {
|
when {
|
||||||
titleName.any { it.isLetter() } -> "$chapterNum - $titleName"
|
titleName.isNotBlank() && titleName.any { it.isLetter() } ->
|
||||||
else -> "Chapter $chapterNum"
|
"$chapterNum - ${cleanChapterTitle(
|
||||||
|
titleName,
|
||||||
|
ChapterTitleContext(
|
||||||
|
mangaTitle = mangaTitle,
|
||||||
|
chapterNumber = chapterNum,
|
||||||
|
volumeName = volume.name,
|
||||||
|
),
|
||||||
|
)}"
|
||||||
|
else ->
|
||||||
|
"$chapterLabel $chapterNum"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChapterType.Issue -> {
|
ChapterType.Issue -> {
|
||||||
val issueNum = chapter.number.toIntOrNull()?.let {
|
val issueNum = chapter.number.toIntOrNull()?.toString()?.padStart(3, '0') ?: chapter.number
|
||||||
it.toString().padStart(3, '0')
|
cleanChapterTitle(
|
||||||
} ?: chapter.number
|
|
||||||
when {
|
when {
|
||||||
titleName.any { it.isLetter() } -> "$titleName (#$issueNum)"
|
titleName.any { it.isLetter() } -> "$titleName (#$issueNum)"
|
||||||
else -> "Issue #$issueNum"
|
else -> "Issue #$issueNum"
|
||||||
}
|
},
|
||||||
|
ChapterTitleContext(
|
||||||
|
mangaTitle = mangaTitle,
|
||||||
|
chapterNumber = issueNum,
|
||||||
|
volumeName = volume.name,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle decimal chapter numbers properly
|
||||||
chapter_number = when {
|
chapter_number = when {
|
||||||
type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->
|
type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->
|
||||||
singleFileVolumeNumber.toFloat()
|
singleFileVolumeNumber.toFloat()
|
||||||
@@ -131,39 +165,127 @@ class KavitaHelper {
|
|||||||
volume.minNumber.toFloat()
|
volume.minNumber.toFloat()
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is a regular chapter (not a volume)
|
// For regular chapters
|
||||||
type != ChapterType.SingleFileVolume ->
|
type != ChapterType.SingleFileVolume && type != ChapterType.Special -> {
|
||||||
chapter.number.toFloatOrNull() ?: 0f
|
// Handle decimal chapter numbers
|
||||||
|
if (chapter.minNumber % 1 != 0.0) {
|
||||||
|
chapter.minNumber.toFloat()
|
||||||
|
} else {
|
||||||
|
chapter.minNumber.toInt().toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// For volumes in mixed content, place them below chapter 0
|
// For volumes/specials in mixed content, place them below chapter 0
|
||||||
else -> {
|
else -> {
|
||||||
val volumeNum = try {
|
val volumeNum = try {
|
||||||
volume.minNumber.toString().toFloatOrNull() ?: 0f
|
if (volume.minNumber % 1 != 0.0) {
|
||||||
|
volume.minNumber.toFloat()
|
||||||
|
} else {
|
||||||
|
volume.minNumber.toInt().toFloat()
|
||||||
|
}
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
0f
|
0f
|
||||||
}
|
}
|
||||||
-volumeNum - VOLUME_NUMBER_OFFSET
|
-volumeNum - KavitaConstants.VOLUME_NUMBER_OFFSET
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
url = when {
|
url = when {
|
||||||
type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->
|
type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->
|
||||||
"volume_${volume.id}"
|
"volume_${volume.id}"
|
||||||
else -> chapter.id.toString()
|
else -> "chapter_${chapter.id}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle multi-file chapters
|
||||||
if (chapter.fileCount > 1) {
|
if (chapter.fileCount > 1) {
|
||||||
|
// Add small offset to maintain order while keeping the main chapter number
|
||||||
chapter_number += 0.001f * chapter.fileCount
|
chapter_number += 0.001f * chapter.fileCount
|
||||||
|
// Append file count to URL for reference
|
||||||
url = "${url}_${chapter.fileCount}"
|
url = "${url}_${chapter.fileCount}"
|
||||||
}
|
}
|
||||||
|
|
||||||
date_upload = parseDateSafe(chapter.created)
|
date_upload = parseDateSafe(chapter.created)
|
||||||
scanlator = when (type) {
|
scanlator = when (type) {
|
||||||
ChapterType.SingleFileVolume -> "Volume"
|
ChapterType.SingleFileVolume -> if (isWebtoon) "Season" else "Volume"
|
||||||
ChapterType.Special -> "Special"
|
ChapterType.Special -> "Special"
|
||||||
ChapterType.Issue -> "Issue"
|
ChapterType.Issue -> "Issue"
|
||||||
ChapterType.Chapter -> "Chapter"
|
ChapterType.Chapter -> if (isWebtoon) "Episode" else "Chapter"
|
||||||
ChapterType.Regular -> "Chapter"
|
ChapterType.Regular -> if (isWebtoon) "Episode" else "Chapter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun formatVolumeNumber(volume: VolumeDto): String {
|
||||||
|
return when {
|
||||||
|
volume.maxNumber > volume.minNumber ->
|
||||||
|
"${removeTrailingZero(volume.minNumber)}-${removeTrailingZero(volume.maxNumber)}"
|
||||||
|
else -> removeTrailingZero(volume.minNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatChapterNumber(chapter: ChapterDto, padLength: Int = 2): String {
|
||||||
|
val chapterNum = when {
|
||||||
|
chapter.maxNumber > chapter.minNumber ->
|
||||||
|
"${removeTrailingZero(chapter.minNumber)}-${removeTrailingZero(chapter.maxNumber)}"
|
||||||
|
else -> removeTrailingZero(chapter.minNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only pad if it's a single whole number (not a range and not decimal)
|
||||||
|
return if (!chapterNum.contains('-') && !chapterNum.contains('.')) {
|
||||||
|
chapterNum.toIntOrNull()?.toString()?.padStart(padLength, '0') ?: chapterNum
|
||||||
|
} else {
|
||||||
|
chapterNum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to remove .0 from whole numbers while preserving actual decimals
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
|
private fun removeTrailingZero(number: Double): String {
|
||||||
|
return if (number % 1 == 0.0) {
|
||||||
|
number.toInt().toString()
|
||||||
|
} else {
|
||||||
|
// Use String.format to avoid scientific notation for very small decimals
|
||||||
|
String.format("%.10f", number).replace(",", ".").trimEnd('0').trimEnd('.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanChapterTitle(
|
||||||
|
originalTitle: String,
|
||||||
|
context: ChapterTitleContext? = null,
|
||||||
|
): String {
|
||||||
|
var title = originalTitle.trim()
|
||||||
|
val mangaTitle = context?.mangaTitle ?: ""
|
||||||
|
|
||||||
|
// Remove manga name if present
|
||||||
|
if (mangaTitle.isNotBlank()) {
|
||||||
|
title = title.replace(Regex("(?i)\\b${Regex.escape(mangaTitle)}\\b[\\s\\-:]*"), "").trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove chapter number patterns like c0116, c001, etc.
|
||||||
|
title = title.replace(Regex("""(?i)\bc\d+\b"""), "").trim()
|
||||||
|
|
||||||
|
// Clean up problematic hyphen/space patterns
|
||||||
|
title = title.replace(Regex("""\s*[-.]\s*[-.]\s*"""), " - ") // Handles -.-, - . -, etc.
|
||||||
|
.replace(Regex("""\s*-\s*-\s*"""), " - ") // Double hyphens with spaces
|
||||||
|
.replace(Regex("""^\s*[-.]\s*|\s*[-.]\s*$"""), "") // Leading/trailing hyphens or dots
|
||||||
|
.trim()
|
||||||
|
|
||||||
|
return title.ifBlank { originalTitle }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class to hold context information for cleaning chapter titles
|
||||||
|
*/
|
||||||
|
data class ChapterTitleContext(
|
||||||
|
val mangaTitle: String = "",
|
||||||
|
val chapterNumber: String = "",
|
||||||
|
val volumeName: String = "",
|
||||||
|
val isWebtoon: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun isWebtoonOrLongStrip(tags: List<String>): Boolean {
|
||||||
|
return tags.any {
|
||||||
|
val normalized = it.trim().lowercase()
|
||||||
|
normalized.contains("webtoon") || normalized.contains("long strip")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,8 +301,4 @@ class KavitaHelper {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val VOLUME_NUMBER_OFFSET = 100000f
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ enum class MangaFormat(val format: Int) {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val map = values().associateBy(MangaFormat::format)
|
private val map = values().associateBy(MangaFormat::format)
|
||||||
fun fromInt(type: Int): MangaFormat? = map[type]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +189,8 @@ data class AuthorDto(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class VolumeDto(
|
data class VolumeDto(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val minNumber: Int,
|
val minNumber: Double,
|
||||||
|
val maxNumber: Double,
|
||||||
val name: String = "",
|
val name: String = "",
|
||||||
val pages: Int,
|
val pages: Int,
|
||||||
val pagesRead: Int,
|
val pagesRead: Int,
|
||||||
@@ -211,15 +211,15 @@ enum class ChapterType {
|
|||||||
;
|
;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val SPECIAL_NUMBER = 100_000
|
|
||||||
private const val UNNUMBERED_VOLUME_NUMBER = -100_000
|
private const val UNNUMBERED_VOLUME_NUMBER = -100_000
|
||||||
|
|
||||||
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.minNumber == SPECIAL_NUMBER -> Special
|
volume.minNumber.toInt() == KavitaConstants.SPECIAL_NUMBER ||
|
||||||
|
chapter.minNumber.toInt() == KavitaConstants.SPECIAL_NUMBER -> Special
|
||||||
// Issue
|
// Issue
|
||||||
volume.minNumber == UNNUMBERED_VOLUME_NUMBER -> when (libraryType) {
|
volume.minNumber.toInt() == UNNUMBERED_VOLUME_NUMBER -> when (libraryType) {
|
||||||
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> Issue
|
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> Issue
|
||||||
else -> Chapter
|
else -> Chapter
|
||||||
}
|
}
|
||||||
@@ -241,6 +241,8 @@ data class ChapterDto(
|
|||||||
val id: Int,
|
val id: Int,
|
||||||
val range: String,
|
val range: String,
|
||||||
val number: String,
|
val number: String,
|
||||||
|
val minNumber: Double,
|
||||||
|
val maxNumber: Double,
|
||||||
val pages: Int,
|
val pages: Int,
|
||||||
val isSpecial: Boolean,
|
val isSpecial: Boolean,
|
||||||
val title: String,
|
val title: String,
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ data class MetadataTag(val id: Int, val title: String)
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class MetadataAgeRatings(val value: Int, val title: String)
|
data class MetadataAgeRatings(val value: Int, val title: String)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MetadataUserRatings(val value: Int, val title: String)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MetadataLanguages(val isoCode: String, val title: String)
|
data class MetadataLanguages(val isoCode: String, val title: String)
|
||||||
|
|
||||||
@@ -37,40 +40,3 @@ data class SmartFilter(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val filter: String,
|
val filter: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MetadataPayload(
|
|
||||||
val forceUseMetadataPayload: Boolean = true,
|
|
||||||
var sorting: Int = 1,
|
|
||||||
var sorting_asc: Boolean = true,
|
|
||||||
var readStatus: ArrayList<String> = arrayListOf("notRead", "inProgress", "read"),
|
|
||||||
var genres_i: ArrayList<Int> = arrayListOf(),
|
|
||||||
var genres_e: ArrayList<Int> = arrayListOf(),
|
|
||||||
var tags_i: ArrayList<Int> = arrayListOf(),
|
|
||||||
var tags_e: ArrayList<Int> = arrayListOf(),
|
|
||||||
var ageRating_i: ArrayList<Int> = arrayListOf(),
|
|
||||||
var ageRating_e: ArrayList<Int> = arrayListOf(),
|
|
||||||
var formats: ArrayList<Int> = arrayListOf(),
|
|
||||||
var collections_i: ArrayList<Int> = arrayListOf(),
|
|
||||||
var collections_e: ArrayList<Int> = arrayListOf(),
|
|
||||||
var userRating: Int = 0,
|
|
||||||
var people: ArrayList<Int> = arrayListOf(),
|
|
||||||
var language_i: ArrayList<String> = arrayListOf(),
|
|
||||||
var language_e: ArrayList<String> = arrayListOf(),
|
|
||||||
var libraries_i: ArrayList<Int> = arrayListOf(),
|
|
||||||
var libraries_e: ArrayList<Int> = arrayListOf(),
|
|
||||||
var pubStatus: ArrayList<Int> = arrayListOf(),
|
|
||||||
var seriesNameQuery: String = "",
|
|
||||||
var releaseYearRangeMin: Int = 0,
|
|
||||||
var releaseYearRangeMax: Int = 0,
|
|
||||||
var peopleWriters: ArrayList<Int> = arrayListOf(),
|
|
||||||
var peoplePenciller: ArrayList<Int> = arrayListOf(),
|
|
||||||
var peopleInker: ArrayList<Int> = arrayListOf(),
|
|
||||||
var peopleColorist: ArrayList<Int> = arrayListOf(),
|
|
||||||
var peopleLetterer: ArrayList<Int> = arrayListOf(),
|
|
||||||
var peopleCoverArtist: ArrayList<Int> = arrayListOf(),
|
|
||||||
var peopleEditor: ArrayList<Int> = arrayListOf(),
|
|
||||||
var peoplePublisher: ArrayList<Int> = arrayListOf(),
|
|
||||||
var peopleCharacter: ArrayList<Int> = arrayListOf(),
|
|
||||||
var peopleTranslator: ArrayList<Int> = arrayListOf(),
|
|
||||||
)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user