Theme.kt
/*
Copyright 2016 Hermann Krumrey <hermann@krumreyh.com>
This file is part of anitheme-dl.
anitheme-dl is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
anitheme-dl is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with anitheme-dl. If not, see <http://www.gnu.org/licenses/>.
*/
package net.namibsun.anitheme.dl.lib.parsing
import net.namibsun.anitheme.dl.lib.utils.createDirectoryIfNotExists
import java.io.File
import java.io.IOException
import java.net.URL
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import java.util.logging.Logger
import javax.net.ssl.SSLException
import kotlin.concurrent.thread
/**
* The Theme class models an anime theme song indexed on [themes.moe](https://themes.moe).
*
* It consists of a short description as displayed on [themes.moe](https://themes.moe),
* as well as a URL to the video file of the theme song
*
* @param description The description of the theme song
* @param url The URL to the theme song file
*/
class Theme constructor(val description: String, val url: String) {
val logger: Logger = Logger.getLogger("Theme")
/**
* Instance variable that is set to true whenever a download is progressing.
*/
var downloading = false
/**
* Downloads the theme song file to the specified target directory using an automatic naming scheme
*
* The description is used as the file name, a suffix and prefix may also be supplied
* The downloader can automatically convert the file into different file types. By default,
* only the original .webm file is kept
*
* This method only generates a filename based off the information provided, then delegates
* the file download and conversion to the [handleDownload] method
*
* @param targetDir The target directory in which the file should be saved
* @param fileTypes The filetypes to convert the file into. Defaults to only .webm
* @param prefix An optional prefix for the generated file name
* @param suffix An optional suffix for the generated file name
* @param retriesAllowed The amount of times the download should be retried before giving up.
* @throws IOException If an IO error occurs (for example, the URL is invalid)
* @throws FileSystemException If the directory could not be created
*/
fun download(
targetDir: String,
fileTypes: List<FileTypes> = listOf(FileTypes.WEBM),
prefix: String = "",
suffix: String = "",
retriesAllowed: Int = 0
) {
createDirectoryIfNotExists(targetDir)
val filepath = Paths.get(targetDir, "$prefix${this.description}$suffix").toString()
this.handleDownload(filepath, fileTypes, retriesAllowed)
}
/**
* Handles the different cases of the existing file structure's state and delegates
* downloading and converting.
*
* The [fileTypes] parameter specifies which media formats the file should be
* converted to. By default, only the original .webm file is downloaded and left as-is.
*
* @param targetFile The file name of the target file, without any file type associated suffixes like .webm etc.
* @param fileTypes The types of media files to convert the theme song into once downloaded
* @param retriesAllowed The amount of times the download should be retried before giving up.
* @throws IOException if something happened during the download
*/
fun handleDownload(
targetFile: String,
fileTypes: List<FileTypes> = listOf(FileTypes.WEBM),
retriesAllowed: Int = 0
) {
val extension = this.url.split(".").last()
val target = "$targetFile.$extension"
if (File(target).isFile) {
logger.info { "$target exists. Skipping download." }
} else if (this.url.startsWith("https://streamable.com")) {
logger.info { "${this.url} is on streamable.com, which is not supported. Skipping download." }
return // No conversion at end since file does not exist, hence we return
} else {
this.logger.info { "Downloading ${this.url} to $target" }
this.downloadFile(target, retriesAllowed)
}
this.handleConversion(targetFile, target, fileTypes)
}
/**
* Downloads the file from the URL to a local file.
* Starts a separate thread that prints the current progress to the terminal
* Handles retries of downloads
* @param target The target file of the download in the local file system
* @param retriesAllowed The amount of retries that are allowed
*/
fun downloadFile(target: String, retriesAllowed: Int = 0) {
var url = URL(this.url)
var httpConnection = url.openConnection()
httpConnection.addRequestProperty("User-Agent", "Mozilla/4.0")
this.downloading = true
var retryCount = 0
this.printDownloadProgress(target, httpConnection.contentLength)
while (this.downloading) {
try {
val data = httpConnection.inputStream
Files.copy(data, Paths.get(target), StandardCopyOption.REPLACE_EXISTING)
this.logger.info { "Download completed" }
this.downloading = false
} catch (e: Exception) {
this.handleExceptionInDownload(e, target, retryCount++, retriesAllowed)
// Re-establish connection
url = URL(this.url)
httpConnection = url.openConnection()
httpConnection.addRequestProperty("User-Agent", "Mozilla/4.0")
}
}
}
/**
* Handles an exception caught in the [downloadFile] method
* Allows for retries, if the maximum amount of retries is exceeded, throws the exception again, after
* aborting the download.
* @param exception The caught exception
* @param target The target file to which is being downloaded
* @param retries The amount of retries already attempted
* @param maxRetries The maximum amount of retries allowed
*/
fun handleExceptionInDownload(exception: Exception, target: String, retries: Int, maxRetries: Int) {
this.logger.severe { "Download of file ${this.url} failed" }
when (exception) {
is IOException,
is SSLException -> {
if (retries > maxRetries) {
this.logger.severe("Maximum number of retries attempted.")
this.abortDownload(target)
throw exception
} else {
this.logger.info("Retrying download...")
}
} else -> {
this.logger.severe("Unknown error occurred.")
this.logger.fine(exception.toString())
this.abortDownload(target)
throw exception
}
}
}
/**
* Aborts the download. Resets the [downloading] flag and deletes the target file
* @param target The target file to delete on abort
*/
fun abortDownload(target: String) {
this.downloading = false
this.logger.severe("Aborted Download of $target")
if (File(target).exists()) {
File(target).delete()
}
}
/**
* Starts a new thread which continuously prints the current progress to the terminal
* @param target The target file. Used to check the current size
* @param size The total size of the download target in bytes
*/
fun printDownloadProgress(target: String, size: Int) {
thread(start = true) {
val downloadFile = File(target)
while (this.downloading) {
print("Progress: ${downloadFile.length() / 1000} KB / ${size / 1000} KB\r")
Thread.sleep(500)
}
}
}
/**
* Handles the conversion of the WEBM files to the various specified formats
* @param name The name of the downloaded file without any extension
* @param webmFile the path to the original webm file
* @param fileTypes The file types to which to convert to
*/
fun handleConversion(name: String, webmFile: String, fileTypes: List<FileTypes>) {
fileTypeLoop@ for (fileType in fileTypes) {
when (fileType) {
FileTypes.WEBM -> continue@fileTypeLoop
FileTypes.MP3 -> convertToMP3(name, webmFile)
}
}
if (FileTypes.WEBM !in fileTypes && File(webmFile).exists()) {
this.logger.info { "Deleting original .webm file" }
File(webmFile).delete()
}
}
/**
* Converts a .webm file to mp3 using ffmpeg
* @param name: The filename of the resulting mp3 file
* @param webm: The path to the source .webm file
*/
private fun convertToMP3(name: String, webm: String) {
if (!File(name + ".mp3").exists()) {
logger.info { "Converting $name to MP3." }
Runtime.getRuntime().exec(arrayOf("ffmpeg", "-i", webm, "-acodec", "libmp3lame", "-aq", "4", name + ".mp3"))
.waitFor()
} else {
logger.fine("MP3 File exists, skipping: ${name + ".mp3"}")
}
}
/**
* Formats the [Theme] object for printing to the console
*
* This is done in the following format:
*
* Description: URL
*
* @return The formatted String
*/
override fun toString(): String {
return "${this.description}: ${this.url}"
}
}