├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── LICENSE.txt ├── README.md ├── build.gradle ├── screenshots └── MainView.png ├── settings.gradle ├── src ├── main │ ├── deploy │ │ ├── engines.json │ │ └── package │ │ │ └── windows │ │ │ ├── VDM-setup-icon.bmp │ │ │ ├── VDM.ico │ │ │ └── engines │ │ │ └── README.txt │ ├── kotlin │ │ └── com │ │ │ └── ingbyr │ │ │ └── vdm │ │ │ ├── Main.kt │ │ │ ├── controllers │ │ │ ├── CreateDownloadTaskController.kt │ │ │ ├── MainController.kt │ │ │ ├── MediaFormatsController.kt │ │ │ ├── PreferencesController.kt │ │ │ ├── TextEditorController.kt │ │ │ ├── ThemeController.kt │ │ │ └── WizardController.kt │ │ │ ├── dao │ │ │ ├── DownloadTaskDAO.kt │ │ │ ├── DownloadTaskTable.kt │ │ │ ├── TaskConfigDAO.kt │ │ │ └── TaskConfigTable.kt │ │ │ ├── engines │ │ │ ├── AbstractEngine.kt │ │ │ ├── Annie.kt │ │ │ ├── YoutubeDL.kt │ │ │ └── utils │ │ │ │ ├── EngineDownloadType.kt │ │ │ │ ├── EngineException.kt │ │ │ │ ├── EngineFactory.kt │ │ │ │ └── EngineType.kt │ │ │ ├── events │ │ │ ├── DownloadTaskEvents.kt │ │ │ ├── RefreshUIEvents.kt │ │ │ └── RestorePreferencesViewEvent.kt │ │ │ ├── models │ │ │ ├── DownloadTaskModel.kt │ │ │ ├── DownloadTaskStatus.kt │ │ │ ├── DownloadTaskType.kt │ │ │ ├── MediaFormat.kt │ │ │ └── TaskConfig.kt │ │ │ ├── stylesheets │ │ │ ├── DarkTheme.kt │ │ │ └── LightTheme.kt │ │ │ ├── utils │ │ │ ├── Attributes.kt │ │ │ ├── DBUtils.kt │ │ │ ├── DateTimeUtils.kt │ │ │ ├── DebugUtils.kt │ │ │ ├── FileCompressUtils.kt │ │ │ ├── FileEditorOption.kt │ │ │ ├── NetUtils.kt │ │ │ ├── OSUtils.kt │ │ │ ├── UpdateUtils.kt │ │ │ └── config │ │ │ │ └── ConfigUtils.kt │ │ │ └── views │ │ │ ├── AboutView.kt │ │ │ ├── CreateDownloadTaskView.kt │ │ │ ├── DonationView.kt │ │ │ ├── MainView.kt │ │ │ ├── MediaFormatsView.kt │ │ │ ├── PreferencesView.kt │ │ │ ├── TextEditorView.kt │ │ │ └── WizardView.kt │ └── resources │ │ ├── fxml │ │ ├── AboutView.fxml │ │ ├── CreateDownloadTaskView.fxml │ │ ├── FileEditorView.fxml │ │ ├── MainView.fxml │ │ ├── MediaFormatsView.fxml │ │ └── PreferencesView.fxml │ │ ├── i18n │ │ ├── AboutView.properties │ │ ├── AboutView_en.properties │ │ ├── AboutView_hu_HU.properties │ │ ├── AboutView_zh_CN.properties │ │ ├── AboutView_zh_TW.properties │ │ ├── CreateDownloadTaskView.properties │ │ ├── CreateDownloadTaskView_en.properties │ │ ├── CreateDownloadTaskView_hu_HU.properties │ │ ├── CreateDownloadTaskView_zh_CN.properties │ │ ├── CreateDownloadTaskView_zh_TW.properties │ │ ├── FileEditorView.properties │ │ ├── FileEditorView_en.properties │ │ ├── FileEditorView_hu_HU.properties │ │ ├── FileEditorView_zh_CN.properties │ │ ├── FileEditorView_zh_TW.properties │ │ ├── MainView.properties │ │ ├── MainView_en.properties │ │ ├── MainView_hu_HU.properties │ │ ├── MainView_zh_CN.properties │ │ ├── MainView_zh_TW.properties │ │ ├── MediaFormatsView.properties │ │ ├── MediaFormatsView_en.properties │ │ ├── MediaFormatsView_hu_HU.properties │ │ ├── MediaFormatsView_zh_CN.properties │ │ ├── MediaFormatsView_zh_TW.properties │ │ ├── PreferencesView.properties │ │ ├── PreferencesView_en.properties │ │ ├── PreferencesView_hu_HU.properties │ │ ├── PreferencesView_zh_CN.properties │ │ └── PreferencesView_zh_TW.properties │ │ ├── imgs │ │ ├── logo.jpg │ │ ├── paypal-min.png │ │ └── zhifubao-min.png │ │ └── logback.xml └── test │ └── kotlin │ └── com │ └── ingbyr │ └── vdm │ ├── dao │ └── DownloadTaskDAOTests.kt │ ├── engines │ └── AnnieTests.kt │ └── utils │ └── FileCompressUtilsTests.kt └── translate.sh /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please make sure these boxes are checked before submitting your issue – thank you! 2 | 3 | - [ ] You can actually watch the video in your browser, but not download them with `VDM`. 4 | - [ ] Your `VDM` is up-to-date. 5 | - [ ] Your downloading engine is up-to-date. 6 | 7 | Please paste the full `%HOMEPATH%\.vdm\log\debug.log` inside the fences: 8 | 9 | ``` 10 | [PASTE IN ME] 11 | ``` 12 | 13 | If there's anything else you would like to say(e.g some new features) fill in the box below 14 | > [WRITE SOMETHING] 15 | 16 | 在提交前,请确保您已经检查了以下内容! 17 | 18 | - [ ] 你可以在浏览器中观看视频,但不能使用`VDM`下载。 19 | - [ ] 您的`VDM`为最新版。 20 | - [ ] 您的下载引擎为最新版。 21 | 22 | 请将`%HOMEPATH%\.vdm\log\debug.log`文件内容粘贴在下面: 23 | 24 | ``` 25 | [在这里粘贴完整日志] 26 | ``` 27 | 28 | 如果您有其他留言(如需要新特性等),请在下面添加: 29 | > [您的留言] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | .idea/ 3 | out/ 4 | log/ 5 | /VDM.iml 6 | /package 7 | /db 8 | /configs 9 | /cookies 10 | /tmp 11 | /engines 12 | /.DS_Store 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ============================================== 2 | This is a copy of the MIT license. 3 | ============================================== 4 | Copyright (C) 2016-2018 ingbyr 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | of the Software, and to permit persons to whom the Software is furnished to do 11 | so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Video Download Manager 2 | ![](https://img.shields.io/badge/version-0.4.0-green.svg) 3 | ![](https://img.shields.io/github/stars/ingbyr/VDM.svg) 4 | ![](https://img.shields.io/github/forks/ingbyr/VDM.svg) 5 | ![](https://img.shields.io/github/issues/ingbyr/VDM.svg) 6 | ![](https://img.shields.io/badge/license-MIT-blue.svg) 7 | 8 | 9 | # Download 10 | - Install the [JRE8](http://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html) first (If you have installed the JRE, skip this step). 11 | - Download the [VDM.zip](https://github.com/ingbyr/VDM/releases), unzip it and then click the `VDM.jar` file 12 | 13 | 14 | # Screenshots 15 | ![](screenshots/MainView.png) 16 | 17 | 18 | # Feedback Bugs 19 | If something is broken and `VDM` can't get you things you want, don't panic. 20 | Open a new issue on [GitHub](https://github.com/ingbyr/VDM/issues), with detailed `%HOMEPATH%\.vdm\log\debug.log` output attached. 21 | 22 | 23 | # Legal Issues 24 | This software is distributed under the [MIT license](https://raw.githubusercontent.com/ingbyr/VDM/master/LICENSE.txt). 25 | In particular, please be aware that 26 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | SOFTWARE. 33 | 34 | 35 | # Contributors 36 | - gojko71: Hungarian Language Support 37 | - david082321: Traditional Chinese Support 38 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | group = "com.ingbyr.vdm" 2 | version = "0.4.0" 3 | 4 | buildscript { 5 | ext.kotlin_version = "1.3.10" 6 | 7 | repositories { 8 | maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } 9 | } 10 | dependencies { 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | classpath "de.dynamicfiles.projects.gradle.plugins:javafx-gradle-plugin:8.8.2" 13 | } 14 | } 15 | 16 | apply plugin: "kotlin" 17 | apply plugin: "javafx-gradle-plugin" 18 | 19 | sourceCompatibility = 1.8 20 | targetCompatibility = 1.8 21 | 22 | repositories { 23 | maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } 24 | maven { url "https://dl.bintray.com/kotlin/exposed" } 25 | } 26 | 27 | dependencies { 28 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 29 | implementation "no.tornado:tornadofx:1.7.18" 30 | implementation "com.jfoenix:jfoenix:8.0.8" 31 | implementation "ch.qos.logback:logback-core:1.2.3" 32 | implementation "ch.qos.logback:logback-classic:1.2.3" 33 | implementation "com.squareup.okhttp3:okhttp:3.10.0" 34 | implementation "com.squareup.okio:okio:1.14.0" 35 | implementation "com.h2database:h2:1.4.197" 36 | implementation "org.jetbrains.exposed:exposed:0.11.2" 37 | implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.4.1" 38 | implementation "org.apache.commons:commons-compress:1.18" 39 | implementation "commons-io:commons-io:2.6" 40 | testImplementation('org.junit.jupiter:junit-jupiter-api:5.2.0') 41 | testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.2.0') 42 | 43 | } 44 | 45 | compileKotlin { 46 | kotlinOptions.jvmTarget = "1.8" 47 | } 48 | 49 | compileTestKotlin { 50 | kotlinOptions.jvmTarget = "1.8" 51 | } 52 | 53 | test { 54 | useJUnitPlatform() 55 | } 56 | 57 | jfx { 58 | verbose = false 59 | mainClass = "com.ingbyr.vdm.MainKt" 60 | jfxMainAppJarName = "VDM.jar" 61 | vendor = "ingbyr" 62 | appName = "VDM" 63 | additionalAppResources = "src/main/deploy" 64 | skipMainClassScanning = true 65 | jvmProperties = ["-Dfile.encoding": "UTF-8"] 66 | nativeReleaseVersion = "0.4.0" 67 | bundler = "exe" 68 | needShortcut = true 69 | bundleArguments = [ 70 | installdirChooser: "true" 71 | ] 72 | } -------------------------------------------------------------------------------- /screenshots/MainView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ingbyr/vdm/98b8246fb73e3811e37714ffcbc2f7299002b870/screenshots/MainView.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'VDM' 2 | 3 | -------------------------------------------------------------------------------- /src/main/deploy/engines.json: -------------------------------------------------------------------------------- 1 | {"windows":[{"name":"youtube-dl","path":"windows\\engines\\youtube-dl.exe","version":"2018.08.22","remoteVersionUrl":"https://raw.githubusercontent.com/rg3/youtube-dl/master/youtube_dl/version.py"},{"name":"annie","path":"windows\\engines\\annie.exe","version":"0.7.18","remoteVersionUrl":"https://raw.githubusercontent.com/iawia002/annie/master/config/version.go"}],"linux":[{"name":"youtube-dl","path":"youtube-dl","version":"2018.08.04","remoteVersionUrl":"https://raw.githubusercontent.com/rg3/youtube-dl/master/youtube_dl/version.py"},{"name":"annie","path":"annie","version":"0.7.16","remoteVersionUrl":"https://raw.githubusercontent.com/iawia002/annie/master/config/version.go"}],"macos":[{"name":"youtube-dl","path":"youtube-dl","version":"2018.08.04","remoteVersionUrl":"https://raw.githubusercontent.com/rg3/youtube-dl/master/youtube_dl/version.py"},{"name":"annie","path":"annie","version":"0.7.16","remoteVersionUrl":"https://raw.githubusercontent.com/iawia002/annie/master/config/version.go"}]} -------------------------------------------------------------------------------- /src/main/deploy/package/windows/VDM-setup-icon.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ingbyr/vdm/98b8246fb73e3811e37714ffcbc2f7299002b870/src/main/deploy/package/windows/VDM-setup-icon.bmp -------------------------------------------------------------------------------- /src/main/deploy/package/windows/VDM.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ingbyr/vdm/98b8246fb73e3811e37714ffcbc2f7299002b870/src/main/deploy/package/windows/VDM.ico -------------------------------------------------------------------------------- /src/main/deploy/package/windows/engines/README.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ingbyr/vdm/98b8246fb73e3811e37714ffcbc2f7299002b870/src/main/deploy/package/windows/engines/README.txt -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/Main.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm 2 | 3 | import com.ingbyr.vdm.utils.Attributes 4 | import com.ingbyr.vdm.views.MainView 5 | import javafx.application.Application 6 | import javafx.scene.image.Image 7 | import javafx.stage.Stage 8 | import tornadofx.* 9 | import java.nio.file.Path 10 | import java.util.* 11 | 12 | class MainApp : App(MainView::class) { 13 | override val configPath: Path = Attributes.configFilePath 14 | private val availableLanguages = listOf("zh", "en", "hu") 15 | 16 | init { 17 | Locale.setDefault(Locale("en", "")) 18 | val language = Locale.getDefault().language 19 | if (language !in availableLanguages) { 20 | Locale.setDefault(Locale("en", "US")) 21 | } 22 | } 23 | 24 | override fun start(stage: Stage) { 25 | super.start(stage) 26 | addStageIcon(Image("/imgs/logo.jpg")) 27 | } 28 | } 29 | 30 | 31 | fun main(args: Array) { 32 | Application.launch(MainApp::class.java, *args) 33 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/controllers/CreateDownloadTaskController.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.controllers 2 | 3 | import com.ingbyr.vdm.engines.utils.EngineType 4 | import com.ingbyr.vdm.models.DownloadTaskModel 5 | import com.ingbyr.vdm.models.DownloadTaskType 6 | import com.ingbyr.vdm.models.ProxyType 7 | import com.ingbyr.vdm.models.TaskConfig 8 | import com.ingbyr.vdm.utils.Attributes 9 | import com.ingbyr.vdm.utils.config.proxy 10 | import tornadofx.* 11 | 12 | class CreateDownloadTaskController : Controller() { 13 | 14 | val downloadDefaultFormat = 15 | app.config.boolean(Attributes.DOWNLOAD_DEFAULT_FORMAT, Attributes.Defaults.DOWNLOAD_DEFAULT_FORMAT) 16 | 17 | fun createDownloadTaskInstance(url: String): DownloadTaskModel { 18 | val engineType = EngineType.valueOf(app.config.string(Attributes.ENGINE_TYPE, EngineType.YOUTUBE_DL.name)) 19 | val storagePath = app.config.string(Attributes.STORAGE_PATH, Attributes.Defaults.STORAGE_PATH) 20 | 21 | val ffmpeg = app.config.string(Attributes.FFMPEG_PATH, Attributes.Defaults.FFMPEG_PATH) 22 | val cookie = "" // TODO support cookie 23 | 24 | val taskConfig = TaskConfig( 25 | url, engineType, DownloadTaskType.SINGLE_MEDIA, 26 | downloadDefaultFormat, storagePath, cookie, ffmpeg 27 | ) 28 | 29 | val proxyType = app.config.proxy(Attributes.PROXY_TYPE) 30 | when (proxyType) { 31 | ProxyType.SOCKS5 -> { 32 | taskConfig.proxy( 33 | proxyType, 34 | app.config.proxy(Attributes.SOCKS5_PROXY_ADDRESS).name, 35 | app.config.proxy(Attributes.SOCKS5_PROXY_PORT).name 36 | ) 37 | } 38 | ProxyType.HTTP -> { 39 | taskConfig.proxy( 40 | proxyType, 41 | app.config.string(Attributes.SOCKS5_PROXY_ADDRESS, Attributes.Defaults.SOCKS5_PROXY_ADDRESS), 42 | app.config.string(Attributes.SOCKS5_PROXY_PORT, Attributes.Defaults.SOCKS5_PROXY_PORT) 43 | ) 44 | } 45 | ProxyType.NONE -> { 46 | taskConfig.proxy(proxyType, "", "") 47 | } 48 | } 49 | return DownloadTaskModel(taskConfig) 50 | } 51 | 52 | private fun prepareCookieFile() { 53 | // cookie 列表 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/controllers/MainController.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.controllers 2 | 3 | import com.ingbyr.vdm.engines.AbstractEngine 4 | import com.ingbyr.vdm.engines.utils.EngineFactory 5 | import com.ingbyr.vdm.engines.utils.EngineType 6 | import com.ingbyr.vdm.events.CreateDownloadTask 7 | import com.ingbyr.vdm.events.RefreshEngineVersion 8 | import com.ingbyr.vdm.events.RestorePreferencesViewEvent 9 | import com.ingbyr.vdm.events.UpdateEngineTask 10 | import com.ingbyr.vdm.models.DownloadTaskModel 11 | import com.ingbyr.vdm.models.DownloadTaskStatus 12 | import com.ingbyr.vdm.models.DownloadTaskType 13 | import com.ingbyr.vdm.models.TaskConfig 14 | import com.ingbyr.vdm.utils.* 15 | import com.ingbyr.vdm.utils.Attributes 16 | import com.ingbyr.vdm.utils.config.update 17 | import javafx.application.Platform 18 | import org.slf4j.Logger 19 | import org.slf4j.LoggerFactory 20 | import tornadofx.* 21 | import java.util.* 22 | import java.util.concurrent.ConcurrentHashMap 23 | 24 | 25 | class MainController : Controller() { 26 | 27 | init { 28 | messages = ResourceBundle.getBundle("i18n/MainView") 29 | } 30 | 31 | private val logger: Logger = LoggerFactory.getLogger(this::class.java) 32 | val downloadTaskModelList = mutableListOf().observable() 33 | private val engineList = ConcurrentHashMap() // FIXME auto clean the finished models engines 34 | 35 | init { 36 | 37 | // debug mode 38 | DebugUtils.changeDebugMode(app.config.boolean(Attributes.DEBUG_MODE, Attributes.Defaults.DEBUG_MODE)) 39 | 40 | subscribe { 41 | logger.debug("create models: ${it.downloadTask}") 42 | addToModelListAndStartTask(it.downloadTask) 43 | saveTaskToDB(it.downloadTask) 44 | } 45 | 46 | // background thread 47 | subscribe { 48 | val charset = app.config.string(Attributes.CHARSET, Attributes.Defaults.CHARSET) 49 | val engine = EngineFactory.create(it.engineType, charset) 50 | val taskConfig = TaskConfig("", it.engineType, DownloadTaskType.ENGINE, true, engine.enginePath) 51 | val downloadTask = DownloadTaskModel(taskConfig, DateTimeUtils.now(), title = "[${messages["ui.update"]} ${it.engineType.name}]") 52 | // make sure thread safe 53 | Platform.runLater { 54 | downloadTaskModelList.add(downloadTask) 55 | } 56 | try { 57 | if (engine.existNewVersion(it.localVersion)) { 58 | downloadTask.taskConfig.url = engine.updateUrl() 59 | logger.info("[${downloadTask.taskConfig.engineType}] update engine from ${downloadTask.taskConfig.url}") 60 | NetUtils().downloadEngine(downloadTask, engine.remoteVersion!!, needUnzip = engine.downloadNewEngineNeedUnzip) 61 | } else { 62 | downloadTask.title += messages["ui.noAvailableUpdates"] 63 | downloadTask.size = "" 64 | downloadTask.status = DownloadTaskStatus.COMPLETED 65 | downloadTask.progress = 1.0 66 | } 67 | } catch (e: Exception) { 68 | logger.error(e.toString()) 69 | downloadTask.status = DownloadTaskStatus.FAILED 70 | } 71 | fire(RestorePreferencesViewEvent) 72 | } 73 | 74 | subscribe { 75 | when (it.engineType) { 76 | EngineType.YOUTUBE_DL -> { 77 | app.config.update(Attributes.YOUTUBE_DL_VERSION, it.newVersion) 78 | } 79 | EngineType.ANNIE -> { 80 | app.config.update(Attributes.ANNIE_VERSION, it.newVersion) 81 | } 82 | } 83 | } 84 | } 85 | 86 | 87 | fun startTask(downloadTask: DownloadTaskModel) { 88 | if (downloadTask.taskConfig.downloadType == DownloadTaskType.ENGINE) return 89 | downloadTask.status = DownloadTaskStatus.ANALYZING 90 | val charset = app.config.string(Attributes.CHARSET, Attributes.Defaults.CHARSET) 91 | runAsync { 92 | // download 93 | val engine = EngineFactory.create(downloadTask.taskConfig.engineType, charset) 94 | engineList[downloadTask.createdAt] = engine 95 | val taskConfig = downloadTask.taskConfig 96 | engine.addProxy(taskConfig.proxyType, taskConfig.proxyAddress, taskConfig.proxyPort) 97 | .format(taskConfig.formatId) 98 | .output(taskConfig.storagePath) 99 | .cookies(taskConfig.cookie) 100 | .ffmpegPath(taskConfig.ffmpeg) 101 | .url(taskConfig.url) 102 | .downloadMedia(downloadTask, messages) 103 | } 104 | } 105 | 106 | fun startAllTask() { 107 | downloadTaskModelList.forEach { 108 | if (it.status != DownloadTaskStatus.COMPLETED && it.taskConfig.downloadType != DownloadTaskType.ENGINE) { 109 | startTask(it) 110 | } 111 | } 112 | } 113 | 114 | fun stopTask(downloadTask: DownloadTaskModel) { 115 | logger.debug("try to stop download models $downloadTask") 116 | engineList[downloadTask.createdAt]?.stopTask() 117 | } 118 | 119 | fun stopAllTask() { 120 | logger.debug("try to stop all download tasks") 121 | engineList.forEach { 122 | it.value.stopTask() 123 | } 124 | } 125 | 126 | fun deleteTask(downloadTaskModel: DownloadTaskModel) { 127 | stopTask(downloadTaskModel) 128 | downloadTaskModelList.remove(downloadTaskModel) 129 | DBUtils.deleteDownloadTask(downloadTaskModel) 130 | } 131 | 132 | private fun saveTaskToDB(downloadTask: DownloadTaskModel) { 133 | logger.debug("add download task $downloadTask to db") 134 | DBUtils.saveDownloadTask(downloadTask) 135 | } 136 | 137 | private fun addToModelListAndStartTask(downloadTask: DownloadTaskModel) { 138 | downloadTaskModelList.add(downloadTask) 139 | startTask(downloadTask) 140 | } 141 | 142 | fun loadTaskFromDB() { 143 | DBUtils.loadAllDownloadTasks(downloadTaskModelList) 144 | } 145 | 146 | fun clear() { 147 | stopAllTask() 148 | downloadTaskModelList.forEach { saveTaskToDB(it) } 149 | } 150 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/controllers/MediaFormatsController.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.controllers 2 | 3 | import com.ingbyr.vdm.engines.AbstractEngine 4 | import com.ingbyr.vdm.engines.utils.EngineFactory 5 | import com.ingbyr.vdm.events.StopBackgroundTask 6 | import com.ingbyr.vdm.models.DownloadTaskModel 7 | import com.ingbyr.vdm.models.MediaFormat 8 | import com.ingbyr.vdm.utils.Attributes 9 | import org.slf4j.LoggerFactory 10 | import tornadofx.* 11 | import java.util.* 12 | 13 | class MediaFormatsController : Controller() { 14 | 15 | private val logger = LoggerFactory.getLogger(MediaFormatsController::class.java) 16 | var engine: AbstractEngine? = null 17 | 18 | init { 19 | messages = ResourceBundle.getBundle("i18n/MediaFormatsView") 20 | subscribe { 21 | engine?.stopTask() 22 | } 23 | } 24 | 25 | 26 | fun requestMedia(downloadTaskModel: DownloadTaskModel): List? { 27 | val taskConfig = downloadTaskModel.taskConfig 28 | val charset = app.config.string(Attributes.CHARSET, Attributes.Defaults.CHARSET) 29 | engine = EngineFactory.create(taskConfig.engineType, charset) 30 | if (engine != null) { 31 | engine!!.simulateJson().addProxy(taskConfig.proxyType, taskConfig.proxyAddress, taskConfig.proxyPort).url(taskConfig.url) 32 | try { 33 | val jsonData = engine!!.fetchMediaJson() 34 | return engine!!.parseFormatsJson(jsonData) 35 | } catch (e: Exception) { 36 | logger.error(e.toString()) 37 | } 38 | } else { 39 | logger.error("bad engine: ${downloadTaskModel.taskConfig.engineType}") 40 | } 41 | return null 42 | } 43 | 44 | fun clear() { 45 | engine = null 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/controllers/PreferencesController.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.controllers 2 | 3 | import com.ingbyr.vdm.engines.utils.EngineType 4 | import com.ingbyr.vdm.events.UpdateEngineTask 5 | import com.ingbyr.vdm.utils.Attributes 6 | import com.ingbyr.vdm.utils.DebugUtils 7 | import com.ingbyr.vdm.utils.config.update 8 | import javafx.beans.property.SimpleBooleanProperty 9 | import javafx.beans.property.SimpleStringProperty 10 | import tornadofx.* 11 | 12 | class PreferencesController : Controller() { 13 | 14 | val debugModeProperty = SimpleBooleanProperty() 15 | var debugMode: Boolean by debugModeProperty 16 | var cookieList = mutableListOf().observable() 17 | val cookieProperty = SimpleStringProperty() 18 | private var cookie by cookieProperty 19 | 20 | init { 21 | initDebugMode() 22 | freshCookieListAndContent() 23 | } 24 | 25 | private fun initDebugMode() { 26 | // enable debug default 27 | debugMode = app.config.boolean(Attributes.DEBUG_MODE, Attributes.Defaults.DEBUG_MODE) 28 | debugModeProperty.addListener { _, _, mode -> 29 | DebugUtils.changeDebugMode(mode) 30 | app.config.update(Attributes.DEBUG_MODE, mode) 31 | } 32 | } 33 | 34 | fun freshCookieListAndContent() { 35 | 36 | } 37 | 38 | fun readCookieContent() { 39 | // TODO save cookie to db 40 | // val cookieName = ConfigUtils.safeLoad(Attributes.CURRENT_COOKIE, "") 41 | // if (cookieName.isBlank()) cookie= "" 42 | // val cookieFile = Attributes.COOKIES_DIR.resolve(cookieName).toFile() 43 | // cookie = if (cookieFile.exists()) { 44 | // cookieFile.readText() 45 | // } else { 46 | // "" 47 | // } 48 | } 49 | 50 | fun updateEngine(engineType: EngineType, localVersion: String) { 51 | fire(UpdateEngineTask(engineType, localVersion)) 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/controllers/TextEditorController.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.controllers 2 | 3 | import tornadofx.* 4 | 5 | class TextEditorController : Controller() { 6 | 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/controllers/ThemeController.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.controllers 2 | 3 | import com.ingbyr.vdm.stylesheets.DarkTheme 4 | import com.ingbyr.vdm.stylesheets.LightTheme 5 | import javafx.beans.property.SimpleListProperty 6 | import javafx.beans.property.SimpleObjectProperty 7 | import org.slf4j.Logger 8 | import org.slf4j.LoggerFactory 9 | import tornadofx.* 10 | import java.util.* 11 | import kotlin.reflect.KClass 12 | 13 | open class ThemeController : Controller() { 14 | private val logger: Logger = LoggerFactory.getLogger(ThemeController::class.java) 15 | 16 | init { 17 | messages = ResourceBundle.getBundle("i18n/PreferencesView") 18 | } 19 | 20 | val themes = SimpleListProperty>(listOf(LightTheme::class, DarkTheme::class).observable()) 21 | val activeThemeProperty = SimpleObjectProperty>() 22 | var activeTheme by activeThemeProperty 23 | 24 | 25 | fun initTheme() { 26 | // add listener to change theme 27 | activeThemeProperty.addListener { _, oldTheme, newTheme -> 28 | oldTheme?.let{ removeStylesheet(it)} 29 | newTheme?.let { importStylesheet(it) } 30 | } 31 | 32 | // TODO load theme from app 33 | activeTheme = themes.first() 34 | logger.debug("init theme ${activeTheme.simpleName}") 35 | } 36 | 37 | fun reloadTheme() { 38 | removeStylesheet(activeTheme) 39 | importStylesheet(activeTheme) 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/controllers/WizardController.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.controllers 2 | 3 | 4 | import com.ingbyr.vdm.engines.utils.EngineType 5 | import com.ingbyr.vdm.events.UpdateEngineTask 6 | import com.ingbyr.vdm.utils.Attributes 7 | import com.ingbyr.vdm.utils.config.update 8 | import org.slf4j.Logger 9 | import org.slf4j.LoggerFactory 10 | import tornadofx.* 11 | import java.util.* 12 | 13 | class WizardController : Controller() { 14 | init { 15 | messages = ResourceBundle.getBundle("i18n/PreferencesView") 16 | } 17 | 18 | private val logger: Logger = LoggerFactory.getLogger(WizardController::class.java) 19 | private val themeController: ThemeController by inject() 20 | 21 | // summary info 22 | private val enginesNeedDownload = mutableSetOf() 23 | private val commonSettingChanges = mutableMapOf() 24 | 25 | fun addEngine(engine: String) { 26 | enginesNeedDownload.add(EngineType.valueOf(engine.replace("-", "_").toUpperCase())) 27 | } 28 | 29 | fun removeEngine(engine: String) { 30 | enginesNeedDownload.remove(EngineType.valueOf(engine.replace("-", "_").toUpperCase())) 31 | } 32 | 33 | fun changePrimaryColor(color: String) { 34 | app.config.update(Attributes.THEME_PRIMARY_COLOR, color) 35 | themeController.reloadTheme() 36 | commonSettingChanges[messages["ui.primaryColor"]] = color 37 | } 38 | 39 | fun changeSecondaryColor(color: String) { 40 | app.config.update(Attributes.THEME_SECONDARY_COLOR, color) 41 | themeController.reloadTheme() 42 | commonSettingChanges[messages["ui.secondaryColor"]] = color 43 | } 44 | 45 | fun changeStoragePath(storagePath: String) { 46 | app.config.update(Attributes.STORAGE_PATH, storagePath) 47 | commonSettingChanges[messages["ui.storagePath"]] = storagePath 48 | } 49 | 50 | fun summary(): String { 51 | val sb = StringBuilder() 52 | // engines 53 | sb.appendln("${messages["ui.installEngines"]}: $enginesNeedDownload") 54 | sb.appendln() 55 | 56 | // TODO ui display color in a better way 57 | commonSettingChanges.forEach { configName, config -> sb.appendln("$configName ${messages["ui.changeTo"]} $config") } 58 | return sb.toString() 59 | } 60 | 61 | fun startDownloadSelectedEngines() { 62 | enginesNeedDownload.forEach { 63 | fire(UpdateEngineTask(it, "0.0.0")) 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/dao/DownloadTaskDAO.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.dao 2 | 3 | import com.ingbyr.vdm.models.DownloadTaskModel 4 | import com.ingbyr.vdm.models.DownloadTaskStatus 5 | import org.jetbrains.exposed.dao.EntityID 6 | import org.jetbrains.exposed.dao.IntEntity 7 | import org.jetbrains.exposed.dao.IntEntityClass 8 | 9 | class DownloadTaskDAO(id: EntityID) : IntEntity(id) { 10 | companion object : IntEntityClass(DownloadTaskTable) 11 | 12 | var taskConfig by TaskConfigDAO referencedOn DownloadTaskTable.taskConfig 13 | var checked by DownloadTaskTable.checked 14 | var title by DownloadTaskTable.title 15 | var size by DownloadTaskTable.size 16 | var status by DownloadTaskTable.status 17 | var progress by DownloadTaskTable.progress 18 | var createdAt by DownloadTaskTable.createdAt 19 | } 20 | 21 | /** 22 | * transfer DownloadTaskDAO to DownloadTaskModel 23 | */ 24 | fun DownloadTaskDAO.toModel(): DownloadTaskModel = DownloadTaskModel( 25 | this.taskConfig.toModel(), 26 | this.createdAt, 27 | this.checked, 28 | this.title, 29 | this.size, 30 | this.progress.toDouble(), 31 | DownloadTaskStatus.valueOf(this.status)) -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/dao/DownloadTaskTable.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.dao 2 | 3 | import org.jetbrains.exposed.dao.EntityID 4 | import org.jetbrains.exposed.dao.IntIdTable 5 | import org.jetbrains.exposed.sql.Column 6 | 7 | object DownloadTaskTable : IntIdTable() { 8 | val taskConfig: Column> = reference("taskConfig", TaskConfigTable) 9 | val checked: Column = bool("checked").default(false) 10 | val title: Column = varchar("title", length = 255) 11 | val size: Column = varchar("size", length = 50) 12 | val status: Column = varchar("status", length = 255) 13 | val progress: Column = float("progress").default(0f) 14 | val createdAt: Column = varchar("createdAt", length = 50).uniqueIndex() 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/dao/TaskConfigDAO.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.dao 2 | 3 | import com.ingbyr.vdm.engines.utils.EngineType 4 | import com.ingbyr.vdm.models.DownloadTaskType 5 | import com.ingbyr.vdm.models.ProxyType 6 | import com.ingbyr.vdm.models.TaskConfig 7 | import org.jetbrains.exposed.dao.EntityID 8 | import org.jetbrains.exposed.dao.IntEntity 9 | import org.jetbrains.exposed.dao.IntEntityClass 10 | 11 | class TaskConfigDAO(id: EntityID) : IntEntity(id) { 12 | companion object : IntEntityClass(TaskConfigTable) 13 | 14 | var url by TaskConfigTable.url 15 | var downloadType by TaskConfigTable.downloadType 16 | var engineType by TaskConfigTable.engineType 17 | var downloadDefaultFormat by TaskConfigTable.downloadDefaultFormat 18 | var storagePath by TaskConfigTable.storagePath 19 | var cookie by TaskConfigTable.cookie 20 | var ffmpeg by TaskConfigTable.ffmpeg 21 | var formatId by TaskConfigTable.formatID 22 | var proxyType by TaskConfigTable.proxyType 23 | var proxyAddress by TaskConfigTable.proxyAddress 24 | var proxyPort by TaskConfigTable.proxyPort 25 | } 26 | 27 | fun TaskConfigDAO.toModel() = TaskConfig( 28 | this.url, 29 | EngineType.valueOf(this.engineType), 30 | DownloadTaskType.valueOf(this.downloadType), 31 | this.downloadDefaultFormat, 32 | this.storagePath, 33 | this.cookie, 34 | this.ffmpeg, 35 | this.formatId, 36 | ProxyType.valueOf(this.proxyType), 37 | this.proxyAddress, 38 | this.proxyPort 39 | ) 40 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/dao/TaskConfigTable.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.dao 2 | 3 | import org.jetbrains.exposed.dao.IntIdTable 4 | import org.jetbrains.exposed.sql.Column 5 | 6 | object TaskConfigTable : IntIdTable() { 7 | val url: Column = varchar("url", length = 255) 8 | val downloadType: Column = varchar("downloadType", length = 50) 9 | val engineType: Column = varchar("engineType", 50) 10 | val downloadDefaultFormat: Column = bool("downloadDefaultFormat") 11 | val storagePath: Column = varchar("storagePath", 255) 12 | val cookie: Column = varchar("cookie", 255) 13 | val ffmpeg: Column = varchar("ffmpeg", 255) 14 | val formatID: Column = varchar("formatID", length = 50).default("-1") 15 | val proxyType: Column = varchar("proxyType", length = 50) 16 | val proxyAddress: Column = varchar("proxyAddress", length = 50) 17 | val proxyPort: Column = varchar("proxyPort", length = 50) 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/engines/AbstractEngine.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.engines 2 | 3 | import com.ingbyr.vdm.engines.utils.EngineDownloadType 4 | import com.ingbyr.vdm.engines.utils.EngineType 5 | import com.ingbyr.vdm.models.DownloadTaskModel 6 | import com.ingbyr.vdm.models.MediaFormat 7 | import com.ingbyr.vdm.models.ProxyType 8 | import org.slf4j.Logger 9 | import java.util.* 10 | import java.util.concurrent.atomic.AtomicBoolean 11 | 12 | abstract class AbstractEngine { 13 | var charset = "UTF-8" 14 | 15 | protected abstract val logger: Logger 16 | protected val running: AtomicBoolean = AtomicBoolean(false) 17 | abstract val downloadNewEngineNeedUnzip: Boolean 18 | abstract val enginePath: String 19 | abstract val engineType: EngineType 20 | abstract val argsMap: MutableMap 21 | abstract val remoteVersionUrl: String 22 | abstract var remoteVersion: String? 23 | abstract var version: String 24 | abstract var taskModel: DownloadTaskModel? 25 | 26 | abstract fun url(url: String): AbstractEngine 27 | abstract fun simulateJson(): AbstractEngine 28 | abstract fun addProxy(type: ProxyType, address: String, port: String): AbstractEngine 29 | abstract fun fetchMediaJson(): String 30 | abstract fun format(formatID: String): AbstractEngine 31 | abstract fun output(outputPath: String): AbstractEngine 32 | abstract fun ffmpegPath(ffmpegPath: String): AbstractEngine 33 | abstract fun cookies(cookies: String): AbstractEngine 34 | abstract fun downloadMedia(downloadTaskModel: DownloadTaskModel, message: ResourceBundle) 35 | abstract fun parseDownloadOutput(line: String) 36 | abstract fun execCommand(command: MutableList, downloadType: EngineDownloadType): StringBuilder? 37 | abstract fun parseFormatsJson(jsonString: String): List 38 | 39 | abstract fun engineExecPath(): String 40 | abstract fun updateUrl(): String 41 | abstract fun existNewVersion(localVersion: String): Boolean 42 | 43 | open fun stopTask() { 44 | /** 45 | * Please overwrite this if need extra operation. 46 | */ 47 | running.set(false) 48 | } 49 | 50 | protected fun MutableMap.build(): MutableList { 51 | /** 52 | * Generate the command line from argsMap 53 | */ 54 | val args = mutableListOf() 55 | 56 | this.forEach { 57 | if (it.key.startsWith("-")) { 58 | args.add(it.key) 59 | args.add(it.value) 60 | } else { 61 | args.add(it.value) 62 | } 63 | } 64 | logger.debug("exec $args") 65 | return args 66 | } 67 | 68 | protected fun String.toProgress(): Double { 69 | /** 70 | * Transfer "42.3%"(String) to 0.423(Double) 71 | */ 72 | val s = this.replace("%", "") 73 | return s.trim().toDouble() / 100 74 | } 75 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/engines/Annie.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.engines 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties 4 | import com.fasterxml.jackson.annotation.JsonProperty 5 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 6 | import com.fasterxml.jackson.module.kotlin.readValue 7 | import com.ingbyr.vdm.engines.utils.EngineDownloadType 8 | import com.ingbyr.vdm.engines.utils.EngineException 9 | import com.ingbyr.vdm.engines.utils.EngineType 10 | import com.ingbyr.vdm.models.DownloadTaskModel 11 | import com.ingbyr.vdm.models.DownloadTaskStatus 12 | import com.ingbyr.vdm.models.MediaFormat 13 | import com.ingbyr.vdm.models.ProxyType 14 | import com.ingbyr.vdm.utils.* 15 | import com.ingbyr.vdm.utils.config.app 16 | import org.slf4j.Logger 17 | import org.slf4j.LoggerFactory 18 | import java.io.BufferedReader 19 | import java.io.InputStreamReader 20 | import java.util.* 21 | import java.util.concurrent.TimeUnit 22 | import java.util.regex.Pattern 23 | 24 | 25 | class Annie : AbstractEngine() { 26 | override val logger: Logger = LoggerFactory.getLogger(Annie::class.java) 27 | override val downloadNewEngineNeedUnzip: Boolean = true 28 | override val enginePath: String = engineExecPath() 29 | override val engineType: EngineType = EngineType.ANNIE 30 | override val argsMap: MutableMap = mutableMapOf("engine" to enginePath) 31 | override val remoteVersionUrl: String = "https://raw.githubusercontent.com/iawia002/annie/master/app/version.go" 32 | override var remoteVersion: String? = null 33 | override var version: String = app.config.string(Attributes.ANNIE_VERSION, Attributes.Defaults.ENGINE_VERSION) 34 | override var taskModel: DownloadTaskModel? = null 35 | 36 | private val remoteVersionPattern: Pattern = Pattern.compile("\\d+.+\\d+") 37 | private var speed = "0MiB/s" 38 | private var progress = 0.0 39 | private var size = "" 40 | private var title = "" 41 | private val progressPattern = Pattern.compile("\\d+\\.\\d*%") 42 | private val speedPattern = Pattern.compile("\\d+\\.\\d*\\s+\\w+/s") 43 | 44 | override fun url(url: String): AbstractEngine { 45 | argsMap["url"] = url 46 | return this 47 | } 48 | 49 | override fun addProxy(type: ProxyType, address: String, port: String): AbstractEngine { 50 | return if (address.isEmpty() or port.isEmpty()) { 51 | this 52 | } else { 53 | when (type) { 54 | ProxyType.SOCKS5 -> { 55 | argsMap["-s"] = "$address:$port" 56 | } 57 | ProxyType.HTTP -> { 58 | argsMap["-x"] = "http://$address:$port" 59 | } 60 | else -> { 61 | } 62 | } 63 | this 64 | } 65 | } 66 | 67 | override fun simulateJson(): AbstractEngine { 68 | argsMap["SimulateJson"] = "-j" 69 | return this 70 | } 71 | 72 | override fun fetchMediaJson(): String { 73 | val mediaData = execCommand(argsMap.build(), EngineDownloadType.JSON) 74 | if (mediaData != null) { 75 | return mediaData.toString() 76 | } else { 77 | logger.error("no media json return") 78 | throw EngineException("no media json return") 79 | } 80 | } 81 | 82 | override fun format(formatID: String): AbstractEngine { 83 | if (formatID.isNotEmpty()) argsMap["-f"] = formatID 84 | return this 85 | } 86 | 87 | override fun output(outputPath: String): AbstractEngine { 88 | if (outputPath.isNotEmpty()) argsMap["-o"] = outputPath 89 | return this 90 | } 91 | 92 | override fun ffmpegPath(ffmpegPath: String): AbstractEngine { 93 | return this 94 | } 95 | 96 | override fun cookies(cookies: String): AbstractEngine { 97 | if (cookies.isNotEmpty()) { 98 | argsMap["-c"] = cookies 99 | } 100 | return this 101 | } 102 | 103 | override fun downloadMedia(downloadTaskModel: DownloadTaskModel, message: ResourceBundle) { 104 | taskModel = downloadTaskModel 105 | taskModel?.run { 106 | execCommand(argsMap.build(), EngineDownloadType.SINGLE) 107 | } 108 | } 109 | 110 | override fun parseDownloadOutput(line: String) { 111 | if (title.isEmpty() && line.trim().startsWith("Title")) { 112 | title = line.trim().removePrefix("Title:").trim() 113 | if (title.isNotEmpty()) taskModel?.title = title 114 | } 115 | if (size.isEmpty() && line.trim().startsWith("Size")) { 116 | size = line.trim().removePrefix("Size:").trim().split("(").firstOrNull()?.trim() ?: "" 117 | if (size.isNotEmpty()) taskModel?.size = size.trim() 118 | } 119 | 120 | progress = progressPattern.matcher(line).takeIf { it.find() }?.group()?.toProgress() ?: progress 121 | speed = speedPattern.matcher(line).takeIf { it.find() }?.group()?.toString() ?: speed 122 | logger.debug("$line -> title=$title, progress=$progress, size=$size, speed=$speed") 123 | 124 | taskModel?.run { 125 | if (this@Annie.progress >= 1.0) { 126 | this.progress = 1.0 127 | if (line.trim().startsWith("[ffmpeg]")) 128 | this.status = DownloadTaskStatus.MERGING 129 | else 130 | this.status = DownloadTaskStatus.COMPLETED 131 | } else if (this@Annie.progress > 0) { 132 | this.progress = this@Annie.progress 133 | this.status = DownloadTaskStatus.DOWNLOADING 134 | } 135 | } 136 | } 137 | 138 | override fun execCommand(command: MutableList, downloadType: EngineDownloadType): StringBuilder? { 139 | running.set(true) 140 | val builder = ProcessBuilder(command) 141 | builder.redirectErrorStream(true) 142 | val p = builder.start() 143 | val r = BufferedReader(InputStreamReader(p.inputStream, charset)) 144 | logger.debug("parse output as $charset") 145 | val output = StringBuilder() 146 | var line: String? 147 | when (downloadType) { 148 | EngineDownloadType.JSON -> { 149 | // fetch the media json and return string builder 150 | while (running.get()) { 151 | line = r.readLine() 152 | if (line != null) { 153 | output.append(line.trim()) 154 | } else { 155 | break 156 | } 157 | } 158 | } 159 | 160 | EngineDownloadType.SINGLE, EngineDownloadType.PLAYLIST -> { 161 | while (running.get()) { 162 | line = r.readLine() 163 | if (line != null) { 164 | parseDownloadOutput(line) 165 | } else { 166 | break 167 | } 168 | } 169 | } 170 | } 171 | 172 | if (p.isAlive) { // means user stop this models manually 173 | p.destroy() 174 | p.waitFor(200, TimeUnit.MICROSECONDS) 175 | } 176 | 177 | if (p.isAlive) { 178 | p.destroyForcibly() 179 | } 180 | 181 | return if (running.get()) { 182 | running.set(false) 183 | output 184 | } else { // means user stop this models manually 185 | taskModel?.run { 186 | status = DownloadTaskStatus.STOPPED 187 | } 188 | logger.debug("stop the models of $taskModel") 189 | null 190 | } 191 | } 192 | 193 | override fun parseFormatsJson(jsonString: String): List { 194 | val formats = mutableListOf() 195 | val annieMediaJson = jacksonObjectMapper().readValue(jsonString) 196 | annieMediaJson.streams.forEach { id, formatInfo -> 197 | 198 | formats.add( 199 | MediaFormat( 200 | title = annieMediaJson.title, 201 | desc = "", 202 | formatID = id, 203 | format = formatInfo.quality, 204 | formatNote = "", 205 | fileSize = formatInfo.size.toLong(), 206 | ext = formatInfo.urls.first().ext // FIXME youtube has many urls 207 | ) 208 | ) 209 | } 210 | return formats 211 | } 212 | 213 | override fun engineExecPath(): String = when (OSUtils.currentOS) { 214 | OSType.WINDOWS -> { 215 | Attributes.ENGINES_DIR.resolve("annie.exe").toAbsolutePath().toString() 216 | } 217 | OSType.LINUX -> { 218 | Attributes.ENGINES_DIR.resolve("annie").toAbsolutePath().toString() 219 | } 220 | OSType.MAC_OS -> { 221 | Attributes.ENGINES_DIR.resolve("annie").toAbsolutePath().toString() 222 | } 223 | } 224 | 225 | override fun updateUrl(): String = when (OSUtils.currentOS) { 226 | OSType.WINDOWS -> { 227 | "https://github.com/iawia002/annie/releases/download/$remoteVersion/annie_${remoteVersion}_Windows_32-bit.zip" 228 | } 229 | OSType.LINUX -> { 230 | "https://github.com/iawia002/annie/releases/download/$remoteVersion/annie_${remoteVersion}_Linux_32-bit.tar.gz" 231 | } 232 | OSType.MAC_OS -> { 233 | "https://github.com/iawia002/annie/releases/download/$remoteVersion/annie_${remoteVersion}_macOS_32-bit.tar.gz" 234 | } 235 | } 236 | 237 | override fun existNewVersion(localVersion: String): Boolean { 238 | val remoteVersionData = NetUtils().get(remoteVersionUrl) 239 | return if (remoteVersionData?.isNotEmpty() == true) { 240 | remoteVersion = remoteVersionPattern.matcher(remoteVersionData).takeIf { it.find() }?.group() 241 | if (remoteVersion != null) { 242 | logger.debug("[$engineType] local version $localVersion, remote version $remoteVersion") 243 | UpdateUtils.check(localVersion, remoteVersion!!) 244 | } else { 245 | logger.error("[$engineType] get remote version failed") 246 | false 247 | } 248 | } else { 249 | false 250 | } 251 | } 252 | } 253 | 254 | @JsonIgnoreProperties(ignoreUnknown = true) 255 | data class AnnieMediaJson( 256 | @JsonProperty("site") 257 | val site: String, 258 | @JsonProperty("streams") 259 | val streams: Map, 260 | @JsonProperty("title") 261 | val title: String, 262 | @JsonProperty("type") 263 | val type: String, 264 | @JsonProperty("url") 265 | val url: String 266 | ) 267 | 268 | @JsonIgnoreProperties(ignoreUnknown = true) 269 | data class Stream( 270 | @JsonProperty("quality") 271 | val quality: String, 272 | @JsonProperty("size") 273 | val size: Int, 274 | @JsonProperty("urls") 275 | val urls: List 276 | ) 277 | 278 | @JsonIgnoreProperties(ignoreUnknown = true) 279 | data class Url( 280 | @JsonProperty("ext") 281 | val ext: String, 282 | @JsonProperty("size") 283 | val size: Int, 284 | @JsonProperty("url") 285 | val url: String 286 | ) 287 | 288 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/engines/YoutubeDL.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.engines 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties 4 | import com.fasterxml.jackson.annotation.JsonProperty 5 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 6 | import com.fasterxml.jackson.module.kotlin.readValue 7 | import com.ingbyr.vdm.engines.utils.EngineDownloadType 8 | import com.ingbyr.vdm.engines.utils.EngineException 9 | import com.ingbyr.vdm.engines.utils.EngineType 10 | import com.ingbyr.vdm.models.DownloadTaskModel 11 | import com.ingbyr.vdm.models.DownloadTaskStatus 12 | import com.ingbyr.vdm.models.MediaFormat 13 | import com.ingbyr.vdm.models.ProxyType 14 | import com.ingbyr.vdm.utils.* 15 | import com.ingbyr.vdm.utils.config.app 16 | import org.slf4j.Logger 17 | import org.slf4j.LoggerFactory 18 | import java.io.BufferedReader 19 | import java.io.InputStreamReader 20 | import java.nio.file.Paths 21 | import java.util.* 22 | import java.util.concurrent.TimeUnit 23 | import java.util.regex.Pattern 24 | 25 | 26 | class YoutubeDL : AbstractEngine() { 27 | override val logger: Logger = LoggerFactory.getLogger(this::class.java) 28 | override val downloadNewEngineNeedUnzip: Boolean = false 29 | override val remoteVersionUrl: String = 30 | "https://raw.githubusercontent.com/rg3/youtube-dl/master/youtube_dl/version.py" 31 | override val engineType = EngineType.YOUTUBE_DL 32 | override val enginePath: String = engineExecPath() 33 | override var remoteVersion: String? = null 34 | override var version: String = app.config.string(Attributes.YOUTUBE_DL_VERSION, Attributes.Defaults.ENGINE_VERSION) 35 | override var taskModel: DownloadTaskModel? = null 36 | override val argsMap: MutableMap = mutableMapOf("engines" to enginePath) 37 | 38 | private var speed = "0MiB/s" 39 | private var progress = 0.0 40 | private var size = "" 41 | private var title = "" 42 | private val nameTemplate = "%(title)s.%(ext)s" 43 | private val progressPattern = Pattern.compile("\\d+\\W?\\d*%") 44 | private val speedPattern = Pattern.compile("\\d+\\W?\\d*\\w+/s") 45 | private val titlePattern = Pattern.compile("[/\\\\][^/^\\\\]+\\.\\w+") 46 | private val fileSizePattern = Pattern.compile("\\s\\d+\\W?\\d*\\w+B\\s") 47 | private val remoteVersionPattern = Pattern.compile("\\d+.+\\d+") 48 | 49 | override fun parseFormatsJson(jsonString: String): List { 50 | val formats = mutableListOf() 51 | val mapper = jacksonObjectMapper() 52 | val mediaJson = mapper.readValue(jsonString) 53 | mediaJson.formats.forEach { 54 | formats.add( 55 | MediaFormat( 56 | title = mediaJson.title, 57 | desc = mediaJson.description, 58 | formatID = it.formatId, 59 | format = it.format, 60 | formatNote = it.formatNote, 61 | fileSize = it.filesize, 62 | ext = it.ext 63 | ) 64 | ) 65 | } 66 | return formats 67 | } 68 | 69 | override fun simulateJson(): AbstractEngine { 70 | argsMap["SimulateJson"] = "-j" 71 | return this 72 | } 73 | 74 | override fun url(url: String): AbstractEngine { 75 | argsMap["url"] = url 76 | return this 77 | } 78 | 79 | override fun addProxy(type: ProxyType, address: String, port: String): AbstractEngine { 80 | if (address.isEmpty() or port.isEmpty()) { 81 | return this 82 | } 83 | when (type) { 84 | ProxyType.SOCKS5 -> { 85 | argsMap["--proxy"] = "socks5://$address:$port" 86 | } 87 | ProxyType.HTTP -> { 88 | argsMap["--proxy"] = "$address:$port" 89 | } 90 | else -> { 91 | logger.debug("no proxy") 92 | } 93 | } 94 | return this 95 | } 96 | 97 | override fun fetchMediaJson(): String { 98 | val mediaData = execCommand(argsMap.build(), EngineDownloadType.JSON) 99 | if (mediaData != null) { 100 | return mediaData.toString() 101 | } else { 102 | logger.error("no media json return") 103 | throw EngineException("no media json return") 104 | } 105 | } 106 | 107 | override fun format(formatID: String): AbstractEngine { 108 | return if (formatID.isEmpty()) { 109 | this 110 | } else { 111 | argsMap["-f"] = formatID 112 | this 113 | } 114 | } 115 | 116 | override fun output(outputPath: String): AbstractEngine { 117 | argsMap["-o"] = Paths.get(outputPath, nameTemplate).toString() 118 | return this 119 | } 120 | 121 | override fun ffmpegPath(ffmpegPath: String): AbstractEngine { 122 | return if (ffmpegPath.isEmpty()) { 123 | this 124 | } else { 125 | argsMap["--ffmpeg-location"] = ffmpegPath 126 | this 127 | } 128 | } 129 | 130 | override fun cookies(cookies: String): AbstractEngine { 131 | return if (cookies.isEmpty()) { 132 | this 133 | } else { 134 | argsMap["--cookies"] = cookies 135 | this 136 | } 137 | } 138 | 139 | override fun downloadMedia(downloadTaskModel: DownloadTaskModel, message: ResourceBundle) { 140 | taskModel = downloadTaskModel 141 | taskModel?.run { 142 | // init display 143 | execCommand(argsMap.build(), EngineDownloadType.SINGLE) 144 | } 145 | } 146 | 147 | override fun parseDownloadOutput(line: String) { 148 | if (title.isEmpty()) { 149 | title = titlePattern.matcher(line).takeIf { it.find() }?.group()?.toString() ?: title 150 | title = title.removePrefix("/").removePrefix("\\") 151 | if (title.isNotEmpty()) taskModel?.title = title 152 | } 153 | if (size.isEmpty()) { 154 | size = fileSizePattern.matcher(line).takeIf { it.find() }?.group()?.toString() ?: size 155 | if (size.isNotEmpty()) taskModel?.size = size.trim() 156 | } 157 | 158 | progress = progressPattern.matcher(line).takeIf { it.find() }?.group()?.toProgress() ?: progress 159 | speed = speedPattern.matcher(line).takeIf { it.find() }?.group()?.toString() ?: speed 160 | logger.debug("$line -> title=$title, progress=$progress, size=$size, speed=$speed") 161 | 162 | taskModel?.run { 163 | if (this@YoutubeDL.progress >= 1.0) { 164 | this.progress = 1.0 165 | if (line.trim().startsWith("[ffmpeg]")) 166 | this.status = DownloadTaskStatus.MERGING 167 | else 168 | this.status = DownloadTaskStatus.COMPLETED 169 | } else if (this@YoutubeDL.progress > 0) { 170 | this.progress = this@YoutubeDL.progress 171 | this.status = DownloadTaskStatus.DOWNLOADING 172 | } 173 | } 174 | } 175 | 176 | override fun execCommand(command: MutableList, downloadType: EngineDownloadType): StringBuilder? { 177 | /** 178 | * Exec the command by invoking the system shell etc. 179 | * Long time models 180 | */ 181 | running.set(true) 182 | val builder = ProcessBuilder(command) 183 | builder.redirectErrorStream(true) 184 | val p = builder.start() 185 | val r = BufferedReader(InputStreamReader(p.inputStream, charset)) 186 | logger.debug("parse output as $charset") 187 | val output = StringBuilder() 188 | var line: String? 189 | when (downloadType) { 190 | EngineDownloadType.JSON -> { 191 | // fetch the media json and return string builder 192 | while (running.get()) { 193 | line = r.readLine() 194 | if (line != null) { 195 | output.append(line.trim()) 196 | } else { 197 | break 198 | } 199 | } 200 | } 201 | 202 | EngineDownloadType.SINGLE, EngineDownloadType.PLAYLIST -> { 203 | while (running.get()) { 204 | line = r.readLine() 205 | if (line != null) { 206 | parseDownloadOutput(line) 207 | } else { 208 | break 209 | } 210 | } 211 | } 212 | } 213 | 214 | if (p.isAlive) { // means user stop this models manually 215 | p.destroy() 216 | p.waitFor(200, TimeUnit.MICROSECONDS) 217 | } 218 | 219 | if (p.isAlive) { 220 | p.destroyForcibly() 221 | } 222 | 223 | return if (running.get()) { 224 | running.set(false) 225 | output 226 | } else { // means user stop this models manually 227 | taskModel?.run { 228 | status = DownloadTaskStatus.STOPPED 229 | } 230 | logger.debug("stop the models of $taskModel") 231 | null 232 | } 233 | } 234 | 235 | override fun engineExecPath(): String = when (OSUtils.currentOS) { 236 | OSType.WINDOWS -> { 237 | Attributes.ENGINES_DIR.resolve("youtube-dl.exe").toAbsolutePath().toString() 238 | } 239 | OSType.LINUX -> { 240 | Attributes.ENGINES_DIR.resolve("youtube-dl").toAbsolutePath().toString() 241 | } 242 | OSType.MAC_OS -> { 243 | Attributes.ENGINES_DIR.resolve("youtube-dl").toAbsolutePath().toString() 244 | } 245 | } 246 | 247 | override fun updateUrl() = when (OSUtils.currentOS) { 248 | OSType.WINDOWS -> { 249 | "https://github.com/rg3/youtube-dl/releases/download/$remoteVersion/youtube-dl.exe" 250 | } 251 | OSType.LINUX, OSType.MAC_OS -> { 252 | "https://github.com/rg3/youtube-dl/releases/download/$remoteVersion/youtube-dl" 253 | } 254 | } 255 | 256 | override fun existNewVersion(localVersion: String): Boolean { 257 | val remoteVersionInfo = NetUtils().get(remoteVersionUrl) 258 | return if (remoteVersionInfo?.isNotEmpty() == true) { 259 | remoteVersion = remoteVersionPattern.matcher(remoteVersionInfo).takeIf { it.find() }?.group() 260 | if (remoteVersion != null) { 261 | logger.debug("[$engineType] local version $localVersion, remote version $remoteVersion") 262 | UpdateUtils.check(localVersion, remoteVersion!!) 263 | } else { 264 | logger.error("[$engineType] get remote version failed") 265 | false 266 | } 267 | } else { 268 | logger.error("[$engineType] get remote version failed") 269 | false 270 | } 271 | } 272 | } 273 | 274 | @JsonIgnoreProperties(ignoreUnknown = true) 275 | data class YoutubeDlMediaJson( 276 | @JsonProperty("formats") val formats: List = listOf(), 277 | @JsonProperty("fulltitle") val fulltitle: String = "", 278 | @JsonProperty("description") val description: String = "", 279 | @JsonProperty("title") val title: String = "" 280 | ) 281 | 282 | @JsonIgnoreProperties(ignoreUnknown = true) 283 | data class Format( 284 | @JsonProperty("format") val format: String = "", 285 | @JsonProperty("format_note") val formatNote: String = "", 286 | @JsonProperty("ext") val ext: String = "", 287 | @JsonProperty("filesize") val filesize: Long = 0, 288 | @JsonProperty("format_id") val formatId: String = "" 289 | ) 290 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/engines/utils/EngineDownloadType.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.engines.utils 2 | 3 | enum class EngineDownloadType { 4 | JSON, 5 | SINGLE, 6 | PLAYLIST 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/engines/utils/EngineException.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.engines.utils 2 | 3 | class EngineException(message: String) : Exception(message) -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/engines/utils/EngineFactory.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.engines.utils 2 | 3 | import com.ingbyr.vdm.engines.AbstractEngine 4 | import com.ingbyr.vdm.engines.Annie 5 | import com.ingbyr.vdm.engines.YoutubeDL 6 | 7 | object EngineFactory { 8 | fun create(engineType: EngineType, charset: String = "UTF-8"): AbstractEngine { 9 | return when (engineType) { 10 | EngineType.YOUTUBE_DL -> { 11 | val engine = YoutubeDL() 12 | engine.charset = charset 13 | engine 14 | } 15 | EngineType.ANNIE -> { 16 | val engine = Annie() 17 | engine.charset = charset 18 | engine 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/engines/utils/EngineType.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.engines.utils 2 | 3 | import java.io.Serializable 4 | 5 | enum class EngineType : Serializable { 6 | YOUTUBE_DL, 7 | ANNIE 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/events/DownloadTaskEvents.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.events 2 | 3 | import com.ingbyr.vdm.engines.utils.EngineType 4 | import com.ingbyr.vdm.models.DownloadTaskModel 5 | import tornadofx.* 6 | 7 | class StopBackgroundTask(val downloadTask: DownloadTaskModel? = null, val stopAll: Boolean = false) : FXEvent() 8 | 9 | class CreateDownloadTask(val downloadTask: DownloadTaskModel) : FXEvent() 10 | 11 | class UpdateEngineTask(val engineType: EngineType, val localVersion: String) : FXEvent(EventBus.RunOn.BackgroundThread) -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/events/RefreshUIEvents.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.events 2 | 3 | import com.ingbyr.vdm.engines.utils.EngineType 4 | import tornadofx.* 5 | 6 | class RefreshEngineVersion(val engineType: EngineType, val newVersion: String) : FXEvent() 7 | 8 | object RefreshCookieContent : FXEvent() -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/events/RestorePreferencesViewEvent.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.events 2 | 3 | import tornadofx.* 4 | 5 | object RestorePreferencesViewEvent : FXEvent() -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/models/DownloadTaskModel.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.models 2 | 3 | import javafx.beans.property.SimpleBooleanProperty 4 | import javafx.beans.property.SimpleDoubleProperty 5 | import javafx.beans.property.SimpleObjectProperty 6 | import javafx.beans.property.SimpleStringProperty 7 | import tornadofx.* 8 | import java.time.LocalDateTime 9 | import java.util.* 10 | 11 | class DownloadTaskModel( 12 | val taskConfig: TaskConfig, 13 | createdAt: String? = null, 14 | checked: Boolean? = false, 15 | title: String? = null, 16 | size: String? = null, 17 | progress: Double? = 0.0, 18 | status: DownloadTaskStatus? = null) : ViewModel() { 19 | 20 | init { 21 | messages = ResourceBundle.getBundle("i18n/MainView") 22 | } 23 | 24 | val checkedProperty = SimpleBooleanProperty(this, "checked", checked ?: false) 25 | var checked: Boolean by checkedProperty 26 | val titleProperty = SimpleStringProperty(this, "title", title ?: messages["ui.analyzing"]) 27 | var title: String by titleProperty 28 | val sizeProperty = SimpleStringProperty(this, "size", size ?: messages["ui.analyzing"]) 29 | var size: String by sizeProperty 30 | val statusProperty = SimpleObjectProperty(this, "status", status 31 | ?: DownloadTaskStatus.ANALYZING) 32 | var status: DownloadTaskStatus by statusProperty 33 | val progressProperty = SimpleDoubleProperty(this, "progress", progress ?: 0.0) 34 | var progress: Double by progressProperty 35 | val createdAtProperty = SimpleStringProperty(this, "createdAt", createdAt) 36 | var createdAt: String by createdAtProperty 37 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/models/DownloadTaskStatus.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.models 2 | 3 | import java.io.Serializable 4 | 5 | enum class DownloadTaskStatus : Serializable { 6 | ANALYZING, 7 | DOWNLOADING, 8 | STOPPED, 9 | FAILED, 10 | COMPLETED, 11 | MERGING 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/models/DownloadTaskType.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.models 2 | 3 | import java.io.Serializable 4 | 5 | enum class DownloadTaskType : Serializable { 6 | SINGLE_MEDIA, 7 | PLAYLIST, 8 | ENGINE 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/models/MediaFormat.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.models 2 | 3 | data class MediaFormat( 4 | val title: String, 5 | val desc: String, 6 | val formatID: String, 7 | val format: String, 8 | val formatNote: String, 9 | val fileSize: Long, 10 | val ext: String) -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/models/TaskConfig.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.models 2 | 3 | import com.ingbyr.vdm.engines.utils.EngineType 4 | 5 | data class TaskConfig( 6 | var url: String, 7 | val engineType: EngineType, 8 | val downloadType: DownloadTaskType, 9 | val downloadDefaultFormat: Boolean, 10 | val storagePath: String, 11 | 12 | val cookie: String = "", 13 | val ffmpeg: String = "", 14 | var formatId: String = "", 15 | var proxyType: ProxyType = ProxyType.NONE, 16 | var proxyAddress: String = "", 17 | var proxyPort: String = "") { 18 | 19 | fun proxy(type: ProxyType, address: String, port: String) { 20 | this.proxyType = type 21 | this.proxyAddress = address 22 | this.proxyPort = port 23 | } 24 | } 25 | 26 | enum class ProxyType { 27 | SOCKS5, 28 | HTTP, 29 | NONE 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/stylesheets/DarkTheme.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.stylesheets 2 | 3 | import tornadofx.* 4 | 5 | class DarkTheme : Stylesheet() { 6 | companion object { 7 | var primaryColor = c("#37474F") 8 | } 9 | 10 | init { 11 | 12 | } 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/stylesheets/LightTheme.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.stylesheets 2 | 3 | import com.ingbyr.vdm.utils.Attributes 4 | import com.ingbyr.vdm.utils.config.app 5 | import javafx.scene.paint.Color 6 | import javafx.scene.text.FontWeight 7 | import tornadofx.* 8 | 9 | class LightTheme : Stylesheet() { 10 | // main color 11 | private val primaryColor = app.config.color(Attributes.THEME_PRIMARY_COLOR, Attributes.Defaults.THEME_PRIMARY_COLOR) 12 | private val secondaryColor = app.config.color(Attributes.THEME_SECONDARY_COLOR, Attributes.Defaults.THEME_SECONDARY_COLOR) 13 | 14 | 15 | companion object { 16 | // icon size 17 | private val regionSize = 1.5.em 18 | private val regionColor = Color.WHITE 19 | 20 | // id properties 21 | private val smallText by cssid() 22 | private val shortcutIcons by cssid() 23 | private val shortcutButton by cssid() 24 | private val buttonDownloadRegion by cssid() 25 | private val buttonPauseRegion by cssid() 26 | private val buttonPlayRegion by cssid() 27 | private val buttonDeleteRegion by cssid() 28 | private val buttonOpenRegion by cssid() 29 | private val buttonSearchRegion by cssid() 30 | private val buttonSettingRegion by cssid() 31 | private val buttonMoreMenus by cssid() 32 | private val labelOption by cssid() 33 | private val formatsListViewTitle by cssid() 34 | private val createDownloadTaskView by cssid() 35 | private val aboutView by cssid() 36 | private val mainView by cssid() 37 | private val preferencesView by cssid() 38 | private val mediaFormatsView by cssid() 39 | private val wizardView by cssid() 40 | private val wizardStepView by cssid() 41 | 42 | 43 | // class properties 44 | private val jfxTextField by cssclass() 45 | private val jfxButton by cssclass() 46 | private val jfxRippler by cssclass() 47 | private val jfxToggleButton by cssclass() 48 | private val jfxCheckBox by cssclass() 49 | private val jfxListView by cssclass() 50 | private val jfxProgressBar by cssclass() 51 | private val jfxComboBox by cssclass() 52 | private val jfxTabPane by cssclass() 53 | private val tabSelectedLine by cssclass() 54 | 55 | 56 | // custom jfx props 57 | private val jfxRipplerFill by cssproperty("-jfx-rippler-fill") 58 | private val jfxToogleColor by cssproperty("-jfx-toggle-color") 59 | private val jfxSize by cssproperty>("-jfx-size") 60 | private val jfxCheckedColor by cssproperty("-jfx-checked-color") 61 | private val jfxUncheckedColor by cssproperty("-jfx-unchecked-color") 62 | private val fxVerticalGap by cssproperty>("-fx-vertical-gap") 63 | private val jfxFocusColor by cssproperty("-jfx-focus-color") 64 | private val jfxUnfocusColor by cssproperty("-jfx-unfocus-color") 65 | } 66 | 67 | 68 | init { 69 | 70 | text { 71 | fontSize = 1.2.em 72 | } 73 | 74 | smallText { 75 | fontSize = 1.0.em 76 | } 77 | 78 | tooltip { 79 | fontSize = 1.0.em 80 | textFill = Color.WHITE 81 | backgroundColor += primaryColor 82 | } 83 | 84 | jfxTextField { 85 | fontSize = 1.0.em 86 | jfxFocusColor.value = primaryColor 87 | jfxUnfocusColor.value = secondaryColor 88 | } 89 | 90 | menu contains separator contains line { 91 | borderColor += box(primaryColor) 92 | } 93 | 94 | menuItem contains label { 95 | textFill = Color.WHITE 96 | } 97 | 98 | menuItem { 99 | backgroundColor += primaryColor 100 | } 101 | 102 | contextMenu { 103 | backgroundColor += primaryColor 104 | } 105 | 106 | menuItem and focused { 107 | backgroundColor += primaryColor 108 | } 109 | 110 | menuButton contains arrowButton { 111 | padding = box(0.0.em) 112 | } 113 | 114 | menuButton contains arrowButton contains arrow { 115 | padding = box(0.0.em) 116 | } 117 | 118 | // icons 119 | shortcutIcons { 120 | backgroundColor += primaryColor 121 | } 122 | 123 | shortcutButton { 124 | backgroundColor += primaryColor 125 | } 126 | 127 | buttonDownloadRegion { 128 | backgroundColor += regionColor 129 | minHeight = regionSize 130 | minWidth = regionSize 131 | maxHeight = regionSize 132 | maxWidth = regionSize 133 | shape = 134 | "M512 64q91 0 174 35 81 34 143 96t96 143q35 83 35 174t-35 174q-34 81-96 143t-143 96q-83 35-174 35t-174-35q-81-34-143-96T99 686q-35-83-35-174t35-174q34-81 96-143t143-96q83-35 174-35zm0-64Q373 0 255 68.5T68.5 255 0 512t68.5 257T255 955.5t257 68.5 257-68.5T955.5 769t68.5-257-68.5-257T769 68.5 512 0zm288 480H544V224q0-13-9.5-22.5T512 192t-22.5 9.5T480 224v256H224q-13 0-22.5 9.5T192 512t9.5 22.5T224 544h256v256q0 13 9.5 22.5T512 832t22.5-9.5T544 800V544h256q13 0 22.5-9.5T832 512t-9.5-22.5T800 480z" 135 | } 136 | 137 | buttonPauseRegion { 138 | backgroundColor += regionColor 139 | minHeight = regionSize 140 | minWidth = regionSize 141 | maxHeight = regionSize 142 | maxWidth = regionSize 143 | shape = 144 | "M352 768h-1q-13 0-22-9.5t-9-22.5V288q0-13 9-22.5t22-9.5h1q13 0 22.5 9.5T384 288v448q0 13-9.5 22.5T352 768zm321 0h-1q-13 0-22.5-9.5T640 736V288q0-13 9.5-22.5T672 256h1q13 0 22.5 9.5T705 288v448q0 13-9.5 22.5T673 768zM512 64q91 0 174 35 81 34 143 96t96 143q35 83 35 174t-35 174q-34 81-96 143t-143 96q-83 35-174 35t-174-35q-81-34-143-96T99 686q-35-83-35-174t35-174q34-81 96-143t143-96q83-35 174-35zm0-64Q373 0 255 68.5T68.5 255 0 512t68.5 257T255 955.5t257 68.5 257-68.5T955.5 769t68.5-257-68.5-257T769 68.5 512 0z" 145 | } 146 | 147 | buttonPlayRegion { 148 | backgroundColor += regionColor 149 | minHeight = regionSize 150 | minWidth = regionSize 151 | maxHeight = regionSize 152 | maxWidth = regionSize 153 | shape = 154 | "M512 64q91 0 174 35 81 34 143 96t96 143q35 83 35 174t-35 174q-34 81-96 143t-143 96q-83 35-174 35t-174-35q-81-34-143-96T99 686q-35-83-35-174t35-174q34-81 96-143t143-96q83-35 174-35zm0-64Q373 0 255 68.5T68.5 255 0 512t68.5 257T255 955.5t257 68.5 257-68.5T955.5 769t68.5-257-68.5-257T769 68.5 512 0zm-64 316l231 196-231 196V316zm-40-112q-9 0-16.5 6.5T384 226v572q0 9 7.5 15t17 6 16.5-6l335-285q8-7 8-16t-8-16L425 211q-7-7-17-7z" 155 | } 156 | 157 | buttonDeleteRegion { 158 | backgroundColor += regionColor 159 | minHeight = regionSize 160 | minWidth = regionSize 161 | maxHeight = regionSize 162 | maxWidth = regionSize 163 | shape = 164 | "M558 512l195-191q9-10 9.5-23t-8.5-23q-10-9-23-9t-23 9L512 467 316 275q-10-9-23-9t-23 10q-9 9-9 22.5t10 22.5l195 191-195 191q-9 10-9.5 23t8.5 23q10 9 23 9t23-9l196-192 196 192q10 9 23 9t23-10q9-9 9-22.5T753 703zM512 64q91 0 174 35 81 34 143 96t96 143q35 83 35 174t-35 174q-34 81-96 143t-143 96q-83 35-174 35t-174-35q-81-34-143-96T99 686q-35-83-35-174t35-174q34-81 96-143t143-96q83-35 174-35zm0-64Q373 0 255 68.5T68.5 255 0 512t68.5 257T255 955.5t257 68.5 257-68.5T955.5 769t68.5-257-68.5-257T769 68.5 512 0z" 165 | } 166 | 167 | buttonOpenRegion { 168 | backgroundColor += regionColor 169 | minHeight = regionSize 170 | minWidth = regionSize 171 | maxHeight = regionSize 172 | maxWidth = regionSize 173 | shape = 174 | "M405 64q5 0 11 5t7 10l19 77v1q5 23 26 39 20 17 45 17h437q10 0 10 11v597q0 5-3 8t-6 3H73q-9 0-9-11V75q0-11 9-11h332zm0-64H73Q43 0 21.5 22T0 75v746q0 31 21.5 53T73 896h878q30 0 51.5-22t21.5-53V224q0-31-21.5-53T951 149H514q-3 0-6-2.5t-4-4.5l-18-77q-7-28-30-46.5T405 0z" 175 | } 176 | 177 | buttonSearchRegion { 178 | backgroundColor += regionColor 179 | minHeight = regionSize 180 | minWidth = regionSize 181 | maxHeight = regionSize 182 | maxWidth = regionSize 183 | shape = 184 | "M1015 969L732 687q100-117 100-271 0-172-122-294T416 0 122 122 0 416t122 294 294 122q154 0 271-100l282 283q10 9 23 9t23-9q9-10 9-23t-9-23zM553 740q-65 28-137 28t-137-28q-63-26-112-75T92 553q-28-65-28-137t28-137q26-63 75-112t112-75q65-28 137-28t137 28q63 26 112 75t75 112q28 65 28 137t-28 137q-26 63-75 112t-112 75z" 185 | } 186 | 187 | buttonSettingRegion { 188 | backgroundColor += regionColor 189 | minHeight = regionSize 190 | minWidth = regionSize 191 | maxHeight = regionSize 192 | maxWidth = regionSize 193 | shape = 194 | "M512 64q26 0 54 4l16 46 10 31 32 9q32 10 62 26l29 15 29-15 43-21q44 34 78 77l-22 43-14 30 15 29q15 30 26 62l9 31 31 11 46 15q4 29 4 54v1q0 26-4 54l-46 16-31 10-10 32q-9 32-25 62l-16 29 15 29 22 43q-18 23-36 42h-1q-18 18-41 36l-43-22-29-15-29 16q-30 15-62 25l-32 10-10 31-15 46q-29 4-55 4t-55-4l-15-46-10-31-32-10q-33-10-62-25l-29-16-29 15-43 22q-23-18-42-36-18-19-36-42l22-43 15-29-16-29q-16-31-25-62l-10-32-31-10-46-15q-4-29-4-55t4-55l46-15 31-10 9-32q10-32 26-62l15-29-14-29-22-43q34-44 77-78l44 22 29 15 29-16q30-15 62-26l32-9 10-31 15-46q30-4 55-4zm0-64q-37 0-81 7l-22 4-28 83q-39 12-73 30l-78-39-18 13q-66 48-115 114l-13 18 40 78q-19 36-31 73l-83 28-3 22q-7 44-7 81t7 81l3 22 83 28q12 38 31 73l-39 78 13 18q24 34 52 62 26 26 62 52l18 13 78-39q36 19 73 30l28 84 22 3q44 7 81 7t81-7l22-3 27-84q38-11 74-30l78 39 18-13q35-26 62-52 27-27 52-62l13-18-39-79q18-35 30-73l83-27 4-22q7-44 7-81t-7-81l-4-22-83-28q-12-38-30-73l39-78-13-18q-49-67-115-115l-18-13-78 39q-35-18-73-30l-27-83-22-3q-44-7-81-7zm0 386q52 0 89 37t37 89-37 89-89 37-89-37-37-89 37-89 89-37zm0-64q-79 0-134.5 55.5T322 512t55.5 134.5T512 702t134.5-55.5T702 512t-55.5-134.5T512 322z" 195 | } 196 | 197 | buttonMoreMenus { 198 | backgroundColor += regionColor 199 | minHeight = regionSize 200 | minWidth = regionSize 201 | maxHeight = regionSize 202 | maxWidth = regionSize 203 | shape = 204 | "M192 32q0-13 9.5-22.5T224 0h768q13 0 22.5 9.5T1024 32q0 14-9.5 23T992 64H224q-13 0-22.5-9T192 32zM0 32Q0 18 9.5 9T32 0t22.5 9T64 32q0 13-9.5 22.5T32 64 9.5 54.5 0 32zm192 320q0-14 9.5-23t22.5-9h768q13 0 22.5 9t9.5 23q0 13-9.5 22.5T992 384H224q-13 0-22.5-9.5T192 352zM0 350q0-13 9.5-22.5T32 318t22.5 9.5T64 350q0 14-9.5 23T32 382t-22.5-9T0 350zm192 323q0-13 9.5-22.5T224 641h768q13 0 22.5 9.5t9.5 22.5-9.5 22.5T992 705H224q-13 0-22.5-9.5T192 673zM0 672q0-13 9.5-22.5T32 640t22.5 9.5T64 672t-9.5 22.5T32 704t-22.5-9.5T0 672z" 205 | } 206 | 207 | jfxButton { 208 | backgroundColor += secondaryColor 209 | textFill = Color.WHITE 210 | } 211 | 212 | jfxButton contains jfxRippler { 213 | jfxRipplerFill.value = primaryColor 214 | } 215 | 216 | jfxToggleButton { 217 | jfxToogleColor.value = primaryColor 218 | jfxSize.value = 0.5.em 219 | } 220 | 221 | jfxCheckBox { 222 | jfxCheckedColor.value = primaryColor 223 | jfxUncheckedColor.value = Color.BLACK 224 | } 225 | 226 | jfxListView { 227 | fxVerticalGap.value = 1.0.em 228 | } 229 | 230 | jfxListView contains listCell { 231 | textFill = Color.BLACK 232 | } 233 | 234 | jfxListView contains listCell and selected { 235 | backgroundColor += secondaryColor 236 | } 237 | 238 | tableView { 239 | borderColor += box(primaryColor) 240 | backgroundInsets += box(0.em) 241 | padding = box(0.em) 242 | } 243 | 244 | tableRowCell { 245 | borderColor += box(Color.LIGHTGREY) 246 | } 247 | 248 | tableRowCell and odd { 249 | backgroundColor += Color.LIGHTGREY 250 | } 251 | 252 | tableRowCell and selected { 253 | backgroundColor += secondaryColor 254 | } 255 | 256 | tableView contains columnHeader { 257 | backgroundColor += secondaryColor 258 | } 259 | 260 | tableView contains columnHeader contains label { 261 | textFill = Color.WHITE 262 | fontWeight = FontWeight.BOLD 263 | } 264 | 265 | jfxProgressBar contains bar { 266 | backgroundColor += primaryColor 267 | } 268 | 269 | jfxComboBox { 270 | jfxFocusColor.value = secondaryColor 271 | jfxUnfocusColor.value = Color.GREY 272 | } 273 | 274 | comboBoxPopup contains listView contains listCell and filled and selected { 275 | backgroundColor += secondaryColor 276 | } 277 | 278 | comboBoxPopup contains listView contains listCell and filled and selected and hover { 279 | backgroundColor += secondaryColor 280 | } 281 | 282 | comboBoxPopup contains listView contains listCell and filled and hover { 283 | backgroundColor += secondaryColor 284 | } 285 | 286 | labelOption { 287 | textFill = Color.WHITE 288 | } 289 | 290 | tabHeaderBackground { 291 | backgroundColor += primaryColor 292 | } 293 | 294 | tabLabel { 295 | fontSize = 1.0.em 296 | } 297 | 298 | jfxTabPane contains headersRegion child tab contains jfxRippler { 299 | jfxRipplerFill.value = secondaryColor 300 | } 301 | 302 | jfxTabPane contains headersRegion contains tabSelectedLine { 303 | backgroundColor += secondaryColor 304 | } 305 | 306 | splitPane and vertical contains splitPaneDivider { 307 | backgroundColor += secondaryColor 308 | padding = box(0.0.em, 0.01.em) 309 | } 310 | 311 | formatsListViewTitle { 312 | backgroundColor += primaryColor 313 | } 314 | 315 | formatsListViewTitle contains label { 316 | textFill = Color.WHITE 317 | } 318 | 319 | createDownloadTaskView { 320 | prefWidth = 600.px 321 | prefHeight = 200.px 322 | } 323 | 324 | aboutView { 325 | backgroundColor += primaryColor 326 | prefWidth = 400.px 327 | prefHeight = 300.px 328 | } 329 | 330 | aboutView contains label { 331 | textFill = Color.WHITE 332 | } 333 | 334 | mainView { 335 | prefWidth = 1200.px 336 | prefHeight = 400.px 337 | } 338 | 339 | preferencesView { 340 | prefWidth = 600.px 341 | prefHeight = 400.px 342 | } 343 | 344 | mediaFormatsView { 345 | prefWidth = 500.px 346 | prefHeight = 500.px 347 | } 348 | 349 | wizardView { 350 | prefWidth = 600.px 351 | prefHeight = 400.px 352 | } 353 | } 354 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/utils/Attributes.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.utils 2 | 3 | import com.ingbyr.vdm.engines.utils.EngineType 4 | import com.ingbyr.vdm.models.ProxyType 5 | import javafx.scene.paint.Color 6 | import tornadofx.* 7 | import java.nio.file.Files 8 | import java.nio.file.Path 9 | import java.nio.file.Paths 10 | 11 | object Attributes { 12 | // path app 13 | val APP_DIR: Path = Paths.get(System.getProperty("user.dir")) 14 | val ENGINES_DIR: Path = APP_DIR.resolve("engines") 15 | val CONFIG_DIR: Path = APP_DIR.resolve("configs") 16 | val DB_DIR: Path = APP_DIR.resolve("db") 17 | val TMP_DIR: Path = APP_DIR.resolve("tmp") 18 | val configFilePath: Path = CONFIG_DIR.resolve("vdm.properties") 19 | val engineConfigFilePath: Path = CONFIG_DIR.resolve("engines.properties") 20 | 21 | init { 22 | Files.createDirectories(CONFIG_DIR) 23 | Files.createDirectories(ENGINES_DIR) 24 | Files.createDirectories(DB_DIR) 25 | Files.createDirectories(TMP_DIR) 26 | } 27 | 28 | // database 29 | val DATABASE_URL = "jdbc:h2:${DB_DIR.resolve("vdm")}" 30 | 31 | 32 | const val FIRST_TIME_USE = "FIRST_TIME_USE" 33 | const val STORAGE_PATH = "STORAGE_PATH" 34 | const val FFMPEG_PATH = "FFMPEG_PATH" 35 | const val DOWNLOAD_DEFAULT_FORMAT = "DOWNLOAD_DEFAULT_FORMAT" 36 | const val ENGINE_TYPE = "ENGINE_TYPE" 37 | const val PROXY_TYPE = "PROXY_TYPE" 38 | const val SOCKS5_PROXY_ADDRESS = "SOCKS5_PROXY_ADDRESS" 39 | const val SOCKS5_PROXY_PORT = "SOCKS5_PROXY_PORT" 40 | const val HTTP_PROXY_ADDRESS = "HTTP_PROXY_ADDRESS" 41 | const val HTTP_PROXY_PORT = "HTTP_PROXY_PORT" 42 | const val ENABLE_COOKIE = "ENABLE_COOKIE" 43 | const val CURRENT_COOKIE = "CURRENT_COOKIE" 44 | const val DEBUG_MODE = "DEBUG_MODE" 45 | const val VDM_VERSION = "VDM_VERSION" 46 | const val THEME = "THEME" 47 | const val CHARSET = "CHARSET" 48 | const val THEME_PRIMARY_COLOR = "THEME_PRIMARY_COLOR" 49 | const val THEME_SECONDARY_COLOR = "THEME_SECONDARY_COLOR" 50 | // app content 51 | const val VDM_UPDATE_URL = "https://github.com/ingbyr/VDM/releases/latest" 52 | const val VDM_SOURCE_CODE = "https://github.com/ingbyr/VDM" 53 | const val VDM_LICENSE = "https://raw.githubusercontent.com/ingbyr/VDM/master/LICENSE.txt" 54 | const val VDM_REPORT_BUGS = "https://github.com/ingbyr/VDM/issues" 55 | const val UNKNOWN_VERSION = "0.0.0" 56 | const val DONATION_URL = "https://paypal.me/ingbyr" 57 | 58 | // youtube-dl 59 | const val YOUTUBE_DL_VERSION = "YOUTUBE_DL_VERSION" 60 | 61 | // annie 62 | const val ANNIE_VERSION = "ANNIE_VERSION" 63 | 64 | // Default value for above 65 | object Defaults { 66 | val STORAGE_PATH: String = Attributes.APP_DIR.toString() 67 | val FFMPEG_PATH: String = "" // todo ffmpeg 68 | const val DOWNLOAD_DEFAULT_FORMAT: Boolean = true 69 | val ENGINE_TYPE: EngineType = EngineType.YOUTUBE_DL 70 | const val ENGINE_VERSION: String = "0.0.0" 71 | const val SOCKS5_PROXY_ADDRESS: String = "" 72 | const val SOCKS5_PROXY_PORT: String = "" 73 | const val HTTP_PROXY_ADDRESS: String = "" 74 | const val HTTP_PROXY_PORT: String = "" 75 | val PROXY_TYPE: ProxyType = ProxyType.NONE 76 | const val ENABLE_COOKIE: Boolean = false 77 | const val COOKIE: String = "" 78 | val CHARSET: String = Charsets.UTF_8.name() 79 | const val FIRST_TIME_USE: Boolean = false 80 | val THEME_PRIMARY_COLOR: Color = c("#263238") 81 | val THEME_SECONDARY_COLOR: Color = c("#455A64") 82 | const val DEBUG_MODE: Boolean = false 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/utils/DBUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.utils 2 | 3 | import com.ingbyr.vdm.dao.* 4 | import com.ingbyr.vdm.models.DownloadTaskModel 5 | import com.ingbyr.vdm.models.TaskConfig 6 | import javafx.collections.ObservableList 7 | import org.jetbrains.exposed.sql.Database 8 | import org.jetbrains.exposed.sql.SchemaUtils 9 | import org.jetbrains.exposed.sql.StdOutSqlLogger 10 | import org.jetbrains.exposed.sql.addLogger 11 | import org.jetbrains.exposed.sql.transactions.transaction 12 | import org.slf4j.LoggerFactory 13 | 14 | object DBUtils { 15 | 16 | private val log = LoggerFactory.getLogger(DBUtils::class.java) 17 | 18 | init { 19 | Database.connect(Attributes.DATABASE_URL, driver = "org.h2.Driver", user = "vdm", password = "vdm") 20 | transaction { 21 | // addLogger(StdOutSqlLogger) // TODO enable db logger 22 | SchemaUtils.create(TaskConfigTable, DownloadTaskTable) 23 | } 24 | } 25 | 26 | 27 | fun saveDownloadTask(newDownloadTaskModel: DownloadTaskModel) { 28 | val newTaskConfig = newDownloadTaskModel.taskConfig 29 | transaction { 30 | val oldDownloadTask = DownloadTaskDAO.find { DownloadTaskTable.createdAt eq newDownloadTaskModel.createdAt }.firstOrNull() 31 | if (oldDownloadTask != null) { 32 | // update task app 33 | updateTaskConfigInDB(oldDownloadTask.taskConfig, newTaskConfig) 34 | // update download task model 35 | updateDownloadTaskModelInDB(oldDownloadTask, newDownloadTaskModel) 36 | } else { 37 | // create new one and save to db 38 | val taskConfigDB = createTaskConfigInDB(newTaskConfig) 39 | createDownloadTaskModelInDB(newDownloadTaskModel, taskConfigDB) 40 | } 41 | } 42 | 43 | } 44 | 45 | /** 46 | * must be wrapped in transaction block 47 | */ 48 | private fun updateTaskConfigInDB(oldTaskConfig: TaskConfigDAO, newTaskConfig: TaskConfig) { 49 | log.debug("update download task app data in db") 50 | // update task app 51 | oldTaskConfig.url = newTaskConfig.url 52 | oldTaskConfig.downloadType = newTaskConfig.downloadType.name 53 | oldTaskConfig.engineType = newTaskConfig.engineType.name 54 | oldTaskConfig.downloadDefaultFormat = newTaskConfig.downloadDefaultFormat 55 | oldTaskConfig.storagePath = newTaskConfig.storagePath 56 | oldTaskConfig.cookie = newTaskConfig.cookie 57 | oldTaskConfig.ffmpeg = newTaskConfig.ffmpeg 58 | oldTaskConfig.proxyType = newTaskConfig.proxyType.name 59 | oldTaskConfig.proxyAddress = newTaskConfig.proxyAddress 60 | oldTaskConfig.proxyPort = newTaskConfig.proxyPort 61 | } 62 | 63 | /** 64 | * must be wrapped in transaction block 65 | */ 66 | private fun updateDownloadTaskModelInDB(oldDownloadTaskModel: DownloadTaskDAO, newDownloadTaskModel: DownloadTaskModel) { 67 | log.debug("update download task data in db") 68 | oldDownloadTaskModel.checked = newDownloadTaskModel.checked 69 | oldDownloadTaskModel.title = newDownloadTaskModel.title 70 | oldDownloadTaskModel.size = newDownloadTaskModel.size 71 | oldDownloadTaskModel.status = newDownloadTaskModel.status.name 72 | oldDownloadTaskModel.progress = newDownloadTaskModel.progress.toFloat() 73 | oldDownloadTaskModel.createdAt = newDownloadTaskModel.createdAt 74 | } 75 | 76 | /** 77 | * must be wrapped in transaction block 78 | */ 79 | private fun createTaskConfigInDB(taskConfig: TaskConfig) = TaskConfigDAO.new { 80 | log.debug("create task app and save to db") 81 | url = taskConfig.url 82 | downloadType = taskConfig.downloadType.name 83 | engineType = taskConfig.engineType.name 84 | downloadDefaultFormat = taskConfig.downloadDefaultFormat 85 | storagePath = taskConfig.storagePath 86 | cookie = taskConfig.cookie 87 | ffmpeg = taskConfig.ffmpeg 88 | formatId = taskConfig.formatId 89 | proxyType = taskConfig.proxyType.name 90 | proxyAddress = taskConfig.proxyPort 91 | proxyPort = taskConfig.proxyPort 92 | } 93 | 94 | private fun createDownloadTaskModelInDB(downloadTask: DownloadTaskModel, taskConfigDAO: TaskConfigDAO) = DownloadTaskDAO.new { 95 | log.debug("create download task model and save to db") 96 | taskConfig = taskConfigDAO 97 | checked = downloadTask.checked 98 | title = downloadTask.title 99 | size = downloadTask.size 100 | status = downloadTask.status.name 101 | progress = downloadTask.progress.toFloat() 102 | createdAt = downloadTask.createdAt 103 | } 104 | 105 | 106 | fun deleteDownloadTask(downloadTask: DownloadTaskModel) { 107 | log.debug("delete download models from db") 108 | transaction { 109 | val downloadTaskDB = DownloadTaskDAO.find { 110 | DownloadTaskTable.createdAt eq downloadTask.createdAt 111 | }.firstOrNull() 112 | downloadTaskDB?.delete() 113 | } 114 | } 115 | 116 | fun loadAllDownloadTasks(downloadTaskModelList: ObservableList) { 117 | log.debug("load all download tasks") 118 | transaction { 119 | DownloadTaskDAO.all().sortedBy { it.createdAt }.forEach { 120 | downloadTaskModelList.add(it.toModel()) 121 | } 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/utils/DateTimeUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.utils 2 | 3 | import java.time.LocalDateTime 4 | import java.time.format.DateTimeFormatter 5 | 6 | object DateTimeUtils { 7 | private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS") 8 | 9 | fun now():String { 10 | return LocalDateTime.now().format(formatter) 11 | } 12 | 13 | fun time2String(dateTime: LocalDateTime): String { 14 | return dateTime.format(formatter) 15 | } 16 | 17 | fun string2time(dateTime: String): LocalDateTime { 18 | return LocalDateTime.parse(dateTime, formatter) 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/utils/DebugUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.utils 2 | 3 | import ch.qos.logback.classic.Level 4 | import org.slf4j.Logger 5 | import org.slf4j.LoggerFactory 6 | import tornadofx.* 7 | import java.util.* 8 | 9 | object DebugUtils { 10 | 11 | private val logger: Logger = LoggerFactory.getLogger(DebugUtils.javaClass) 12 | private val prop: Properties = System.getProperties() 13 | private val rootLogger:ch.qos.logback.classic.Logger = LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger 14 | 15 | fun changeDebugMode(enable: Boolean) { 16 | if (enable) { 17 | rootLogger.level = Level.DEBUG 18 | showOSInfo() 19 | }else { 20 | rootLogger.level = Level.ERROR 21 | } 22 | } 23 | 24 | fun showOSInfo() { 25 | logger.debug("OS: ${prop["os.name"]?.toString()} Arch: ${prop["os.arch"]?.toString()} Version: ${prop["os.version"]?.toString()}") 26 | logger.debug("JAVA: ${prop["java.version"]?.toString()} Vendor: ${prop["java.vendor"]?.toString()}") 27 | logger.debug("Default Locale: ${FX.locale} Current Locale:${Locale.getDefault().language}_${Locale.getDefault().country}") 28 | logger.debug("Save app file to ${Attributes.configFilePath}") 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/utils/FileCompressUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.utils 2 | 3 | import org.apache.commons.compress.archivers.ArchiveInputStream 4 | import org.apache.commons.compress.archivers.ArchiveStreamFactory 5 | import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream 6 | import org.apache.commons.io.FilenameUtils 7 | import org.apache.commons.io.IOUtils 8 | import org.slf4j.Logger 9 | import org.slf4j.LoggerFactory 10 | import java.io.File 11 | import java.nio.file.Files 12 | 13 | object FileCompressUtils { 14 | private val log: Logger = LoggerFactory.getLogger(FileCompressUtils::class.java) 15 | 16 | fun decompress(sourceFile: File, destFile: File) { 17 | val fileExtension = FilenameUtils.getExtension(sourceFile.name) 18 | log.debug("decompress the ${sourceFile.name} as $fileExtension") 19 | when (fileExtension) { 20 | "gz" -> decompressGz(sourceFile, destFile) 21 | else -> decompressUsualFile(sourceFile, destFile, fileExtension) 22 | } 23 | } 24 | 25 | 26 | /** 27 | * Decompress tar.gz file. tar.gz -> tar -> decompressed file 28 | */ 29 | private fun decompressGz(sourceFile: File, destFile: File) { 30 | val tmpTarFile = Attributes.TMP_DIR.resolve("tmp.tar").toFile() 31 | GzipCompressorInputStream(sourceFile.inputStream().buffered()).use { gis -> 32 | IOUtils.copy(gis, tmpTarFile.outputStream()) 33 | } 34 | 35 | decompressUsualFile(tmpTarFile, destFile, ArchiveStreamFactory.TAR) 36 | Files.delete(tmpTarFile.toPath()) 37 | } 38 | 39 | /** 40 | * Decompress usual compressed files. 41 | */ 42 | private fun decompressUsualFile(sourceFile: File, destFile: File, fileType: String? = null) { 43 | if (fileType.isNullOrBlank()) { // try to decompress unknown file 44 | ArchiveStreamFactory().createArchiveInputStream(sourceFile.inputStream()).use { ais -> 45 | readArchiveStreamAndSave(ais, destFile) 46 | } 47 | } else { 48 | ArchiveStreamFactory().createArchiveInputStream(fileType, sourceFile.inputStream()).use { ais -> 49 | readArchiveStreamAndSave(ais, destFile) 50 | } 51 | } 52 | } 53 | 54 | private fun readArchiveStreamAndSave(ais: ArchiveInputStream, destFile: File) { 55 | val entry = ais.nextEntry 56 | log.debug("decompress ${entry.name} (size: ${entry.size})") 57 | IOUtils.copy(ais, destFile.outputStream()) 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/utils/FileEditorOption.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.utils 2 | 3 | import java.nio.file.Path 4 | 5 | data class FileEditorOption( 6 | val filePath: Path, 7 | val isNewFile: Boolean, 8 | val extension: String 9 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/utils/NetUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.utils 2 | 3 | import com.ingbyr.vdm.events.RefreshEngineVersion 4 | import com.ingbyr.vdm.models.DownloadTaskModel 5 | import com.ingbyr.vdm.models.DownloadTaskStatus 6 | import okhttp3.OkHttpClient 7 | import okhttp3.Request 8 | import okio.BufferedSource 9 | import okio.Okio 10 | import org.slf4j.Logger 11 | import org.slf4j.LoggerFactory 12 | import tornadofx.* 13 | import java.io.IOException 14 | import java.nio.file.Files 15 | import java.nio.file.Path 16 | import java.nio.file.Paths 17 | import java.nio.file.attribute.PosixFilePermission 18 | import java.text.DecimalFormat 19 | import java.util.* 20 | 21 | 22 | class NetUtils : Controller() { 23 | init { 24 | messages = ResourceBundle.getBundle("i18n/MainView") 25 | } 26 | 27 | private val client = OkHttpClient() 28 | private val logger: Logger = LoggerFactory.getLogger(NetUtils::class.java) 29 | 30 | fun get(url: String): String? { 31 | val request = Request.Builder().url(url).build() 32 | val response = client.newCall(request).execute() 33 | return response.body()?.string() 34 | } 35 | 36 | fun downloadEngine(downloadTaskModel: DownloadTaskModel, 37 | remoteVersion: String, 38 | needUnzip: Boolean = false) { 39 | downloadTaskModel.status = DownloadTaskStatus.ANALYZING 40 | try { 41 | val request = Request.Builder().url(downloadTaskModel.taskConfig.url).build() 42 | val response = client.newCall(request).execute() 43 | val body = response.body() 44 | if (body != null) { 45 | downloadTaskModel.status = DownloadTaskStatus.DOWNLOADING 46 | val contentLength = body.contentLength() 47 | val sizeFormat = DecimalFormat("#.##") 48 | downloadTaskModel.size = "${sizeFormat.format(contentLength / 1000000.0)}MB" 49 | val storagePath = Paths.get(downloadTaskModel.taskConfig.storagePath) 50 | 51 | if (needUnzip) { 52 | val tmpFileName = downloadTaskModel.taskConfig.url.split("/").last() 53 | val tmpFile: Path = Attributes.TMP_DIR.resolve(tmpFileName) 54 | saveBufferData(body.source(), tmpFile, downloadTaskModel, contentLength) 55 | // unzip tmp file and clear it 56 | FileCompressUtils.decompress(tmpFile.toFile(), storagePath.toFile()) 57 | Files.delete(tmpFile) 58 | } else { 59 | saveBufferData(body.source(), storagePath, downloadTaskModel, contentLength) 60 | } 61 | 62 | downloadTaskModel.status = DownloadTaskStatus.COMPLETED 63 | 64 | // add execution permission to the engines file 65 | if (OSUtils.currentOS == OSType.LINUX || OSUtils.currentOS == OSType.MAC_OS) { 66 | Files.setPosixFilePermissions( 67 | storagePath, 68 | mutableSetOf( 69 | PosixFilePermission.OWNER_READ, 70 | PosixFilePermission.OWNER_WRITE, 71 | PosixFilePermission.OWNER_EXECUTE, 72 | PosixFilePermission.GROUP_EXECUTE, 73 | PosixFilePermission.OTHERS_EXECUTE)) 74 | } 75 | // update ui 76 | fire(RefreshEngineVersion(downloadTaskModel.taskConfig.engineType, remoteVersion)) 77 | } else { 78 | logger.error("no response body") 79 | } 80 | } catch (e: IOException) { 81 | downloadTaskModel.status = DownloadTaskStatus.FAILED 82 | logger.error(e.toString()) 83 | } 84 | } 85 | 86 | private fun saveBufferData(source: BufferedSource, dest: Path, 87 | downloadTaskModel: DownloadTaskModel, contentLength: Long) { 88 | logger.debug("saving data to $dest") 89 | 90 | Okio.buffer(Okio.sink(dest)).use { sink -> 91 | source.use { source -> 92 | val sinkBuffer = sink.buffer() 93 | var totalBytesRead: Long = 0 94 | val bufferSize: Long = 2 * 1024 95 | var bytesRead: Long 96 | while (true) { 97 | bytesRead = source.read(sinkBuffer, bufferSize) 98 | if (bytesRead == -1L) break 99 | sink.emit() 100 | totalBytesRead += bytesRead 101 | downloadTaskModel.progress = totalBytesRead.toDouble() / contentLength.toDouble() 102 | } 103 | sink.flush() 104 | } 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/utils/OSUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.utils 2 | 3 | import org.slf4j.Logger 4 | import org.slf4j.LoggerFactory 5 | import java.awt.Desktop 6 | import java.nio.file.Paths 7 | 8 | enum class OSType { 9 | WINDOWS, 10 | LINUX, 11 | MAC_OS, 12 | } 13 | 14 | object OSUtils { 15 | private val logger: Logger = LoggerFactory.getLogger(this::class.java) 16 | private val platform = System.getProperties()["os.name"]?.toString() ?: "Unknown" 17 | val currentOS: OSType = when { 18 | platform.startsWith("Linux") -> OSType.LINUX 19 | platform.startsWith("Win") -> OSType.WINDOWS 20 | platform.startsWith("Mac") -> OSType.MAC_OS 21 | else -> throw OSException("not supported os") 22 | } 23 | 24 | fun openDir(pathStr: String) { 25 | val file = Paths.get(pathStr).toFile() 26 | when (currentOS) { 27 | OSType.LINUX -> { 28 | Runtime.getRuntime().exec("xdg-open $file") 29 | } 30 | OSType.WINDOWS -> { 31 | Desktop.getDesktop().open(file) 32 | } 33 | OSType.MAC_OS -> { 34 | Desktop.getDesktop().open(file) 35 | } 36 | } 37 | } 38 | } 39 | 40 | class OSException(override var message: String) : Exception(message) -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/utils/UpdateUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.utils 2 | 3 | import org.slf4j.LoggerFactory 4 | import kotlin.math.max 5 | 6 | object UpdateUtils { 7 | 8 | private val log = LoggerFactory.getLogger(UpdateUtils::class.java) 9 | 10 | fun check(localVersion: String, remoteVersion: String): Boolean { 11 | try { 12 | val lv: List = localVersion.split(".").map { it.toInt() } 13 | val rv: List = remoteVersion.split(".").map { it.toInt() } 14 | val count = max(lv.size, rv.size) 15 | for (i in 0 until count) { 16 | if (rv.getOrElse(i) { 0 } - lv.getOrElse(i) { 0 } > 0) return true 17 | else if (rv.getOrElse(i) { 0 } - lv.getOrElse(i) { 0 } < 0) return false 18 | } 19 | } catch (e: Exception) { 20 | log.error(e.toString()) 21 | } 22 | return false 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/utils/config/ConfigUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.utils.config 2 | 3 | import com.ingbyr.vdm.engines.utils.EngineType 4 | import com.ingbyr.vdm.models.ProxyType 5 | import com.ingbyr.vdm.utils.Attributes 6 | import javafx.scene.paint.Color 7 | import org.slf4j.Logger 8 | import org.slf4j.LoggerFactory 9 | import tornadofx.* 10 | 11 | /** 12 | * Extension app helper 13 | */ 14 | fun ConfigProperties.engine(key: String, defaultValue: EngineType): EngineType { 15 | return try { 16 | EngineType.valueOf(this.string(key).toString()) 17 | } catch (e: IllegalArgumentException) { 18 | val logger: Logger = LoggerFactory.getLogger(Attributes.javaClass) 19 | logger.error("not fount engine type: ${this.string(key).toString()}") 20 | this.update(key, defaultValue.name) 21 | defaultValue 22 | } 23 | } 24 | 25 | fun ConfigProperties.proxy(key: String, defaultValue: ProxyType = ProxyType.NONE): ProxyType { 26 | return try { 27 | ProxyType.valueOf(this.string(key).toString()) 28 | } catch (e: IllegalArgumentException) { 29 | val logger: Logger = LoggerFactory.getLogger(Attributes.javaClass) 30 | logger.error("not fount proxy type: ${this.string(key).toString()}") 31 | this.update(key, defaultValue.name) 32 | defaultValue 33 | } 34 | } 35 | 36 | fun ConfigProperties.color(key: String, defaultValue: Color = Color.RED): Color { 37 | val color = this.string(key) 38 | return if (color != null) { 39 | c(color) 40 | } else { 41 | defaultValue 42 | } 43 | } 44 | 45 | fun ConfigProperties.update(k: String, v: Any) { 46 | with(this) { 47 | set(k, v.toString()) 48 | save() 49 | } 50 | } 51 | 52 | 53 | // Config patcher because that no app in some class 54 | object app { 55 | 56 | object config : Controller() { 57 | fun color(key: String, defaultValue: Color = Color.RED): Color = config.color(key, defaultValue) 58 | fun string(key: String, defaultValue: String) = config.string(key, defaultValue) 59 | } 60 | 61 | } 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/views/AboutView.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.views 2 | 3 | import com.ingbyr.vdm.utils.Attributes 4 | import com.jfoenix.controls.JFXButton 5 | import javafx.scene.control.Label 6 | import javafx.scene.layout.VBox 7 | import tornadofx.* 8 | import java.util.* 9 | 10 | class AboutView : View() { 11 | init { 12 | messages = ResourceBundle.getBundle("i18n/AboutView") 13 | title = messages["ui.about"] 14 | } 15 | 16 | override val root: VBox by fxml("/fxml/AboutView.fxml") 17 | private val labelVersion: Label by fxid() 18 | private val labelLicense: Label by fxid() 19 | private val labelSourceCode: Label by fxid() 20 | private val btnUpdate: JFXButton by fxid() 21 | private val btnReport: JFXButton by fxid() 22 | 23 | init { 24 | labelVersion.text = app.config.string(Attributes.VDM_VERSION) 25 | initListeners() 26 | } 27 | 28 | private fun initListeners() { 29 | labelLicense.setOnMouseClicked { 30 | hostServices.showDocument(Attributes.VDM_LICENSE) 31 | } 32 | labelSourceCode.setOnMouseClicked { 33 | hostServices.showDocument(Attributes.VDM_SOURCE_CODE) 34 | } 35 | btnUpdate.setOnMouseClicked { 36 | hostServices.showDocument(Attributes.VDM_UPDATE_URL) 37 | } 38 | btnReport.setOnMouseClicked { 39 | hostServices.showDocument(Attributes.VDM_REPORT_BUGS) 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/views/CreateDownloadTaskView.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.views 2 | 3 | import com.ingbyr.vdm.controllers.CreateDownloadTaskController 4 | import com.ingbyr.vdm.events.CreateDownloadTask 5 | import com.ingbyr.vdm.utils.Attributes 6 | import com.ingbyr.vdm.utils.DateTimeUtils 7 | import com.ingbyr.vdm.utils.config.update 8 | import com.jfoenix.controls.JFXButton 9 | import com.jfoenix.controls.JFXTextField 10 | import javafx.scene.control.Label 11 | import javafx.scene.layout.VBox 12 | import javafx.stage.DirectoryChooser 13 | import tornadofx.* 14 | import java.util.* 15 | 16 | 17 | class CreateDownloadTaskView : View() { 18 | init { 19 | messages = ResourceBundle.getBundle("i18n/CreateDownloadTaskView") 20 | title = messages["ui.create"] 21 | } 22 | 23 | override val root: VBox by fxml("/fxml/CreateDownloadTaskView.fxml") 24 | 25 | private val tfURL: JFXTextField by fxid() 26 | private val btnMoreSettings: JFXButton by fxid() 27 | private val btnConfirm: JFXButton by fxid() 28 | private val labelStoragePath: Label by fxid() 29 | private val btnChangeStoragePath: JFXButton by fxid() 30 | 31 | private val controller: CreateDownloadTaskController by inject() 32 | 33 | init { 34 | loadVDMConfig() 35 | initListeners() 36 | addValidation() 37 | } 38 | 39 | private fun loadVDMConfig() { 40 | labelStoragePath.text = app.config.string(Attributes.STORAGE_PATH) 41 | } 42 | 43 | private fun initListeners() { 44 | btnMoreSettings.setOnMouseClicked { 45 | find(PreferencesView::class).openWindow() 46 | } 47 | 48 | btnConfirm.setOnMouseClicked { 49 | val downloadTask = controller.createDownloadTaskInstance(tfURL.text) 50 | if (controller.downloadDefaultFormat) { 51 | // create download task directly 52 | downloadTask.createdAt = DateTimeUtils.now() 53 | fire(CreateDownloadTask(downloadTask)) 54 | } else { 55 | find(mapOf("downloadTask" to downloadTask)).openWindow() 56 | } 57 | this.close() 58 | } 59 | 60 | btnChangeStoragePath.setOnMouseClicked { 61 | val file = DirectoryChooser().showDialog(primaryStage) 62 | file?.apply { 63 | val newPath = this.absoluteFile.toString() 64 | config.update(Attributes.STORAGE_PATH, newPath) 65 | labelStoragePath.text = newPath 66 | } 67 | } 68 | } 69 | 70 | private fun addValidation() { 71 | // validation for the url text field 72 | ValidationContext().addValidator(tfURL, tfURL.textProperty()) { 73 | if (!it!!.startsWith("http")) error(messages["inputCorrectURL"]) else null 74 | } 75 | } 76 | 77 | override fun onUndock() { 78 | super.onUndock() 79 | tfURL.text = "" 80 | } 81 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/views/DonationView.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.views 2 | 3 | import com.ingbyr.vdm.utils.Attributes 4 | import tornadofx.* 5 | 6 | 7 | class DonationView : View() { 8 | override val root = hbox { 9 | spacing = 10.0 10 | imageview("/imgs/zhifubao-min.png") 11 | imageview("/imgs/paypal-min.png").setOnMouseClicked { 12 | hostServices.showDocument(Attributes.DONATION_URL) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/views/MainView.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.views 2 | 3 | import com.ingbyr.vdm.controllers.MainController 4 | import com.ingbyr.vdm.controllers.ThemeController 5 | import com.ingbyr.vdm.models.DownloadTaskModel 6 | import com.ingbyr.vdm.models.DownloadTaskStatus 7 | import com.ingbyr.vdm.utils.Attributes 8 | import com.ingbyr.vdm.utils.OSUtils 9 | import com.ingbyr.vdm.utils.config.update 10 | import com.jfoenix.controls.JFXButton 11 | import com.jfoenix.controls.JFXProgressBar 12 | import javafx.scene.control.* 13 | import javafx.scene.layout.ColumnConstraints 14 | import javafx.scene.layout.GridPane 15 | import javafx.scene.layout.VBox 16 | import tornadofx.* 17 | import java.text.DecimalFormat 18 | import java.util.* 19 | 20 | 21 | class MainView : View() { 22 | init { 23 | messages = ResourceBundle.getBundle("i18n/MainView") 24 | title = messages["ui.vdm"] 25 | } 26 | 27 | private val vdmVersion = "0.4.0" 28 | override val root: VBox by fxml("/fxml/MainView.fxml") 29 | private val controller: MainController by inject() 30 | private val themeController: ThemeController by inject() 31 | 32 | private val btnNew: JFXButton by fxid() 33 | private val btnStart: JFXButton by fxid() 34 | private val btnStop: JFXButton by fxid() 35 | private val btnDelete: JFXButton by fxid() 36 | private val btnOpenFile: JFXButton by fxid() 37 | private val btnSearch: JFXButton by fxid() 38 | private val btnPreferences: JFXButton by fxid() 39 | private val btnMenu: JFXButton by fxid() 40 | private val contextMenu: ContextMenu = ContextMenu() 41 | 42 | private var menuNew: MenuItem 43 | private var menuOpenDir: MenuItem 44 | private var menuStartAllTask: MenuItem 45 | private var menuStopAllTask: MenuItem 46 | private var menuPreferences: MenuItem 47 | private var menuAbout: MenuItem 48 | private var menuQuit: MenuItem 49 | private var menuDonate: MenuItem 50 | 51 | private var selectedTaskModel: DownloadTaskModel? = null 52 | private var downloadTaskTableView: TableView 53 | 54 | init { 55 | themeController.initTheme() 56 | downloadTaskTableView = tableview(controller.downloadTaskModelList) { 57 | fitToParentSize() 58 | columnResizePolicy = SmartResize.POLICY 59 | column(messages["ui.title"], DownloadTaskModel::titleProperty).remainingWidth() 60 | column(messages["ui.size"], DownloadTaskModel::sizeProperty) 61 | column(messages["ui.status"], DownloadTaskModel::statusProperty).cellFormat { 62 | val labelStatus = Label() 63 | when (it!!) { 64 | DownloadTaskStatus.COMPLETED -> labelStatus.text = messages["ui.completed"] 65 | DownloadTaskStatus.STOPPED -> labelStatus.text = messages["ui.stopped"] 66 | DownloadTaskStatus.MERGING -> labelStatus.text = messages["ui.merging"] 67 | DownloadTaskStatus.ANALYZING -> labelStatus.text = messages["ui.analyzing"] 68 | DownloadTaskStatus.DOWNLOADING -> labelStatus.text = messages["ui.downloading"] 69 | DownloadTaskStatus.FAILED -> labelStatus.text = messages["ui.failed"] 70 | } 71 | graphic = labelStatus 72 | } 73 | column(messages["ui.progress"], DownloadTaskModel::progressProperty).pctWidth(20).cellFormat { 74 | val progressFormat = DecimalFormat("#.##") 75 | val progressPane = GridPane() 76 | val progressBar = JFXProgressBar(it.toDouble()) 77 | val progressLabel = Label(progressFormat.format(it.toDouble() * 100) + "%") 78 | progressPane.useMaxSize = true 79 | progressPane.add(progressBar, 0, 0) 80 | progressPane.add(progressLabel, 1, 0) 81 | val columnBar = ColumnConstraints() 82 | columnBar.percentWidth = 75.0 83 | val columnLabel = ColumnConstraints() 84 | columnLabel.percentWidth = 25.0 85 | progressPane.columnConstraints.addAll(columnBar, columnLabel) 86 | progressPane.hgap = 10.0 87 | progressBar.useMaxSize = true 88 | progressLabel.useMaxWidth = true 89 | graphic = progressPane 90 | } 91 | column(messages["ui.createdAt"], DownloadTaskModel::createdAtProperty) 92 | 93 | contextmenu { 94 | item(messages["ui.stopTask"]).action { 95 | selectedTaskModel?.run { controller.stopTask(this) } 96 | } 97 | item(messages["ui.startTask"]).action { 98 | selectedTaskModel?.run { controller.startTask(this) } 99 | } 100 | item(messages["ui.deleteTask"]).action { 101 | selectedTaskModel?.run { controller.deleteTask(this) } 102 | } 103 | } 104 | } 105 | root += downloadTaskTableView 106 | downloadTaskTableView.placeholder = Label(messages["ui.noTaskInList"]) 107 | 108 | // init context menu 109 | menuNew = MenuItem(messages["ui.new"]) 110 | menuOpenDir = MenuItem(messages["ui.openDirectory"]) 111 | menuStartAllTask = MenuItem(messages["ui.startAllTask"]) 112 | menuStopAllTask = MenuItem(messages["ui.stopAllTask"]) 113 | menuPreferences = MenuItem(messages["ui.preferences"]) 114 | menuAbout = MenuItem(messages["ui.about"]) 115 | menuQuit = MenuItem(messages["ui.quit"]) 116 | menuDonate = MenuItem(messages["ui.donate"]) 117 | contextMenu.items.addAll( 118 | menuNew, 119 | menuOpenDir, 120 | menuStartAllTask, 121 | menuStopAllTask, 122 | SeparatorMenuItem(), 123 | menuPreferences, 124 | menuAbout, 125 | menuDonate, 126 | SeparatorMenuItem(), 127 | menuQuit 128 | ) 129 | loadVDMConfig() 130 | initListeners() 131 | controller.loadTaskFromDB() 132 | } 133 | 134 | private fun loadVDMConfig() { 135 | // create the app.app file when first time use VDM 136 | val firstTimeUse = app.config.boolean(Attributes.FIRST_TIME_USE, Attributes.Defaults.FIRST_TIME_USE) 137 | if (firstTimeUse) { 138 | // init app.app file 139 | app.config.update(Attributes.VDM_VERSION, vdmVersion) 140 | 141 | find(PreferencesView::class).openWindow()?.hide() 142 | // find(WizardView::class).openWindow(stageStyle = StageStyle.UNDECORATED)?.isAlwaysOnTop = true // todo use this 143 | find(WizardView::class).openWindow()?.isAlwaysOnTop = true // make sure wizard is always on top 144 | // ConfigUtils.update(Attributes.FIRST_TIME_USE, "false") // TODO uncomment this 145 | } else { 146 | app.config.update(Attributes.VDM_VERSION, vdmVersion) 147 | } 148 | } 149 | 150 | private fun initListeners() { 151 | // models list view 152 | downloadTaskTableView.selectionModel.selectedItemProperty().addListener { _, _, selectedItem -> 153 | selectedTaskModel = selectedItem 154 | } 155 | 156 | // shortcut buttons 157 | // start models 158 | btnStart.setOnMouseClicked { _ -> 159 | selectedTaskModel?.let { controller.startTask(it) } 160 | } 161 | // preferences view 162 | btnPreferences.setOnMouseClicked { 163 | find(PreferencesView::class).openWindow() 164 | } 165 | // create models 166 | btnNew.setOnMouseClicked { 167 | find(CreateDownloadTaskView::class).openWindow() 168 | } 169 | // delete models 170 | btnDelete.setOnMouseClicked { 171 | selectedTaskModel?.run { controller.deleteTask(this) } 172 | } 173 | // stop models 174 | btnStop.setOnMouseClicked { 175 | selectedTaskModel?.run { controller.stopTask(this) } 176 | } 177 | // open dir 178 | btnOpenFile.setOnMouseClicked { 179 | if (selectedTaskModel != null) { 180 | OSUtils.openDir(selectedTaskModel!!.taskConfig.storagePath) 181 | } else { 182 | OSUtils.openDir(app.config.string(Attributes.STORAGE_PATH, Attributes.Defaults.STORAGE_PATH)) 183 | } 184 | } 185 | // TODO search models 186 | btnSearch.isVisible = false 187 | btnSearch.setOnMouseClicked { 188 | } 189 | 190 | // menus 191 | btnMenu.setOnMouseClicked { 192 | contextMenu.show(primaryStage, it.screenX, it.screenY) 193 | } 194 | menuNew.action { 195 | find(CreateDownloadTaskView::class).openWindow() 196 | } 197 | menuOpenDir.action { 198 | if (selectedTaskModel != null) { 199 | OSUtils.openDir(selectedTaskModel!!.taskConfig.storagePath) 200 | } else { 201 | OSUtils.openDir(app.config.string(Attributes.STORAGE_PATH, Attributes.Defaults.STORAGE_PATH)) 202 | } 203 | } 204 | menuStartAllTask.action { 205 | controller.startAllTask() 206 | } 207 | menuStopAllTask.action { 208 | controller.stopAllTask() 209 | } 210 | menuPreferences.action { 211 | find(PreferencesView::class).openWindow() 212 | } 213 | menuAbout.action { 214 | find(AboutView::class).openWindow() 215 | } 216 | menuQuit.action { 217 | this.close() 218 | } 219 | menuDonate.action { 220 | openInternalWindow(DonationView::class) 221 | } 222 | } 223 | 224 | override fun onUndock() { 225 | super.onUndock() 226 | controller.clear() 227 | selectedTaskModel = null 228 | } 229 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ingbyr/vdm/views/MediaFormatsView.kt: -------------------------------------------------------------------------------- 1 | package com.ingbyr.vdm.views 2 | 3 | import com.ingbyr.vdm.controllers.MediaFormatsController 4 | import com.ingbyr.vdm.events.CreateDownloadTask 5 | import com.ingbyr.vdm.models.DownloadTaskModel 6 | import com.ingbyr.vdm.models.MediaFormat 7 | import com.ingbyr.vdm.utils.DateTimeUtils 8 | import com.jfoenix.controls.JFXListView 9 | import javafx.scene.control.Label 10 | import javafx.scene.layout.VBox 11 | import org.slf4j.Logger 12 | import org.slf4j.LoggerFactory 13 | import tornadofx.* 14 | import java.util.* 15 | 16 | 17 | class MediaFormatsView : View() { 18 | 19 | init { 20 | messages = ResourceBundle.getBundle("i18n/MediaFormatsView") 21 | title = messages["ui.mediaList"] 22 | } 23 | 24 | override val root: VBox by fxml("/fxml/MediaFormatsView.fxml") 25 | private val controller: MediaFormatsController by inject() 26 | private val logger: Logger = LoggerFactory.getLogger(this::class.java) 27 | 28 | private val labelTitle: Label by fxid() 29 | private val labelDesc: Label by fxid() 30 | private val listView: JFXListView