Fix downloading

master
Koitharu 2 years ago
parent ae57561591
commit 6e6c70a770
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 617 versionCode = 618
versionName = '6.6.7' versionName = '6.6.8'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp { ksp {

@ -13,6 +13,8 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@JvmName("mangaIds") @JvmName("mangaIds")
@ -113,3 +115,16 @@ val Manga.appUrl: Uri
.appendQueryParameter("name", title) .appendQueryParameter("name", title)
.appendQueryParameter("url", url) .appendQueryParameter("url", url)
.build() .build()
private val chaptersNumberFormat = DecimalFormat("#.#").also { f ->
f.decimalFormatSymbols = DecimalFormatSymbols.getInstance().also {
it.decimalSeparator = '.'
}
}
fun MangaChapter.formatNumber(): String? {
if (number <= 0f) {
return null
}
return chaptersNumberFormat.format(number.toDouble())
}

@ -4,6 +4,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.formatNumber
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.drawableStart import org.koitharu.kotatsu.core.util.ext.drawableStart
@ -26,7 +27,7 @@ fun chapterListItemAD(
bind { payloads -> bind { payloads ->
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
binding.textViewTitle.text = item.chapter.name binding.textViewTitle.text = item.chapter.name
binding.textViewNumber.text = item.chapter.number.toString() binding.textViewNumber.text = item.chapter.formatNumber()
binding.textViewDescription.textAndVisible = item.description() binding.textViewDescription.textAndVisible = item.description()
} }
when { when {

@ -193,12 +193,12 @@ class DownloadWorker @AssistedInject constructor(
val chapters = getChapters(mangaDetails, includedIds) val chapters = getChapters(mangaDetails, includedIds)
for ((chapterIndex, chapter) in chapters.withIndex()) { for ((chapterIndex, chapter) in chapters.withIndex()) {
checkIsPaused() checkIsPaused()
if (chaptersToSkip.remove(chapter.id)) { if (chaptersToSkip.remove(chapter.value.id)) {
publishState(currentState.copy(downloadedChapters = currentState.downloadedChapters + 1)) publishState(currentState.copy(downloadedChapters = currentState.downloadedChapters + 1))
continue continue
} }
val pages = runFailsafe { val pages = runFailsafe {
repo.getPages(chapter) repo.getPages(chapter.value)
} ?: continue } ?: continue
val pageCounter = AtomicInteger(0) val pageCounter = AtomicInteger(0)
channelFlow { channelFlow {
@ -237,7 +237,7 @@ class DownloadWorker @AssistedInject constructor(
), ),
) )
} }
if (output.flushChapter(chapter)) { if (output.flushChapter(chapter.value)) {
runCatchingCancellable { runCatchingCancellable {
localStorageChanges.emit(LocalMangaInput.of(output.rootFile).getManga()) localStorageChanges.emit(LocalMangaInput.of(output.rootFile).getManga())
}.onFailure(Throwable::printStackTraceDebug) }.onFailure(Throwable::printStackTraceDebug)
@ -377,19 +377,26 @@ class DownloadWorker @AssistedInject constructor(
private fun getChapters( private fun getChapters(
manga: Manga, manga: Manga,
includedIds: LongArray?, includedIds: LongArray?,
): List<MangaChapter> { ): List<IndexedValue<MangaChapter>> {
val chapters = checkNotNull(manga.chapters) { val chapters = checkNotNull(manga.chapters) { "Chapters list must not be null" }
"Chapters list must not be null" val chaptersIdsSet = includedIds?.toMutableSet()
}.toMutableList() val result = ArrayList<IndexedValue<MangaChapter>>((chaptersIdsSet ?: chapters).size)
if (includedIds != null) { val counters = HashMap<String?, Int>()
val chaptersIdsSet = includedIds.toMutableSet() for (chapter in chapters) {
chapters.retainAll { x -> chaptersIdsSet.remove(x.id) } val index = counters[chapter.branch] ?: 0
counters[chapter.branch] = index + 1
if (chaptersIdsSet != null && !chaptersIdsSet.remove(chapter.id)) {
continue
}
result.add(IndexedValue(index, chapter))
}
if (chaptersIdsSet != null) {
check(chaptersIdsSet.isEmpty()) { check(chaptersIdsSet.isEmpty()) {
"${chaptersIdsSet.size} of ${includedIds.size} requested chapters not found in manga" "${chaptersIdsSet.size} of ${includedIds.size} requested chapters not found in manga"
} }
} }
check(chapters.isNotEmpty()) { "Chapters list must not be empty" } check(result.isNotEmpty()) { "Chapters list must not be empty" }
return chapters return result
} }
private suspend inline fun <T> withMangaLock(manga: Manga, block: () -> T) = try { private suspend inline fun <T> withMangaLock(manga: Manga, block: () -> T) = try {

@ -12,6 +12,8 @@ import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.find import org.koitharu.kotatsu.parsers.util.find
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
@ -86,19 +88,20 @@ class MangaIndex(source: String?) {
fun getCoverEntry(): String? = json.getStringOrNull("cover_entry") fun getCoverEntry(): String? = json.getStringOrNull("cover_entry")
fun addChapter(chapter: MangaChapter, filename: String?) { fun addChapter(chapter: IndexedValue<MangaChapter>, filename: String?) {
val chapters = json.getJSONObject("chapters") val chapters = json.getJSONObject("chapters")
if (!chapters.has(chapter.id.toString())) { if (!chapters.has(chapter.value.id.toString())) {
val jo = JSONObject() val jo = JSONObject()
jo.put("number", chapter.number) jo.put("number", chapter.value.number)
jo.put("url", chapter.url) jo.put("volume", chapter.value.volume)
jo.put("name", chapter.name) jo.put("url", chapter.value.url)
jo.put("uploadDate", chapter.uploadDate) jo.put("name", chapter.value.name)
jo.put("scanlator", chapter.scanlator) jo.put("uploadDate", chapter.value.uploadDate)
jo.put("branch", chapter.branch) jo.put("scanlator", chapter.value.scanlator)
jo.put("entries", "%08d_%03d\\d{3}".format(chapter.branch.hashCode(), chapter.number)) jo.put("branch", chapter.value.branch)
jo.put("entries", "%08d_%03d\\d{3}".format(chapter.value.branch.hashCode(), chapter.index + 1))
jo.put("file", filename) jo.put("file", filename)
chapters.put(chapter.id.toString(), jo) chapters.put(chapter.value.id.toString(), jo)
} }
} }
@ -162,7 +165,8 @@ class MangaIndex(source: String?) {
id = k.toLong(), id = k.toLong(),
name = v.getString("name"), name = v.getString("name"),
url = v.getString("url"), url = v.getString("url"),
number = v.getInt("number"), number = v.getFloatOrDefault("number", 0f),
volume = v.getIntOrDefault("volume", 0),
uploadDate = v.getLongOrDefault("uploadDate", 0L), uploadDate = v.getLongOrDefault("uploadDate", 0L),
scanlator = v.getStringOrNull("scanlator"), scanlator = v.getStringOrNull("scanlator"),
branch = v.getStringOrNull("branch"), branch = v.getStringOrNull("branch"),

@ -71,7 +71,8 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
MangaChapter( MangaChapter(
id = "$i${f.name}".longHashCode(), id = "$i${f.name}".longHashCode(),
name = f.nameWithoutExtension.toHumanReadable(), name = f.nameWithoutExtension.toHumanReadable(),
number = i + 1, number = 0f,
volume = 0,
source = MangaSource.LOCAL, source = MangaSource.LOCAL,
uploadDate = f.creationTime, uploadDate = f.creationTime,
url = f.toUri().toString(), url = f.toUri().toString(),

@ -7,6 +7,7 @@ import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.util.AlphanumComparator
import org.koitharu.kotatsu.core.util.ext.longHashCode import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.core.util.ext.readText import org.koitharu.kotatsu.core.util.ext.readText
import org.koitharu.kotatsu.core.util.ext.toListSorted import org.koitharu.kotatsu.core.util.ext.toListSorted
@ -71,12 +72,13 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
publicUrl = fileUri, publicUrl = fileUri,
source = MangaSource.LOCAL, source = MangaSource.LOCAL,
coverUrl = zipUri(root, findFirstImageEntry(zip.entries())?.name.orEmpty()), coverUrl = zipUri(root, findFirstImageEntry(zip.entries())?.name.orEmpty()),
chapters = chapters.sortedWith(org.koitharu.kotatsu.core.util.AlphanumComparator()) chapters = chapters.sortedWith(AlphanumComparator())
.mapIndexed { i, s -> .mapIndexed { i, s ->
MangaChapter( MangaChapter(
id = "$i$s".longHashCode(), id = "$i$s".longHashCode(),
name = s.ifEmpty { title }, name = s.ifEmpty { title },
number = i + 1, number = 0f,
volume = 0,
source = MangaSource.LOCAL, source = MangaSource.LOCAL,
uploadDate = 0L, uploadDate = 0L,
url = uriBuilder.fragment(s).build().toString(), url = uriBuilder.fragment(s).build().toString(),
@ -126,7 +128,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
} }
} }
entries entries
.toListSorted(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name }) .toListSorted(compareBy(AlphanumComparator()) { x -> x.name })
.map { x -> .map { x ->
val entryUri = zipUri(file, x.name) val entryUri = zipUri(file, x.name)
MangaPage( MangaPage(
@ -142,7 +144,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
private fun findFirstImageEntry(entries: Enumeration<out ZipEntry>): ZipEntry? { private fun findFirstImageEntry(entries: Enumeration<out ZipEntry>): ZipEntry? {
val list = entries.toList() val list = entries.toList()
.filterNot { it.isDirectory } .filterNot { it.isDirectory }
.sortedWith(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name }) .sortedWith(compareBy(AlphanumComparator()) { x -> x.name })
val map = MimeTypeMap.getSingleton() val map = MimeTypeMap.getSingleton()
return list.firstOrNull { return list.firstOrNull {
map.getMimeTypeFromExtension(it.name.substringAfterLast('.')) map.getMimeTypeFromExtension(it.name.substringAfterLast('.'))

@ -4,7 +4,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.util.ext.deleteAwait import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.takeIfReadable import org.koitharu.kotatsu.core.util.ext.takeIfReadable
@ -47,12 +46,12 @@ class LocalMangaDirOutput(
flushIndex() flushIndex()
} }
override suspend fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String) = mutex.withLock { override suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, ext: String) = mutex.withLock {
val output = chaptersOutput.getOrPut(chapter) { val output = chaptersOutput.getOrPut(chapter.value) {
ZipOutput(File(rootFile, chapterFileName(chapter) + SUFFIX_TMP)) ZipOutput(File(rootFile, chapterFileName(chapter) + SUFFIX_TMP))
} }
val name = buildString { val name = buildString {
append(FILENAME_PATTERN.format(chapter.branch.hashCode(), chapter.number, pageNumber)) append(FILENAME_PATTERN.format(chapter.value.branch.hashCode(), chapter.index + 1, pageNumber))
if (ext.isNotEmpty() && ext.length <= 4) { if (ext.isNotEmpty() && ext.length <= 4) {
append('.') append('.')
append(ext) append(ext)
@ -92,9 +91,9 @@ class LocalMangaDirOutput(
} }
suspend fun deleteChapter(chapterId: Long) = mutex.withLock { suspend fun deleteChapter(chapterId: Long) = mutex.withLock {
val chapter = checkNotNull(index.getMangaInfo()?.chapters) { val chapter = checkNotNull(index.getMangaInfo()?.chapters?.withIndex()) {
"No chapters found" "No chapters found"
}.findById(chapterId) ?: error("Chapter not found") }.find { x -> x.value.id == chapterId } ?: error("Chapter not found")
val chapterDir = File(rootFile, chapterFileName(chapter)) val chapterDir = File(rootFile, chapterFileName(chapter))
chapterDir.deleteAwait() chapterDir.deleteAwait()
index.removeChapter(chapterId) index.removeChapter(chapterId)
@ -111,11 +110,11 @@ class LocalMangaDirOutput(
file.renameTo(resFile) file.renameTo(resFile)
} }
private fun chapterFileName(chapter: MangaChapter): String { private fun chapterFileName(chapter: IndexedValue<MangaChapter>): String {
index.getChapterFileName(chapter.id)?.let { index.getChapterFileName(chapter.value.id)?.let {
return it return it
} }
val baseName = "${chapter.number}_${chapter.name.toFileNameSafe()}".take(18) val baseName = "${chapter.index}_${chapter.value.name.toFileNameSafe()}".take(18)
var i = 0 var i = 0
while (true) { while (true) {
val name = (if (i == 0) baseName else baseName + "_$i") + ".cbz" val name = (if (i == 0) baseName else baseName + "_$i") + ".cbz"

@ -21,7 +21,7 @@ sealed class LocalMangaOutput(
abstract suspend fun addCover(file: File, ext: String) abstract suspend fun addCover(file: File, ext: String)
abstract suspend fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String) abstract suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, ext: String)
abstract suspend fun flushChapter(chapter: MangaChapter): Boolean abstract suspend fun flushChapter(chapter: MangaChapter): Boolean

@ -52,9 +52,9 @@ class LocalMangaZipOutput(
index.setCoverEntry(name) index.setCoverEntry(name)
} }
override suspend fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String) = mutex.withLock { override suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, ext: String) = mutex.withLock {
val name = buildString { val name = buildString {
append(FILENAME_PATTERN.format(chapter.branch.hashCode(), chapter.number, pageNumber)) append(FILENAME_PATTERN.format(chapter.value.branch.hashCode(), chapter.index + 1, pageNumber))
if (ext.isNotEmpty() && ext.length <= 4) { if (ext.isNotEmpty() && ext.length <= 4) {
append('.') append('.')
append(ext) append(ext)
@ -104,7 +104,7 @@ class LocalMangaZipOutput(
} }
} }
} }
otherIndex?.getMangaInfo()?.chapters?.let { chapters -> otherIndex?.getMangaInfo()?.chapters?.withIndex()?.let { chapters ->
for (chapter in chapters) { for (chapter in chapters) {
index.addChapter(chapter, null) index.addChapter(chapter, null)
} }

Loading…
Cancel
Save