diff --git a/appinfo/info.xml b/appinfo/info.xml index a7972a4..48c43e0 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -5,7 +5,7 @@ Aria2 and youtube-dl web gui for nextcloud built-in torrent search;Start and stop Aria2 process, manage Aria2 from the web; Download videos from youtube, twitter and other sites; - 0.1.3 + 0.1.4 agpl jiaxinhuang NCDownloader diff --git a/css/style.css b/css/style.css index a5502fa..0481d96 100644 --- a/css/style.css +++ b/css/style.css @@ -11,6 +11,7 @@ a { display : flex; flex-flow: row nowrap; } + #ncdownloader-form-wrapper .form-input-wrapper select, #ncdownloader-form-wrapper .form-input-wrapper input { justify-content: left; @@ -26,6 +27,7 @@ a:focus { outline : 5px auto -webkit-focus-ring-color; outline-offset: -2px; } + div .number { background : none repeat scroll 0 0 #5f5853; border-radius: 999px; @@ -39,9 +41,13 @@ div .number { padding : 0 8px; } +#ncdownloader-settings-form input[type=text] { + width: 160px; +} + #ncdownloader-form-wrapper input[type=text] { padding: 0px 5px; - flex:auto; + flex : auto; } #ncdownloader-table-data .table-cell { @@ -161,6 +167,18 @@ div .number { color: red; } +.checkboxes label { + display: inline-block; + padding-right: 10px; + white-space: nowrap; + } + .checkboxes input { + vertical-align: middle; + } + .checkboxes label span { + vertical-align: middle; + } + @media only screen and (min-width: 800px) {} @media only screen and (max-width: 1024px) { @@ -168,6 +186,9 @@ div .number { position : relative; margin-top: 2em; } + #ncdownloader-settings-form input[type=text] { + width: 135px; + } } diff --git a/lib/Controller/MainController.php b/lib/Controller/MainController.php index 0c8bb5a..f917bd9 100644 --- a/lib/Controller/MainController.php +++ b/lib/Controller/MainController.php @@ -86,7 +86,7 @@ class MainController extends Controller 'data' => serialize(['link' => $url]), ]; $this->dbconn->save($data); - $resp = ['gid' => $result, 'file' => $filename, 'result' => $result]; + $resp = ['message' => $filename, 'result' => $result,'file' => $filename]; return $resp; } diff --git a/lib/Controller/YoutubeController.php b/lib/Controller/YoutubeController.php index 2d74b95..c3ff4ba 100644 --- a/lib/Controller/YoutubeController.php +++ b/lib/Controller/YoutubeController.php @@ -34,7 +34,10 @@ class YoutubeController extends Controller $this->aria2->init(); $this->tablename = $this->dbconn->queryBuilder->getTableName("ncdownloader_info"); } - + /** + * @NoAdminRequired + * + */ public function Index() { $data = $this->dbconn->getYoutubeByUid($this->uid); @@ -66,12 +69,12 @@ class YoutubeController extends Controller $resp['counter'] = ['youtube-dl' => count($data)]; return new JSONResponse($resp); } - public function Download() { $params = array(); $url = trim($this->request->getParam('form_input_text')); $yt = $this->youtube; + $yt->audioOnly = (bool) $this->request->getParam('audioOnly'); if (!$yt->isInstalled()) { return new JSONResponse($this->installYTD()); } @@ -102,7 +105,7 @@ class YoutubeController extends Controller } if ($this->dbconn->deleteByGid($gid)) { - return new JSONResponse(['message' => $gid . " deleted!"]); + return new JSONResponse(['message' => $gid . " Deleted!"]); } } @@ -131,7 +134,7 @@ class YoutubeController extends Controller 'data' => serialize(['link' => $url]), ]; $this->dbconn->save($data); - $resp = ['gid' => $result, 'file' => $filename, 'result' => $result]; + $resp = ['message' => $filename, 'result' => $result]; return $resp; } diff --git a/lib/Tools/Helper.php b/lib/Tools/Helper.php index 22ce44f..20f3ef1 100644 --- a/lib/Tools/Helper.php +++ b/lib/Tools/Helper.php @@ -284,5 +284,9 @@ class Helper } return sprintf('%04x%04x%04x%04x%04x%04x%04x%04x', mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(16384, 20479), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(16384, 20479)); } + public static function ffmpegInstalled() + { + return (bool) self::findBinaryPath('ffmpeg'); + } } diff --git a/lib/Tools/Youtube.php b/lib/Tools/Youtube.php index 56e6b9f..ef358e3 100644 --- a/lib/Tools/Youtube.php +++ b/lib/Tools/Youtube.php @@ -9,8 +9,9 @@ use Symfony\Component\Process\Process; class Youtube { private $ipv4Only; - private $audioOnly = 0; - private $audioFormat, $videoFormat = 'mp4'; + public $audioOnly = 0; + private $audioFormat = 'm4a', $videoFormat; + private $format = 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best'; private $options = []; private $downloadDir; private $timeout = 60 * 60 * 15; @@ -18,15 +19,25 @@ class Youtube private $defaultDir = "/tmp/downloads"; private $env = []; - public function __construct($config) + public function __construct(array $options) { - $config += ['downloadDir' => '/tmp/downloads']; - $this->bin = $config['binary'] ?? Helper::findBinaryPath('youtube-dl'); - $this->init(); - $this->setDownloadDir($config['downloadDir']); + $options += ['downloadDir' => '/tmp/downloads', 'settings' => []]; + $this->init($options); } - public function init() + public function init(array $options) { + $this->bin = Helper::findBinaryPath('youtube-dl'); + extract($options); + $this->setDownloadDir($downloadDir); + if (!empty($settings)) { + foreach ($settings as $key => $value) { + if (empty($value)) { + $this->addOption($key); + } else { + $this->setOption($key, $value); + } + } + } if (empty($lang = getenv('LANG')) || strpos(strtolower($lang), 'utf-8') === false) { $lang = 'C.UTF-8'; } @@ -39,6 +50,25 @@ class Youtube $this->env[$key] = $val; } + public function audioMode() + { + if (Helper::ffmpegInstalled()) { + $this->addOption('--prefer-ffmpeg'); + $this->addOption('--add-metadata'); + $this->addOption('--metadata-from-title'); + $this->addOption("%(artist)s - %(title)s"); + $this->addOption('--audio-format'); + $this->addOption($this->audioFormat); + } + $this->addOption('--extract-audio'); + return $this; + } + + public function setAudioQuality($value = 0) + { + $this->addOption('--audio-quality', $value); + } + public function GetUrlOnly() { $this->addOption('--get-filename'); @@ -61,7 +91,7 @@ class Youtube return $this->getDownloadDir; } - public function prependOption($option) + public function prependOption(string $option) { array_unshift($this->options, $option); } @@ -89,6 +119,13 @@ class Youtube public function download($url) { + if ($this->audioOnly) { + $this->audioMode(); + $this->outTpl = "/%(id)s-%(title)s." . $this->audioFormat; + } else { + $this->addOption('--format'); + $this->addOption($this->format); + } $this->helper = YoutubeHelper::create(); $this->downloadDir = $this->downloadDir ?? $this->defaultDir; $this->prependOption($this->downloadDir . $this->outTpl); @@ -96,6 +133,7 @@ class Youtube $this->setUrl($url); $this->prependOption($this->bin); $process = new Process($this->options, null, $this->env); + //\OC::$server->getLogger()->error($process->getCommandLine(), ['app' => 'PHP']); $process->setTimeout($this->timeout); $process->run(function ($type, $buffer) use ($url) { if (Process::ERR === $type) { @@ -108,16 +146,7 @@ class Youtube $this->helper->updateStatus(Helper::STATUS['COMPLETE']); return ['message' => $this->helper->file ?? $process->getErrorOutput()]; } - return $process->getErrorOutput(); - - } - public function getFilePath($output) - { - $rules = '#\[download\]\s+Destination:\s+(?.*\.(?(mp4|mp3|aac)))$#i'; - - preg_match($rules, $output, $matches); - - return $matches['filename'] ?? null; + return ['error' => $process->getErrorOutput()]; } private function onError($buffer) @@ -150,7 +179,12 @@ class Youtube //$index = array_search('-i', $this->options); //array_splice($this->options, $index + 1, 0, $url); } - + public function setOption($key, $value) + { + $this->addOption($key); + $this->addOption($value); + return $this; + } public function addOption($option) { array_push($this->options, $option); diff --git a/lib/Tools/YoutubeHelper.php b/lib/Tools/YoutubeHelper.php index 8fa2975..768b0e6 100644 --- a/lib/Tools/YoutubeHelper.php +++ b/lib/Tools/YoutubeHelper.php @@ -29,7 +29,7 @@ class YoutubeHelper } public function getFilePath($output) { - $rules = '#\[download\]\s+Destination:\s+(?.*\.(?(mp4|mp3|aac|webm|m4a|ogg)))$#i'; + $rules = '#\[download\]\s+Destination:\s+(?.*\.(?(mp4|mp3|aac|webm|m4a|ogg|3gp|mkv|wav|flv)))$#i'; preg_match($rules, $output, $matches); diff --git a/package-lock.json b/package-lock.json index 50e5f94..9fba468 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "bootstrap": "^5.1.0", "html-webpack-plugin": "^5.3.2", "jquery": "^3.6.0", + "popper.js": "^1.16.1", "sass": "^1.38.0", "sass-loader": "^10.2.0", "svg-url-loader": "^7.1.1", @@ -9933,6 +9934,16 @@ "node": ">=4" } }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/portfinder": { "version": "1.0.28", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", @@ -22516,6 +22527,11 @@ "find-up": "^2.1.0" } }, + "popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" + }, "portfinder": { "version": "1.0.28", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", diff --git a/package.json b/package.json index 68883c0..749fa4f 100755 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "bootstrap": "^5.1.0", "html-webpack-plugin": "^5.3.2", "jquery": "^3.6.0", + "popper.js": "^1.16.1", "sass": "^1.38.0", "sass-loader": "^10.2.0", "svg-url-loader": "^7.1.1", diff --git a/src/inputAction.js b/src/inputAction.js index 9815b8e..560ea4f 100644 --- a/src/inputAction.js +++ b/src/inputAction.js @@ -40,6 +40,9 @@ const createInputBox = (event, type) => { selectOptions.push({ name: 'TPB', label: 'THEPIRATEBAY', selected: 1 }); container = inputBox.getInstance(name, type, path).createOptions(selectOptions).create().addSpinner(); //container.appendChild(inputBox.createLoading()); + } else if (type === 'ytdl') { + let checkbox = [{id:'audio-only',label:'Audio Only'}]; + container = inputBox.getInstance(name, type, path).createCheckbox(checkbox).create().getContainer(); } else { container = inputBox.getInstance(name, type, path).create().getContainer(); } @@ -69,11 +72,13 @@ const inputHandler = (event) => { let inputData = helper.getData('form-input-wrapper'); let inputValue = inputData.form_input_text; + if (inputData.type !== 'search' && !helper.isURL(inputValue) && !helper.isMagnetURI(inputValue)) { helper.message(t("ncdownloader", "Invalid url")); return; } if (inputData.type === 'ytdl') { + inputData.audioOnly = document.getElementById('audio-only').checked; helper.message(t("ncdownloader", "Your download has started!"), 5000); } if (inputData.type === 'search') { diff --git a/src/inputBox.js b/src/inputBox.js index bbb02bd..8edf201 100644 --- a/src/inputBox.js +++ b/src/inputBox.js @@ -5,6 +5,7 @@ import helper from './helper' class inputBox { path; selectOptions = []; + checkbox = []; constructor(btnName, id, path = null) { this.btnName = btnName; this.id = id; @@ -18,6 +19,9 @@ class inputBox { this.textInput = this._createTextInput(this.id); this.buttonContainer = this._createButtonContainer(); this.formContainer.appendChild(this.textInput); + if (this.checkbox.length !== 0) { + this.formContainer.appendChild(this._createCheckbox()); + } if (this.selectOptions.length !== 0) { this.formContainer.appendChild(this._createSelect()); } @@ -60,6 +64,42 @@ class inputBox { return select; } + _createCheckbox() { + let div = document.createElement("div"); + div.classList.add("checkboxes"); + this.checkbox.forEach(element => { + div.appendChild(element); + }) + return div; + } + + createCheckbox(data) { + if (!data) { + return; + } + data.forEach(element => { + let div = document.createElement('div'); + let label = document.createElement('label'); + let text = document.createTextNode(element.label); + let span = document.createElement('span'); + span.appendChild(text); + + let input = document.createElement('input'); + input.setAttribute('type', 'checkbox'); + input.setAttribute('id', element.id); + input.setAttribute('value', 'off'); + input.setAttribute('name', element.name || element.id); + + label.setAttribute('for',element.id); + label.classList.add("checkbox-label"); + label.appendChild(input); + label.appendChild(span); + div.appendChild(label); + this.checkbox.push(div); + }); + return this; + } + createOptions(data) { if (!data) { return;