Skip to content

Commit

Permalink
chore: add KDocs and integrate Koin for dependency injection
Browse files Browse the repository at this point in the history
  • Loading branch information
abdlhay committed Nov 14, 2024
1 parent c885ee7 commit 1f43b33
Show file tree
Hide file tree
Showing 15 changed files with 406 additions and 154 deletions.
89 changes: 89 additions & 0 deletions src/main/kotlin/com/abmo/Application.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.abmo

import com.abmo.common.Constants
import com.abmo.common.Logger
import com.abmo.model.Config
import com.abmo.services.ProviderDispatcher
import com.abmo.services.VideoDownloader
import com.abmo.util.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.parameter.parametersOf
import java.io.File
import kotlin.system.exitProcess

class Application(private val args: Array<String>): KoinComponent {

private val videoDownloader: VideoDownloader by inject()
private val providerDispatcher: ProviderDispatcher by inject()
private val cliArguments: CliArguments by inject { parametersOf(args) }

suspend fun run() {

val outputFileName = cliArguments.getOutputFileName()
val headers = cliArguments.getHeaders()
val numberOfConnections = cliArguments.getParallelConnections()
Constants.VERBOSE = cliArguments.isVerboseEnabled()

if (outputFileName != null && !isValidPath(outputFileName)) {
exitProcess(0)
}

val scanner = java.util.Scanner(System.`in`)


try {
println("Enter the video URL or ID (e.g., K8R6OOjS7):")
val videoUrl = scanner.nextLine()

val dispatcher = providerDispatcher.getProviderForUrl(videoUrl)
val videoID = dispatcher.getVideoID(videoUrl)

val defaultHeader = if (videoUrl.isValidUrl()) {
mapOf("Referer" to videoUrl?.extractReferer())
} else { emptyMap() }

val url = "https://abysscdn.com/?v=$videoID"
val videoMetadata = videoDownloader.getVideoMetaData(url, headers ?: defaultHeader)

val videoSources = videoMetadata?.sources
?.sortedBy { it?.label?.filter { char -> char.isDigit() }?.toIntOrNull() }

if (videoSources == null) {
Logger.error("Video with ID $videoID not found")
exitProcess(0)
}

// For some reason ANSI applies to rest of text in the terminal starting from here
// I'm not sure what causes that, so I removed all error logger here, and it still occurs
// the only solution for now is to reset ANSI before displaying the message
println("${Logger.RESET}Choose the resolution you want to download:")
videoSources
.forEachIndexed { index, video ->
println("${index + 1}] ${video?.label} - ${video?.size.formatBytes()}")
}

val choice = scanner.nextInt()
val resolution = videoSources[choice - 1]?.label


if (resolution != null) {

val defaultFileName = "${url.getParameter("v")}_${resolution}_${System.currentTimeMillis()}.mp4"
val outputFile = outputFileName?.let { File(it) } ?: run {
Logger.warn("No output file specified. The video will be saved to the current directory as '$defaultFileName'.\n")
File(".", defaultFileName) // Default directory and name for saving video
}

val config = Config(url, resolution, outputFile, headers, numberOfConnections)
Logger.info("video with id $videoID and resolution $resolution being processed...\n")
videoDownloader.downloadSegmentsInParallel(config, videoMetadata)
}
} catch (e: NoSuchElementException) {
println("\nCtrl + C detected. Exiting...")
}


}

}
52 changes: 33 additions & 19 deletions src/main/kotlin/com/abmo/CliArguments.kt
Original file line number Diff line number Diff line change
@@ -1,37 +1,43 @@
package com.abmo

import com.abmo.common.Constants.DEFAULT_CONCURRENT_DOWNLOAD_LIMIT
import com.abmo.common.Logger

