diff --git a/src/main/kotlin/org/koitharu/kotatsu/dl/Main.kt b/src/main/kotlin/org/koitharu/kotatsu/dl/Main.kt index 975b973..cc9e601 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/dl/Main.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/dl/Main.kt @@ -44,7 +44,7 @@ class Main : AppCommand(name = "kotatsu-dl") { private val parallelism: Int by option( names = arrayOf("-j", "--jobs"), help = "Number of parallel jobs for downloading", - ).int().default(1).check("Jobs count should be between 1 and 10") { + ).int().default(4).check("Jobs count should be between 1 and 10") { it in 1..10 } private val throttle: Boolean by option( diff --git a/src/main/kotlin/org/koitharu/kotatsu/dl/download/LocalMangaOutput.kt b/src/main/kotlin/org/koitharu/kotatsu/dl/download/LocalMangaOutput.kt index 76096b7..9041ef8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/dl/download/LocalMangaOutput.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/dl/download/LocalMangaOutput.kt @@ -1,8 +1,10 @@ package org.koitharu.kotatsu.dl.download +import com.github.ajalt.clikt.core.UsageError import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import okio.Closeable +import org.koitharu.kotatsu.dl.util.getNextAvailable import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.util.toFileNameSafe @@ -30,53 +32,72 @@ sealed class LocalMangaOutput( const val SUFFIX_TMP = ".tmp" suspend fun create( - target: File, + destination: File, manga: Manga, - format: DownloadFormat?, + preferredFormat: DownloadFormat?, ): LocalMangaOutput = runInterruptible(Dispatchers.IO) { - val targetFormat = format ?: if (manga.chapters.let { it != null && it.size <= 3 }) { - DownloadFormat.CBZ - } else { - DownloadFormat.DIR - } - var file = if (target.isDirectory || (!target.exists() && targetFormat == DownloadFormat.DIR)) { - if (!target.exists()) { - target.mkdirs() + when { + // option 0 - destination is a existing file/dir and we should write manga into id directly + destination.exists() && (destination.isFile || destination.isMangaDir()) -> { + TODO("Downloading into existing manga destination is not supported yet") } - val baseName = manga.title.toFileNameSafe() - when (targetFormat) { - DownloadFormat.CBZ -> File(target, "$baseName.cbz") - DownloadFormat.ZIP -> File(target, "$baseName.zip") - DownloadFormat.DIR -> File(target, baseName) + // option 1 - destination is an existing directory and we should create a nested dir/file for manga + destination.exists() && destination.isDirectory -> { + val baseName = manga.title.toFileNameSafe() + val format = preferredFormat ?: detectFormat(manga) + val targetFile = File( + destination, when (format) { + DownloadFormat.CBZ -> "$baseName.cbz" + DownloadFormat.ZIP -> "$baseName.zip" + DownloadFormat.DIR -> baseName + } + ) + createDirectly(targetFile.getNextAvailable(), manga, format) } - } else { - target.parentFile?.run { - if (!exists()) mkdirs() + // option 2 - destination is a non-existing file/dir and we should write manga into id directly + !destination.exists() -> { + val parentDir: File? = destination.parentFile + parentDir?.mkdirs() + createDirectly(destination, manga, preferredFormat ?: detectFormat(destination)) } - target + + else -> throw UsageError( + message = "Unable to determine destination file or directory. Please specify it explicitly", + paramName = "--destination" + ) } - getNextAvailable(file, manga) } - private fun getNextAvailable( - file: File, - manga: Manga, - ): LocalMangaOutput { - var i = 0 - val baseName = file.nameWithoutExtension - val ext = file.extension.let { if (it.isNotEmpty()) ".$it" else "" } - while (true) { - val fileName = (if (i == 0) baseName else baseName + "_$i") + ext - val target = File(file.parentFile, fileName) - if (target.exists()) { - i++ - } else { - return when { - target.isDirectory -> LocalMangaDirOutput(target, manga) - else -> LocalMangaZipOutput(target, manga) - } - } + private fun createDirectly(destination: File, manga: Manga, format: DownloadFormat) = when (format) { + DownloadFormat.CBZ, + DownloadFormat.ZIP, + -> LocalMangaZipOutput(destination, manga) + + DownloadFormat.DIR -> LocalMangaDirOutput(destination, manga) + } + + private fun detectFormat(destination: File): DownloadFormat { + return when (destination.extension.lowercase()) { + "cbz" -> return DownloadFormat.CBZ + "zip" -> return DownloadFormat.ZIP + "" -> return DownloadFormat.DIR + else -> throw UsageError( + message = "Unable to determine output format. Please specify it explicitly", + paramName = "--format" + ) } } + + private fun detectFormat(manga: Manga): DownloadFormat { + return if (manga.chapters.let { it != null && it.size <= 5 }) { + DownloadFormat.CBZ + } else { + DownloadFormat.DIR + } + } + + private fun File.isMangaDir(): Boolean { + return list()?.contains("index.json") == true + } } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/dl/util/AppCommand.kt b/src/main/kotlin/org/koitharu/kotatsu/dl/util/AppCommand.kt index aa210a6..b2fd35b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/dl/util/AppCommand.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/dl/util/AppCommand.kt @@ -2,10 +2,10 @@ package org.koitharu.kotatsu.dl.util import com.github.ajalt.clikt.command.CoreSuspendingCliktCommand import com.github.ajalt.clikt.core.FileNotFound +import com.github.ajalt.clikt.core.PrintMessage import com.github.ajalt.clikt.core.ProgramResult import com.github.ajalt.clikt.core.context import okio.IOException -import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import kotlin.io.path.Path import kotlin.io.path.readText @@ -36,11 +36,13 @@ abstract class AppCommand(name: String) : CoreSuspendingCliktCommand(name) { } final override suspend fun run() { - val exitCode = runCatchingCancellable { + val exitCode = try { invoke() - }.onFailure { e -> - e.printStackTrace() - }.getOrDefault(1) + } catch (e: IllegalStateException) { + throw PrintMessage(e.message.ifNullOrEmpty { GENERIC_ERROR_MSG }, 2, true) + } catch (e: NotImplementedError) { + throw PrintMessage(e.message.ifNullOrEmpty { GENERIC_ERROR_MSG }, 2, true) + } throw ProgramResult(exitCode) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/dl/util/Util.kt b/src/main/kotlin/org/koitharu/kotatsu/dl/util/Util.kt index 548edbf..8d7087a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/dl/util/Util.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/dl/util/Util.kt @@ -5,8 +5,11 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.Response import okhttp3.internal.closeQuietly import org.jsoup.HttpStatusException +import java.io.File import java.net.HttpURLConnection +const val GENERIC_ERROR_MSG = "An error has occured" + @Suppress("NOTHING_TO_INLINE") inline operator fun List.component6(): T = get(5) @@ -32,4 +35,19 @@ fun IntList.sum(): Int { var result = 0 forEach { value -> result += value } return result -} \ No newline at end of file +} + +fun File.getNextAvailable(): File { + var i = 0 + val baseName = nameWithoutExtension + val ext = extension.let { if (it.isNotEmpty()) ".$it" else "" } + while (true) { + val fileName = (if (i == 0) baseName else baseName + "_$i") + ext + val target = File(this.parentFile, fileName) + if (target.exists()) { + i++ + } else { + return target + } + } +}