Springer book downloader in Kotlin I'm starting with Kotlin recently and am hoping to improve.

I wrote a small app to parse the list of free Springer books and download the books into your chosen local folder.

Comments around obvious mistakes, unidiomatic Kotlin, any other points of improvements will be greatly appreciated. Thank you.

Gradle dependencies:

    implementation("org.apache.poi:poi:4.1.2")
    implementation("org.apache.poi:poi-ooxml:4.1.2")
    implementation("org.jsoup:jsoup:1.13.1")


Kotlin code:

package dev.rayfdj.kotlinutils.springer

import org.apache.poi.ss.usermodel.WorkbookFactory
import org.jsoup.Jsoup
import java.io.File
import java.io.FileOutputStream
import java.net.URL
import java.nio.channels.Channels
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

data class Book(val title: String, val author: String, val edition: String, val year: String,
                val category: String, val url: String) {
    fun suggestedFileName(): String { return "$title, $edition - $author.pdf" }
}

fun extractBooksFromExcelFile(xlsxFile: File): List&lt;Book&gt; {
    // drop(1): skip the first row because it contains the headers
    return WorkbookFactory.create(xlsxFile).getSheetAt(0).drop(1).map {
        Book(it.getCell(0).stringCellValue, it.getCell(1).stringCellValue,
                it.getCell(2).stringCellValue, it.getCell(4).numericCellValue.toString(),
                it.getCell(11).stringCellValue, it.getCell(18).stringCellValue)
    }
}

fun deriveFullLocalPathForBook(downloadFolder: String, book: Book): Path {
    val fullLocalFileName = arrayOf(
            downloadFolder,
            book.category,
            book.suggestedFileName()).joinToString(separator = File.separator)
    return Paths.get(fullLocalFileName)
}

fun createDirectoriesAndFile(fullLocalFilePath: Path) {
    Files.createDirectories(fullLocalFilePath.parent)
    if(!Files.exists(fullLocalFilePath)) { Files.createFile(fullLocalFilePath) }
}

fun getBookDownloadURL(book: Book): URL {
    val bookPage = Jsoup.connect(book.url).get()
    val bookCanonicalURL = bookPage.select("link[rel=canonical]").attr("href")

    val bookCanonicalPage = Jsoup.connect(bookCanonicalURL).get()
    val bookPDFRelativeURL = bookCanonicalPage.select("a[href^=\"/content/pdf/\"]").attr("href")

    return URL("https://link.springer.com${bookPDFRelativeURL}")
}

fun downloadAndSaveBook(bookDownloadURL: URL, fullLocalFilePath: Path) {
    Channels.newChannel(bookDownloadURL.openStream()).use { inChannel -&gt;
        FileOutputStream(fullLocalFilePath.toFile()).channel.use { outChannel -&gt;
            print("Saving $bookDownloadURL to $fullLocalFilePath... ")
            outChannel.transferFrom(inChannel, 0, Long.MAX_VALUE)
            println("DONE.")
        }
    }
}

fun main(args: Array&lt;String&gt;) {
    if(args.size != 2) {
        println("Please pass &lt;full_path_to_springer_excel_file&gt; and &lt;full_path_to_download_folder")
        kotlin.system.exitProcess(-1)
    }
    val (excelFile, downloadFolder) = args

    val books = extractBooksFromExcelFile(File(excelFile))

    books.forEach { book -&gt;
        val fullLocalFilePath = deriveFullLocalPathForBook(downloadFolder, book)
        createDirectoriesAndFile(fullLocalFilePath)
        val bookDownloadURL = getBookDownloadURL(book)
        downloadAndSaveBook(bookDownloadURL, fullLocalFilePath)
    }
}
It's not exact translation of your code, but still :) I've just tried!

Unfortunately, it's not a github project, so I don't have an access to the excel sheet, so I can't really run and test it. But, anyways, ideas are:


Utilization of toString() java method 
Using of lazy properties allows to 'cache' the URL easily. It's also possible because all of the Books properties are immutable.
Extension method download() doesn't really belong to a book, but, instead,
can easily download any URL
I used require(), though, it's not really correct, since it throws an exception, instead of gracefully exiting the app. I used it, just to demonstrate it, nothing more.


And, probably, something else. Please, tell me what you're thinking!

data class Book(
    val title: String,
    val author: String,
    val edition: String,
    val year: String,
    val category: String,
    val url: String
) {
    constructor(row: Row) : this(
        row.getCell(0).stringCellValue, row.getCell(1).stringCellValue,
        row.getCell(2).stringCellValue, row.getCell(4).numericCellValue.toString(),
        row.getCell(11).stringCellValue, row.getCell(18).stringCellValue
    )

    override fun toString() = "$title, $edition - $author"

    val downloadURL by lazy {
        val canonicalURL = Jsoup.connect(url).get().select("link[rel=canonical]").attr("href")
        val pdfRelativeURL =
            Jsoup.connect(canonicalURL).get().select("""a[href^="/content/pdf/"]""").attr("href")
        URL("https://link.springer.com${pdfRelativeURL}")
    }
}

fun URL.download(to: Path): Path {
    Files.createDirectories(to.parent)

    Channels.newChannel(openStream()).use { inChannel -&gt;
        FileOutputStream(to.toFile()).channel.use { outChannel -&gt;
            print("Saving $this to $to... ")
            outChannel.transferFrom(inChannel, 0, Long.MAX_VALUE)
            println("DONE.")
        }
    }

    return to
}

fun main(args: Array&lt;String&gt;) {
    require(args.size == 2) { "Please pass &lt;full_path_to_springer_excel_file&gt; and &lt;full_path_to_download_folder" }

    val (excelPath, downloadFolder) = args
    val excelFile = WorkbookFactory.create(File(excelPath))

    // drop(1): skip the first row because it contains the headers
    val books = excelFile.getSheetAt(0).drop(1).map { Book(it) }

    books.map { book -&gt;
        val path = Paths.get(downloadFolder, book.category, "$book.pdf")
        book.downloadURL.download(to = path)
    }
}
```