/**
* A class for parsing command-line arguments.
*
* @param args The array of command-line arguments.
*/
class CliArguments(private val args: Array<String>) {

/**
* Extracts headers from command-line arguments in the format "--header key:value".
*
* @return A map of header names to their values, or null if no headers are found.
*/
fun getHeaders(): Map<String, String>? {
val headers = mutableMapOf<String, String>()
var i = 0

while (i < args.size) {
when (args[i]) {
"--header", "-H" -> {
if (i + 1 < args.size) {
val header = args[i + 1]
val parts = header.split(":", limit = 2)
if (parts.size == 2) {
val key = parts[0].trim()
val value = parts[1].trim()
headers[key] = value
} else {
println("Invalid header format. Use 'Header-Name: Header-Value'")
}
i += 1
}

for (i in args.indices) {
if (args[i] in arrayOf("--header", "-H") && i + 1 < args.size) {
val (key, value) = args[i + 1].split(":", limit = 2).map { it.trim() }
if (key.isNotEmpty() && value.isNotEmpty()) {
headers[key] = value
} else {
Logger.error("Invalid header format. Use 'Header-Name: Header-Value'")
}
}
i += 1
}

return headers.ifEmpty { null }
}

fun getOutputFileName(args: Array<String>): String? {
/**
* Retrieves the output file name from command-line arguments.
*
* @return The output file path as a String, or null if not specified.
*/
fun getOutputFileName(): String? {
val index = args.indexOf("-o")
if (index != -1 && index + 1 < args.size) {
val filePath = args[index + 1]
Expand All @@ -40,6 +46,12 @@ class CliArguments(private val args: Array<String>) {
return null
}

/**
* Retrieves the number of parallel connections from command-line arguments.
*
* @return The number of connections, constrained between 1 and 10.
* Returns the default value if not specified.
*/
fun getParallelConnections(): Int {
val maxConnections = 10
val minConnections = 1
Expand All @@ -54,4 +66,6 @@ class CliArguments(private val args: Array<String>) {
return DEFAULT_CONCURRENT_DOWNLOAD_LIMIT
}

fun isVerboseEnabled() = args.contains("--verbose")

}
86 changes: 4 additions & 82 deletions src/main/kotlin/com/abmo/Main.kt
Original file line number Diff line number Diff line change
@@ -1,88 +1,10 @@
package com.abmo

import com.abmo.common.Constants
import com.abmo.common.Logger
import com.abmo.model.Config
import com.abmo.services.ProviderDispatcher
import com.abmo.services.VideoDownloader
import com.abmo.crypto.CryptoHelper
import com.abmo.executor.JavaScriptExecutor
import com.abmo.util.*
import java.io.File
import kotlin.system.exitProcess
import com.abmo.di.koinModule
import org.koin.core.context.startKoin


suspend fun main(args: Array<String>) {

val javaScriptExecutor = JavaScriptExecutor()
val cryptoHelper = CryptoHelper(javaScriptExecutor)
val videoDownloader = VideoDownloader(cryptoHelper)
val cliArguments = CliArguments(args)
val providerDispatcher = ProviderDispatcher(javaScriptExecutor)


val outputFileName = cliArguments.getOutputFileName(args)
val headers = cliArguments.getHeaders()
val numberOfConnections = cliArguments.getParallelConnections()
Constants.VERBOSE = args.contains("--verbose")

if (outputFileName != null && !isValidPath(outputFileName)) {
exitProcess(0)
}

val scanner = java.util.Scanner(System.`in`)


try {
println("Enter the video URL or ID (e.g., K8R6OOjS7):")
val videoUrl = scanner.nextLine()

val dispatcher = providerDispatcher.getProviderForUrl(videoUrl)
val videoID = dispatcher.getVideoID(videoUrl)

val defaultHeader = if (videoUrl.isValidUrl()) {
mapOf("Referer" to videoUrl?.extractReferer())
} else { emptyMap() }

val url = "https://abysscdn.com/?v=$videoID"
val videoMetadata = videoDownloader.getVideoMetaData(url, headers ?: defaultHeader)

val videoSources = videoMetadata?.sources
?.sortedBy { it?.label?.filter { char -> char.isDigit() }?.toIntOrNull() }

if (videoSources == null) {
Logger.error("Video with ID $videoID not found")
exitProcess(0)
}

// For some reason ANSI applies to rest of text in the terminal starting from here
// I'm not sure what causes that, so I removed all error logger here, and it still occurs
// the only solution for now is to reset ANSI before displaying the message
println("${Logger.RESET}Choose the resolution you want to download:")
videoSources
.forEachIndexed { index, video ->
println("${index + 1}] ${video?.label} - ${formatBytes(video?.size)}")
}

val choice = scanner.nextInt()
val resolution = videoSources[choice - 1]?.label


if (resolution != null) {

val defaultFileName = "${url.getParameter("v")}_${resolution}_${System.currentTimeMillis()}.mp4"
val outputFile = outputFileName?.let { File(it) } ?: run {
Logger.warn("No output file specified. The video will be saved to the current directory as '$defaultFileName'.\n")
File(".", defaultFileName) // Default directory and name for saving video
}

val config = Config(url, resolution, outputFile, headers, numberOfConnections)
Logger.info("video with id $videoID and resolution $resolution being processed...\n")
videoDownloader.downloadSegmentsInParallel(config, videoMetadata)
}
} catch (e: NoSuchElementException) {
println("\nCtrl + C detected. Exiting...")
}


startKoin { modules(koinModule) }
Application(args).run()
}
10 changes: 9 additions & 1 deletion src/main/kotlin/com/abmo/common/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ package com.abmo.common

object Constants {

/**
* The default maximum number of concurrent downloads allowed.
*/
const val DEFAULT_CONCURRENT_DOWNLOAD_LIMIT = 4
var VERBOSE = false // Set to true for verbose logging, false to disable

/**
* Toggle for verbose logging.
* Set to `true` to enable detailed logs, `false` to disable.
*/
var VERBOSE = false

}
41 changes: 40 additions & 1 deletion src/main/kotlin/com/abmo/common/Logger.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ object Logger {
private const val CYAN = "\u001B[36m"
private const val GREEN = "\u001B[32m"

// Enable ANSI support on Windows CMD (for Windows 10+)
/**
* Enables ANSI color support on Windows Command Prompt (CMD) for Windows 10 and above.
*
* Checks if the operating system is Windows and, if so, attempts to enable ANSI escape
* code support for colored text output in CMD. This is done by running a command in CMD
* if a console is available.
*/
private fun enableAnsiOnWindows() {
if (System.getProperty("os.name").contains("Windows")) {
try {
Expand All @@ -25,29 +31,62 @@ object Logger {
}
}

/**
* Wraps the given text with an ANSI color code, applying the specified color.
*
* @param text The text to be colorized.
* @param colorCode The ANSI color code to apply to the text.
* @return The colorized text, with the color code prepended and a reset code appended.
*/
private fun colorize( text: String, colorCode: String): String {
return "$colorCode$text$RESET"
}

/**
* Prints an informational message in cyan.
*
* @param message The message to be printed as an informational log.
*/
fun info(message: String) {
println(colorize("INFO: $message", CYAN))
}

/**
* Prints a warning message in yellow.
*
* @param message The message to be printed as a warning log.
*/
fun warn(message: String) {
println(colorize("WARN: $message", YELLOW))
}

/**
* Prints an error message in red.
*
* @param message The message to be printed as an error log.
*/
fun error(message: String) {
println(colorize("ERROR: $message", RED))
}

/**
* Prints a debug message, color-coded based on whether it's an error or not, if verbose mode is enabled.
*
* @param message The message to be printed as a debug log.
* @param isError Indicates whether the debug message represents an error (applies red color if true).
*/
fun debug(message: String, isError: Boolean = false) {
if (Constants.VERBOSE) {
val debugTextColor = if (isError) { RED } else { PURPLE }
println(colorize("DEBUG: $message", debugTextColor))
}
}

/**
* Prints a success message in green.
*
* @param message The message to be printed as a success log.
*/
fun success(message: String) {
println(colorize("SUCCESS: $message", GREEN))
}
Expand Down
Loading

0 comments on commit 1f43b33

Please sign in to comment.