diff --git a/appinfo/application.php b/appinfo/application.php index 219d641..a6142c6 100644 --- a/appinfo/application.php +++ b/appinfo/application.php @@ -4,6 +4,7 @@ namespace OCA\NCDownloader\AppInfo; use OCA\NCDownloader\Controller\Aria2Controller; use OCA\NCDownloader\Controller\MainController; +use OCA\NCDownloader\Controller\YoutubeController; use OCA\NCDownloader\Tools\Aria2; use OCA\NCDownloader\Tools\Helper; use OCA\NCDownloader\Tools\Settings; @@ -60,6 +61,16 @@ class Application extends App $container->query('Aria2') ); }); + $container->registerService('YoutubeController', function (IContainer $container) { + return new YoutubeController( + $container->query('AppName'), + $container->query('Request'), + $container->query('UserId'), + \OC::$server->getL10N('ncdownloader'), + $container->query('Aria2'), + $container->query('Youtube') + ); + }); } private function getRealDownloadDir() diff --git a/appinfo/routes.php b/appinfo/routes.php index e2d0819..af218f1 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -1,28 +1,23 @@ OCA\NCDownloader\Controller\Aria2Controller->index() - * - * The controller class has to be registered in the application.php file since - * it's instantiated in there - */ + return [ 'routes' => [ - ['name' => 'Main#Index', 'url' => '/', 'verb' => 'GET'], - ['name' => 'main#newDownload', 'url' => '/new', 'verb' => 'POST'], - ['name' => 'Aria2#Action', 'url' => '/aria2/{path}', 'verb' => 'POST'], - ['name' => 'Aria2#getStatus', 'url' => '/status/{path}', 'verb' => 'POST'], - ['name' => 'Aria2#Update', 'url' => '/update', 'verb' => 'GET'], - //['name' => 'main#checkStatus', 'url' => '/checkstatus', 'verb' => 'POST'], - // AdminSettings - ['name' => 'Settings#Admin', 'url' => '/admin/save', 'verb' => 'POST'], - // PersonalSettings - ['name' => 'Settings#Personal', 'url' => '/personal/save', 'verb' => 'POST'], - ['name' => 'Settings#aria2Get', 'url' => '/personal/aria2/get', 'verb' => 'POST'], - ['name' => 'Settings#aria2Save', 'url' => '/personal/aria2/save', 'verb' => 'POST'], - ['name' => 'Settings#aria2Delete', 'url' => '/personal/aria2/delete', 'verb' => 'POST'], + ['name' => 'Main#Index', 'url' => '/', 'verb' => 'GET'], + ['name' => 'main#Download', 'url' => '/new', 'verb' => 'POST'], + ['name' => 'Aria2#Action', 'url' => '/aria2/{path}', 'verb' => 'POST'], + ['name' => 'Aria2#getStatus', 'url' => '/status/{path}', 'verb' => 'POST'], + ['name' => 'Aria2#Update', 'url' => '/update', 'verb' => 'GET'], + ['name' => 'Youtube#Index', 'url' => '/youtube/get', 'verb' => 'POST'], + ['name' => 'Youtube#Download', 'url' => '/youtube/new', 'verb' => 'POST'], + ['name' => 'Youtube#Delete', 'url' => '/youtube/delete', 'verb' => 'POST'], + ['name' => 'Search#Execute', 'url' => '/search', 'verb' => 'POST'], + // AdminSettings + ['name' => 'Settings#Admin', 'url' => '/admin/save', 'verb' => 'POST'], + // PersonalSettings + ['name' => 'Settings#Personal', 'url' => '/personal/save', 'verb' => 'POST'], + ['name' => 'Settings#aria2Get', 'url' => '/personal/aria2/get', 'verb' => 'POST'], + ['name' => 'Settings#aria2Save', 'url' => '/personal/aria2/save', 'verb' => 'POST'], + ['name' => 'Settings#aria2Delete', 'url' => '/personal/aria2/delete', 'verb' => 'POST'], - ] + ], ]; - diff --git a/lib/Command/Aria2Command.php b/lib/Command/Aria2Command.php index bcb14b9..3234a5c 100644 --- a/lib/Command/Aria2Command.php +++ b/lib/Command/Aria2Command.php @@ -3,9 +3,8 @@ namespace OCA\NCDownloader\Command; use OCA\NCDownloader\Tools\Aria2; +use OCA\NCDownloader\Tools\Youtube; use OCA\NCDownloader\Tools\DBConn; -use OCA\NCDownloader\Tools\File; -use OCA\NCDownloader\Tools\Helper; use OC\Core\Command\Base; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -22,8 +21,7 @@ class Aria2Command extends base } protected function configure() { - $this - ->setName('aria2') + $this->setName('aria2') ->setDescription('Aria2 hooks') ->addArgument( 'action', @@ -38,7 +36,7 @@ class Aria2Command extends base 'path', 'p', InputOption::VALUE_OPTIONAL, - 'Downloaded file path', + 'Downloaded file path' )->addOption( 'number', 'N', @@ -64,7 +62,7 @@ class Aria2Command extends base if ($parent_gid) { $tablename = $this->conn->queryBuilder->getTableName("ncdownloader_info"); $sql = sprintf("UPDATE %s set followedby = ? WHERE gid = ?", $tablename); - // $data = serialize(['followedby' => "82140bd962946ae0"]); + // $data = serialize(['followedby' => "82140bd962946ae0"]); $this->conn->execute($sql, [$gid, $parent_gid]); } @@ -73,4 +71,5 @@ class Aria2Command extends base $output->writeln(print_r($result, true)); return 0; } + } diff --git a/lib/Controller/Aria2Controller.php b/lib/Controller/Aria2Controller.php index 6c2a556..471f633 100644 --- a/lib/Controller/Aria2Controller.php +++ b/lib/Controller/Aria2Controller.php @@ -3,7 +3,7 @@ namespace OCA\NCDownloader\Controller; use OCA\NCDownloader\Tools\Aria2; use OCA\NCDownloader\Tools\DBConn; -use OCA\NCDownloader\Tools\File; +use OCA\NCDownloader\Tools\folderScan; use OCA\NCDownloader\Tools\Helper; use OCA\NCDownloader\Tools\Settings; use OCP\AppFramework\Controller; @@ -16,11 +16,10 @@ use \OC\Files\Filesystem; class Aria2Controller extends Controller { - private $userId; + private $uid; private $settings = null; //@config OC\AppConfig private $config; - private $aria2Opts; private $l10n; public function __construct($appName, IRequest $request, $UserId, IL10N $IL10N, IRootFolder $rootFolder, Aria2 $aria2) @@ -43,6 +42,7 @@ class Aria2Controller extends Controller public function Action($path) { $path = strtolower(trim($path)); + $resp = []; if (!in_array($path, ['start', 'check']) && !($gid = $this->request->getParam('gid'))) { return new JSONResponse(['error' => "no gid value is received!"]); @@ -55,22 +55,43 @@ class Aria2Controller extends Controller $resp = $this->Start(); break; case "pause": - $resp = $this->aria2->pause($gid); + $resp = $this->doAction('pause', $gid); break; case "remove": - $resp = $this->aria2->remove($gid); + $resp = $this->doAction('remove', $gid); break; case "unpause": - $resp = $this->aria2->unpause($gid); + $resp = $this->doAction('unpause', $gid); break; case "get": - $resp = $this->aria2->tellStatus($gid); + $resp = $this->doAction('tellStatus', $gid); break; case 'purge': - $resp = $this->aria2->removeDownloadResult($gid); + $resp = $this->doAction('removeDownloadResult', $gid); + if (isset($resp['status']) && $resp['status']) { + $this->dbconn->deleteByGid($gid); + } } return new JSONResponse($resp); } + + private function doAction($action, $gid) + { + if (!$action || !$gid) { + return []; + } + $resp = $this->aria2->{$action}($gid); + + if (in_array($action, ['removeDownloadResult', 'remove'])) { + if (isset($resp['result']) && strtolower($resp['result']) === 'ok') { + return ['message' => $this->l10n->t("DONE!"), 'status' => 1]; + } else { + return ['error' => $this->l10n->t("FAILED!"), 'status' => 0]; + } + } + return $resp; + + } private function Start() { if ($this->aria2->isRunning()) { @@ -82,8 +103,8 @@ class Aria2Controller extends Controller } public function Update() { - $resp = File::syncFolder(); - //return new JSONResponse($resp); + $resp = folderScan::create()->scan(); + return new JSONResponse($resp); } private function createActionItem($name, $path) @@ -97,7 +118,7 @@ class Aria2Controller extends Controller { //$path = $this->request->getRequestUri(); $counter = $this->aria2->getCounters(); - $this->Update(); + folderScan::sync(); switch (strtolower($path)) { case "active": $resp = $this->aria2->tellActive(); @@ -179,11 +200,12 @@ class Aria2Controller extends Controller $value['progress'] = array(sprintf("%s(%.2f%%)", $completed, $percentage), $extraInfo); $timestamp = $timestamp ?? 0; //$prefix = $value['files'][0]['path']; - $filename = sprintf('%s', $folderLink, $filename); - $fileInfo = sprintf("%s | %s", $total, date("Y-m-d H:i:s", $timestamp)); - $tmp = []; $actions = []; + $filename = sprintf('%s', $folderLink, $filename); + $fileInfo = sprintf("%s | %s", $total, date("Y-m-d H:i:s", $timestamp)); + $tmp['filename'] = array($filename, $fileInfo); + if ($this->aria2->methodName === "tellStopped") { $actions[] = $this->createActionItem('purge', 'purge'); } else { @@ -192,7 +214,6 @@ class Aria2Controller extends Controller if ($this->aria2->methodName === "tellWaiting") { $actions[] = $this->createActionItem('unpause', 'unpause'); } - $tmp['filename'] = array($filename, $fileInfo); if ($this->aria2->methodName === "tellActive") { $speed = [Helper::formatBytes($value['downloadSpeed']), $left . " left"]; $tmp['speed'] = $speed; diff --git a/lib/Controller/MainController.php b/lib/Controller/MainController.php index e86a275..b901e30 100644 --- a/lib/Controller/MainController.php +++ b/lib/Controller/MainController.php @@ -2,10 +2,8 @@ namespace OCA\NCDownloader\Controller; -use OCA\NCDownloader\Search\torrentSearch; use OCA\NCDownloader\Tools\Aria2; use OCA\NCDownloader\Tools\DBConn; -use OCA\NCDownloader\Tools\File; use OCA\NCDownloader\Tools\Helper; use OCA\NCDownloader\Tools\Youtube; use OCP\AppFramework\Controller; @@ -40,8 +38,8 @@ class MainController extends Controller $this->aria2->init(); $this->youtube = $youtube; $this->dbconn = new DBConn(); + $this->tablename = $this->dbconn->queryBuilder->getTableName("ncdownloader_info"); } - /** * @NoAdminRequired * @NoCSRFRequired @@ -69,62 +67,39 @@ class MainController extends Controller return $response; } - public function newDownload() + public function Download() { - $params = array(); - $inputValue = trim($this->request->getParam('form_input_text')); - $type = trim($this->request->getParam('type')); - if ($type == 'ytdl') { - $yt = $this->youtube; - if (!$yt->isInstalled()) { - try { - $filename = Helper::getFileName($yt->installUrl()); - $this->aria2->setDownloadDir($this->dataDir . "/bin"); - $resp = $this->Save($yt->installUrl(), $filename); - return new JSONResponse($resp); - } catch (\Exception $e) { - return new JSONResponse(['error' => $e->getMessage()]); - } - - return new JSONResponse(['error' => $this->l10n->t("Youtube-dl NOT installed!")]); - } - $resp = $yt->forceIPV4()->download($inputValue); - File::syncFolder(); - return new JSONResponse(['yt' => $resp]); - - } else if ($type === 'search') { - $data = torrentSearch::go($inputValue); - $resp['title'] = ['title', 'seeders', 'info', 'actions']; - $resp['row'] = $data; - return new JSONResponse($resp); - } - - $filename = Helper::getFileName($inputValue); - $resp = $this->Save($inputValue, $filename); + $url = trim($this->request->getParam('form_input_text')); + //$type = trim($this->request->getParam('type')); + $resp = $this->_download($url); return new JSONResponse($resp); } - private function Save($url, $filename = null) + private function _download($url) { - if (isset($filename)) { + $filename = Helper::getFileName($url); + if ($filename) { $this->aria2->setFileName($filename); } - //$this->aria2->setDownloadDir("/tmp/downloads"); - $result = $this->aria2->addUri([$url]); - $gid = $result['result']; - if (!is_string($gid)) { - return ['error' => 'Failed to add download task! ' . $result['error']]; - } else { - $data = [ - 'uid' => $this->uid, - 'gid' => $gid, - 'type' => 1, - 'filename' => $filename ?? 'unknown', - 'timestamp' => time(), - 'data' => serialize(['link' => $url]), - ]; - $this->dbconn->save($data); + $result = $this->aria2->download($url); + if (!$result) { + return ['error' => 'failed to download the file for some reason!']; } - return ['gid' => $gid, 'file' => $filename, 'result' => $gid]; + if (isset($result['error'])) { + return $result; + } + + $data = [ + 'uid' => $this->uid, + 'gid' => $result, + 'type' => Helper::DOWNLOADTYPE['ARIA2'], + 'filename' => $filename ?? 'unknown', + 'timestamp' => time(), + 'data' => serialize(['link' => $url]), + ]; + $this->dbconn->save($data); + $resp = ['gid' => $result, 'file' => $filename, 'result' => $result]; + return $resp; } + } diff --git a/lib/Controller/SearchController.php b/lib/Controller/SearchController.php new file mode 100644 index 0000000..98151ce --- /dev/null +++ b/lib/Controller/SearchController.php @@ -0,0 +1,33 @@ +appName = $appName; + $this->uid = $UserId; + $this->urlGenerator = \OC::$server->getURLGenerator(); + } + + public function execute() + { + $keyword = trim($this->request->getParam('form_input_text')); + $data = torrentSearch::go($keyword); + $resp['title'] = ['title', 'seeders', 'info', 'actions']; + $resp['row'] = $data; + return new JSONResponse($resp); + } + +} diff --git a/lib/Controller/YoutubeController.php b/lib/Controller/YoutubeController.php new file mode 100644 index 0000000..2d74b95 --- /dev/null +++ b/lib/Controller/YoutubeController.php @@ -0,0 +1,152 @@ +appName = $appName; + $this->uid = $UserId; + $this->urlGenerator = \OC::$server->getURLGenerator(); + $this->l10n = $IL10N; + $this->settings = new Settings($UserId); + $this->downloadDir = $this->settings->get('ncd_downloader_dir') ?? "/Downloads"; + $this->dbconn = new DBConn(); + $this->youtube = $youtube; + $this->aria2 = $aria2; + $this->aria2->init(); + $this->tablename = $this->dbconn->queryBuilder->getTableName("ncdownloader_info"); + } + + public function Index() + { + $data = $this->dbconn->getYoutubeByUid($this->uid); + if (is_array($data) && count($data) < 1) { + return []; + } + $resp['title'] = []; + $resp['row'] = []; + $params = ['dir' => $this->downloadDir]; + $folderLink = $this->urlGenerator->linkToRoute('files.view.index', $params); + foreach ($data as $value) { + $tmp = []; + $filename = sprintf('%s', $folderLink, $value['filename']); + $fileInfo = sprintf("%s | %s", $value['filesize'], date("Y-m-d H:i:s", $value['timestamp'])); + $tmp['filename'] = array($filename, $fileInfo); + $tmp['speed'] = $value['speed']; + $tmp['progress'] = $value['progress']; + if ((int) $value['status'] == Helper::STATUS['COMPLETE']) { + $path = $this->urlGenerator->linkToRoute('ncdownloader.Youtube.Delete'); + $tmp['actions'][] = ['name' => 'delete', 'path' => $path]; + } else { + $tmp['actions'][] = ['name' => 'disabled', 'path' => '#']; + } + $tmp['data_gid'] = $value['gid'] ?? 0; + array_push($resp['row'], $tmp); + } + + $resp['title'] = ['filename', 'speed', 'progress', 'actions']; + $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; + if (!$yt->isInstalled()) { + return new JSONResponse($this->installYTD()); + } + if (Helper::isGetUrlSite($url)) { + return new JSONResponse($this->downloadUrlSite($url)); + } + + $resp = $yt->forceIPV4()->download($url); + folderScan::sync(); + return new JSONResponse(['data' => $resp]); + + } + private function downloadUrlSite($url) + { + $yt = $this->youtube; + if ($data = $yt->forceIPV4()->getDownloadUrl($url)) { + return $this->_download($data['url'], $data['filename']); + } else { + return ['error' => $this->l10n->t("failed to get any url!")]; + } + } + + public function Delete() + { + $gid = $this->request->getParam('gid'); + if (!$gid) { + return new JSONResponse(['error' => "no gid value is received!"]); + } + + if ($this->dbconn->deleteByGid($gid)) { + return new JSONResponse(['message' => $gid . " deleted!"]); + + } + } + + private function _download($url, $filename = null) + { + if (!$filename) { + $filename = Helper::getFileName($url); + } + $this->aria2->setFileName($filename); + + $result = $this->aria2->download($url); + if (!$result) { + return ['error' => 'failed to download the file for some reason!']; + } + if (isset($result['error'])) { + return $result; + } + + $data = [ + 'uid' => $this->uid, + 'gid' => $result, + 'type' => 1, + 'filename' => $filename ?? 'unknown', + 'timestamp' => time(), + 'data' => serialize(['link' => $url]), + ]; + $this->dbconn->save($data); + $resp = ['gid' => $result, 'file' => $filename, 'result' => $result]; + return $resp; + } + + private function installYTD() + { + try { + $filename = Helper::getFileName($yt->installUrl()); + $yt->setDownloadDir($this->dataDir . "/bin"); + $resp = $this->Save($yt->installUrl(), $filename); + return $resp; + } catch (\Exception $e) { + return ['error' => $e->getMessage()]; + } + + return ['error' => $this->l10n->t("Youtube-dl NOT installed!")]; + } + +} diff --git a/lib/Search/torrentSearch.php b/lib/Search/torrentSearch.php index c5cad74..9288023 100644 --- a/lib/Search/torrentSearch.php +++ b/lib/Search/torrentSearch.php @@ -4,6 +4,7 @@ namespace OCA\NCDownloader\Search; require __DIR__ . "/../../vendor/autoload.php"; use OCA\NCDownloader\Search\Sites\TPB; +use OCA\NCDownloader\Tools\Helper; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\HttpClient\HttpClient; diff --git a/lib/Tools/Aria2.php b/lib/Tools/Aria2.php index a22c1c1..26bfc75 100644 --- a/lib/Tools/Aria2.php +++ b/lib/Tools/Aria2.php @@ -162,8 +162,11 @@ class Aria2 { $this->filterResponse = false; $resp = $this->tellStopped($range); + if (!isset($resp['result'])) { + return []; + } $result = $this->sortDownloadsResult($resp['result'], ['complete', 'removed']); - $this->filterResponse = true;; + $this->filterResponse = true; return $result; } public function getCounters() @@ -268,6 +271,21 @@ class Aria2 $this->start(); } + public function download(String $url) + { + $resp = $this->addUri([$url]); + + if (isset($resp['error'])) { + return $resp; + } + + if (isset($resp['result']) && is_string($gid = $resp['result'])) { + return $gid; + } + + return false; + } + public function getDefaults() { return [ diff --git a/lib/Tools/DBConn.php b/lib/Tools/DBConn.php index e17ade9..e89fe2e 100644 --- a/lib/Tools/DBConn.php +++ b/lib/Tools/DBConn.php @@ -18,7 +18,7 @@ class DBConn //$this->conn = $this->connAdapter->getInner(); } - public function create($insert) + public function insert($insert) { $inserted = (bool) $this->conn->insertIfNotExist('*PREFIX*' . $this->table, $insert, [ 'gid', @@ -46,6 +46,19 @@ class DBConn return $queryBuilder->fetchAll(); } + public function getYoutubeByUid($uid) + { + $queryBuilder = $this->queryBuilder + ->select('*') + ->from($this->table) + ->where('uid = :uid') + ->where('type = :type') + ->setParameter('uid', $uid) + ->setParameter('type', 2) + ->execute(); + return $queryBuilder->fetchAll(); + } + public function getByGid($gid) { $queryBuilder = $this->queryBuilder @@ -57,9 +70,9 @@ class DBConn return $queryBuilder->fetch(); } - public function save(array $keys, $values = array()) + public function save(array $keys, $values = array(),$conditions = array()) { - return $this->conn->setValues($this->table, $keys, $values); + return $this->conn->setValues($this->table, $keys, $values,$conditions); } public function deleteByGid($gid) @@ -74,14 +87,14 @@ class DBConn } public function execute($sql, $values) { - return $this->conn->executeStatement($sql, $values); + return $this->conn->executeUpdate($sql, $values); // for some reason this doesn't work $query = $this->queryBuilder; $query->update('ncdownloader_info') ->set("data", $query->createNamedParameter($value)) ->where($query->expr()->eq('gid', $query->createNamedParameter($gid))); - // ->setParameter('gid', $gid); + // ->setParameter('gid', $gid); // return $query->execute(); //return $query->getSQL(); return $this->queryBuilder->getSQL(); diff --git a/lib/Tools/File.php b/lib/Tools/File.php deleted file mode 100644 index 4a8113b..0000000 --- a/lib/Tools/File.php +++ /dev/null @@ -1,42 +0,0 @@ -getUserSession()->getUser()->getUID(); - if (!isset($dir)) { - $settings = new Settings($user); - $downloadDir = $settings->get('ncd_downloader_dir') ?? "/Downloads"; - $rootFolder = Helper::getUserFolder($user); - $path = $rootFolder . "/" . ltrim($downloadDir, '/\\'); - } else { - $path = $dir; - } - - $realDir =\OC::$server->getSystemConfig()->getValue('datadirectory') . "/" . $path; - if (!(Helper::folderUpdated($realDir))) { - return ['message' => "no change"]; - } - $logger = \OC::$server->getLogger(); - $scanner = new Scanner($user, \OC::$server->getDatabaseConnection(), \OC::$server->query(IEventDispatcher::class), $logger); - try { - $scanner->scan($path); - // Helper::debug($logger->getLogPath()); - //$logger->warning($logger->getLogPath(),['app' =>'Ncdownloader']); - } catch (ForbiddenException $e) { - $logger->warning("Make sure you're running the scan command only as the user the web server runs as"); - } catch (\Exception $e) { - - $logger->warning("Exception during scan: " . $e->getMessage() . $e->getTraceAsString()); - } - return ['message' => "changed"]; - - } -} diff --git a/lib/Tools/Helper.php b/lib/Tools/Helper.php index e81f322..f0f01fd 100644 --- a/lib/Tools/Helper.php +++ b/lib/Tools/Helper.php @@ -8,7 +8,7 @@ use OC\Files\Filesystem; class Helper { public const DOWNLOADTYPE = ['ARIA2' => 1, 'YOUTUBE-DL' => 2, 'OTHERS' => 3]; - public const STATUS = ['ACTIVE' => 1, 'ERROR' => 2, 'COMPLETE' => 3]; + public const STATUS = ['ACTIVE' => 1, 'PAUSED' => 2, 'COMPLETE' => 3, 'ERROR' => 4]; public static function isUrl($URL) { $URLPattern = '%^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@|\d{1,3}(?:\.\d{1,3}){3}|(?:(?:[a-z\d\x{00a1}-\x{ffff}' diff --git a/lib/Tools/Youtube.php b/lib/Tools/Youtube.php index 1a99670..c202081 100644 --- a/lib/Tools/Youtube.php +++ b/lib/Tools/Youtube.php @@ -2,6 +2,7 @@ namespace OCA\NCDownloader\Tools; use OCA\NCDownloader\Tools\Helper; +use OCA\NCDownloader\Tools\YoutubeHelper; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; @@ -12,11 +13,22 @@ class Youtube private $audioFormat, $videoFormat = 'mp4'; private $options = []; private $downloadDir; + private $timeout = 60 * 60 * 15; + private $outTpl = "/%(id)s-%(title)s.%(ext)s"; + private $defaultDir = "/tmp/downloads"; + public function __construct($config) { $config += ['downloadDir' => '/tmp/downloads']; $this->bin = Helper::findBinaryPath('youtube-dl'); + $this->init(); $this->setDownloadDir($config['downloadDir']); + $this->helper = YoutubeHelper::create(); + } + + public function init() + { + $this->addOption("--no-mtime"); } public function GetUrlOnly() @@ -46,38 +58,80 @@ class Youtube array_unshift($this->options, $option); } - public function download($url) + public function downloadSync($url) { - $this->downloadDir = $this->downloadDir ?? "/tmp/downloads"; - $this->prependOption($this->downloadDir . "/%(id)s-%(title)s.%(ext)s"); + $this->downloadDir = $this->downloadDir ?? $this->defaultDir; + $this->prependOption($this->downloadDir . $this->outTpl); $this->prependOption("-o"); $this->setUrl($url); $this->prependOption($this->bin); // $this->buildCMD(); $process = new Process($this->options); //the maximum time required to download the file - $process->setTimeout(60*60*15); + $process->setTimeout($this->timeout); try { $process->mustRun(); $output = $process->getOutput(); } catch (ProcessFailedException $exception) { $output = $exception->getMessage(); } + return $output; } + public function download($url) + { + $this->downloadDir = $this->downloadDir ?? $this->defaultDir; + $this->prependOption($this->downloadDir . $this->outTpl); + $this->prependOption("-o"); + $this->setUrl($url); + $this->prependOption($this->bin); + $process = new Process($this->options); + $process->setTimeout($this->timeout); + $process->run(function ($type, $buffer) use ($url) { + if (Process::ERR === $type) { + $this->onError($buffer); + } else { + $this->onOutput($buffer, $url); + } + }); + if ($process->isSuccessful()) { + $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; + } + + private function onError($buffer) + { + $this->helper->log($buffer); + } + + public function onOutput($buffer, $url) + { + $this->helper->run($buffer, $url); + } public function getDownloadUrl($url) { $this->setUrl($url); $this->GetUrlOnly(); - //$process = new Process($this->options); $this->buildCMD(); exec($this->cmd, $output, $returnCode); if (count($output) === 1) { return ['url' => reset($output)]; } list($url, $filename) = $output; - return ['url' => $url, 'filename' => Helper::cleanString($filename)]; + $filename = Helper::cleanString($filename); + return ['url' => $url, 'filename' => Helper::clipFilename($filename)]; } public function setUrl($url) diff --git a/lib/Tools/YoutubeHelper.php b/lib/Tools/YoutubeHelper.php new file mode 100644 index 0000000..6d4dc5f --- /dev/null +++ b/lib/Tools/YoutubeHelper.php @@ -0,0 +1,92 @@ +\d+(?:\.\d+)?%)' . //progress + '\s+of\s+[~]?' . + '(?\d+(?:\.\d+)?(?:K|M|G)iB)' . //file size + '(?:\s+at\s+' . + '(?(\d+(?:\.\d+)?(?:K|M|G)iB/s)|Unknown speed))' . //speed + '(?:\s+ETA\s+(?([\d:]{2,8}|Unknown ETA)))?' . //estimated download time + '(\s+in\s+(?[\d:]{2,8}))?#i'; + public $file = null; + public $filesize = null; + public function __construct() + { + $this->dbconn = new DBConn(); + $this->tablename = $this->dbconn->queryBuilder->getTableName("ncdownloader_info"); + $this->user = \OC::$server->getUserSession()->getUser()->getUID(); + } + + public static function create() + { + return new static(); + } + public function getFilePath($output) + { + $rules = '#\[download\]\s+Destination:\s+(?.*\.(?(mp4|mp3|aac)))$#i'; + + preg_match($rules, $output, $matches); + + return $matches['filename'] ?? null; + } + public function log($message) + { + Helper::debug($message); + } + public function updateStatus($status = null) + { + if (isset($status)) { + $this->status = trim($status); + } + $sql = sprintf("UPDATE %s set status = ? WHERE gid = ?", $this->tablename); + $this->dbconn->execute($sql, [$this->status, $this->gid]); + } + public function run($buffer, $url) + { + $this->gid = Helper::generateGID($url); + $file = $this->getFilePath($buffer); + if ($file) { + $data = [ + 'uid' => $this->user, + 'gid' => $this->gid, + 'type' => Helper::DOWNLOADTYPE['YOUTUBE-DL'], + 'filename' => basename($file), + 'status' => Helper::STATUS['ACTIVE'], + 'timestamp' => time(), + 'data' => serialize(['link' => $url]), + ]; + //save the filename as this runs only once + $this->file = $file; + $this->dbconn->insert($data); + //$this->dbconn->save($data,[],['gid' => $this->gid]); + } + if (preg_match_all(self::PROGRESS_PATTERN, $buffer, $matches, PREG_SET_ORDER) !== false) { + if (count($matches) > 0) { + $match = reset($matches); + + //save the filesize + if (!isset($this->filesize) && isset($match['size'])) { + $this->filesize = $match['size']; + } + $size = $match['size']; + $percentage = $match['percentage']; + $speed = $match['speed'] . "|" . $match['eta']; + $sql = sprintf("UPDATE %s set filesize = ?,speed = ?,progress = ? WHERE gid = ?", $this->tablename); + $this->dbconn->execute($sql, [$this->filesize, $speed, $percentage, $this->gid]); + /* $data = [ + 'filesize' => $size, + 'speed' => $speed, + 'progress' => $percentage, + 'gid' => $this->gid, + ]; + $this->dbconn->save([], $data, ['gid' => $this->gid]);*/ + } + } + } +} diff --git a/lib/Tools/folderScan.php b/lib/Tools/folderScan.php new file mode 100644 index 0000000..b347855 --- /dev/null +++ b/lib/Tools/folderScan.php @@ -0,0 +1,93 @@ +user = $user ?? \OC::$server->getUserSession()->getUser()->getUID(); + $this->path = $path ?? $this->getDefaultPath(); + $this->realDir = \OC::$server->getSystemConfig()->getValue('datadirectory') . "/" . $this->path; + } + + public function getDefaultPath() + { + $settings = new Settings($this->user); + $rootFolder = Helper::getUserFolder($this->user); + $downloadDir = $settings->get('ncd_downloader_dir') ?? "/Downloads"; + return $rootFolder . "/" . ltrim($downloadDir, '/\\'); + } + public static function create($path = null, $user = null) + { + return new static($path, $user); + } + + public function setUser($user) + { + $this->user = $user; + return $this; + } + public function setPath($path) + { + $this->path = $path; + return $this; + } + + private function update() + { + if (!(self::folderUpdated($this->realDir))) { + return ['message' => "no change"]; + } + $this->scan(); + return ['message' => "changed"]; + } +//force update + public function scan() + { + $this->logger = \OC::$server->getLogger(); + $this->scanner = new Scanner($this->user, \OC::$server->getDatabaseConnection(), \OC::$server->query(IEventDispatcher::class), $this->logger); + try { + $this->scanner->scan($this->path); + return ['status' => 'OK', 'path' => $this->path]; + } catch (ForbiddenException $e) { + $this->logger->warning("Make sure you're running the scan command only as the user the web server runs as"); + } catch (\Exception $e) { + + $this->logger->warning("Exception during scan: " . $e->getMessage() . $e->getTraceAsString()); + } + return ['status' => $e->getMessage(), 'path' => $this->path]; + + } + public static function folderUpdated($dir) + { + if (!file_exists($dir)) { + return false; + } + $checkFile = $dir . "/.lastmodified"; + if (!file_exists($checkFile)) { + $time = \filemtime($dir); + file_put_contents($checkFile, $time); + return false; + } + $lastModified = (int) file_get_contents($checkFile); + $time = \filemtime($dir); + if ($time > $lastModified) { + file_put_contents($checkFile, $time); + return true; + } + return false; + } + + //update only folder is modified + public static function sync($path = null, $user = null) + { + return self::create($path, $user)->update(); + } +} diff --git a/src/buttonActions.js b/src/buttonActions.js index 7b2e682..bb3da2b 100644 --- a/src/buttonActions.js +++ b/src/buttonActions.js @@ -25,7 +25,10 @@ const buttonHandler = (event, type) => { return; } if (data.hasOwnProperty('result')) { - helper.message("Success for " + data['result']); + helper.message("Success " + data['result']); + } + if (data.hasOwnProperty('message')) { + helper.message(data.message); } if (row && removeRow) row.remove(); diff --git a/src/helper.js b/src/helper.js index 658bba9..3b348b9 100644 --- a/src/helper.js +++ b/src/helper.js @@ -47,10 +47,10 @@ const helper = { return magnetURI.test(url.trim()); }, - message: function (message) { + message: function (message,duration = 5000) { Toastify({ text: message, - duration: 3000, + duration:duration, newWindow: true, close: true, gravity: "top", // `top` or `bottom` diff --git a/src/inputAction.js b/src/inputAction.js index fff3c84..bf06600 100644 --- a/src/inputAction.js +++ b/src/inputAction.js @@ -20,46 +20,49 @@ const createInputBox = (event, type) => { let height = $(window).scrollTop(); if (height > 50) $("html, body").animate({ scrollTop: 0 }, "fast"); - let name; + let name, path; switch (type) { case "ytdl": name = t("ncdownloader", 'YTDL Download'); + path = basePath + "/youtube/new"; break; case "search": name = t("ncdownloader", 'Search'); + path = basePath + "/search"; break; default: name = t("ncdownloader", 'New Download'); + path = basePath + "/new"; } let container; if (type === 'search') { - container = inputBox.getInstance(name, type).addSpinner().create(); + container = inputBox.getInstance(name, type, path).create().addSpinner(); //container.appendChild(inputBox.createLoading()); } else { - container = inputBox.getInstance(name, type).create(); + container = inputBox.getInstance(name, type, path).create().getContainer(); } $("#ncdownloader-form-wrapper").append(container); } -const toggleButton = element => { - if (!element.previousSibling) { +const toggleSpinner = element => { + let spinner = element.previousSibling || element.nextSibling + + if (!spinner) { return; } if (element.style.display === 'none') { element.style.display = 'block' - element.previousSibling.style.display = 'none'; + spinner.style.display = 'none'; } else { element.style.display = 'none' - element.previousSibling.style.display = 'block'; + spinner.style.display = 'block'; } } const inputHandler = (event) => { event.preventDefault(); let element = event.target; - // element.textContent = ''; - //$(element).append(inputBox.createLoading()); - toggleButton(element); + toggleSpinner(element); let inputData = helper.getData('form-input-wrapper'); let inputValue = inputData.form_input_text; if (inputData.type !== 'search' && !helper.isURL(inputValue) && !helper.isMagnetURI(inputValue)) { @@ -67,10 +70,10 @@ const inputHandler = (event) => { return; } if (inputData.type === 'ytdl') { - helper.message(t("ncdownloader", "YTDL Download initiated")); + helper.message(t("ncdownloader", "Please check your download folder for progress"), 5000); } if (inputData.type === 'search') { - //there is a scheduled 60s-interval update running in the background, this is to prevent it from running when searching + //a scheduled 60s-interval update is running in the background, this is to prevent it from interfering when searching helper.enabledPolling = 0; nctable.getInstance().loading(); } @@ -79,7 +82,7 @@ const inputHandler = (event) => { if (data !== null && data.hasOwnProperty("file")) { helper.message(t("ncdownloader", "Downloading" + " " + data.file)); } - toggleButton(element); + toggleSpinner(element); if (data && data.title) { const tableInst = nctable.getInstance(data.title, data.row); tableInst.actionLink = false; diff --git a/src/inputBox.js b/src/inputBox.js index 45a127f..d72769d 100644 --- a/src/inputBox.js +++ b/src/inputBox.js @@ -3,23 +3,35 @@ import helper from './helper' class inputBox { - constructor(name, id) { + path; + constructor(name, id, path = null) { this.name = name; - this.container = this._createForm(); - this.textInput = this._createTextInput(id); - this.controlsContainer = this._createControlsContainer(); + this.id = id; + this.path = path; } - static getInstance(name, id) { - return new inputBox(name, id); + static getInstance(name, id, path = null) { + return new inputBox(name, id, path); } create() { + this.container = this._createForm(); + this.textInput = this._createTextInput(this.id); + this.controlsContainer = this._createControlsContainer(); this.container.appendChild(this.textInput); this.controlsContainer.appendChild(this._createControls()); this.container.appendChild(this.controlsContainer); + return this; + } + + getContainer() { return this.container; } + setPath(path) { + this.path = path; + return this; + } _createControlsContainer() { let div = document.createElement("div"); + div.classList.add("controls-container"); return div; } @@ -36,6 +48,9 @@ class inputBox { textInput.setAttribute('id', "form_input_text"); textInput.setAttribute('data-type', id); textInput.setAttribute('value', ''); + if (this.path) { + textInput.setAttribute('data-path', this.path); + } textInput.classList.add('form-input-text'); return textInput; } @@ -55,7 +70,7 @@ class inputBox { let element = doc.querySelector(".bs-spinner"); element.style.display = 'none'; this.controlsContainer.appendChild(element); - return this; + return this.container; } } diff --git a/src/ncTable.js b/src/ncTable.js index df89dc6..c511861 100644 --- a/src/ncTable.js +++ b/src/ncTable.js @@ -89,6 +89,9 @@ class ncTable { let container = document.createElement("div"); container.classList.add("button-container"); element[key].forEach(value => { + if (!value.name) { + return; + } container.appendChild(this.createActionButton(value.name, value.path)); }) rowItem.appendChild(container); diff --git a/src/updatePage.js b/src/updatePage.js index 71fa5c4..d3df498 100644 --- a/src/updatePage.js +++ b/src/updatePage.js @@ -6,9 +6,12 @@ const tableContainer = ".table"; export default { run: function () { - const eventHandler = (event, type) => { + const clickHandler = (event, type) => { event.preventDefault(); - const path = basePath + type; + let path = basePath + type; + if (type === "youtube-dl") { + path = "/apps/ncdownloader/youtube/get"; + } let name = type + "-downloads"; //avoid repeated click if ($(tableContainer).attr("type") === name && helper.enabledPolling) { @@ -18,19 +21,28 @@ export default { $(tableContainer).removeClass().addClass("table " + name); $(tableContainer).attr("type", name); let delay = 15000; - if (name === "active-downloads") { + if (['active', 'youtube-dl'].includes(type)) { delay = 1500; } helper.loop(helper.refresh, delay, ...[path]) }; - $(".waiting-downloads").on("click", event => eventHandler(event, 'waiting')); - $(".complete-downloads").on("click", event => eventHandler(event, 'complete')); - $(".active-downloads").on("click", event => eventHandler(event, 'active')); - $(".fail-downloads").on("click", event => eventHandler(event, 'fail')); + $(".waiting-downloads").on("click", event => clickHandler(event, 'waiting')); + $(".complete-downloads").on("click", event => clickHandler(event, 'complete')); + $(".active-downloads").on("click", event => clickHandler(event, 'active')); + $(".fail-downloads").on("click", event => clickHandler(event, 'fail')); + $(".youtube-dl-downloads").on("click", event => clickHandler(event, 'youtube-dl')); + + $("#ncdownloader-table-wrapper").on("click", ".download-file-folder", function (event) { + event.stopPropagation(); + const path = "/apps/ncdownloader/update"; + let url = helper.generateUrl(path); + Http.getInstance(url).setMethod('GET').send(); + }); helper.refresh(basePath + "waiting") helper.refresh(basePath + "complete") helper.refresh(basePath + "fail") + helper.refresh("/apps/ncdownloader/youtube/get") helper.loop(helper.refresh, 1000, basePath + "active"); diff --git a/templates/Navigation.php b/templates/Navigation.php index 3518ea6..7076052 100644 --- a/templates/Navigation.php +++ b/templates/Navigation.php @@ -91,5 +91,18 @@ $aria2_installed = $_['aria2_installed']; +
  • +
    + + t('Youtube-dl Downloads'));?> + +
    +
      +
    • +
      0
      +
    • +
    +
    +
  • \ No newline at end of file diff --git a/templates/settings/Admin.php b/templates/settings/Admin.php index bc827c8..6ffc842 100644 --- a/templates/settings/Admin.php +++ b/templates/settings/Admin.php @@ -3,7 +3,10 @@ script("ncdownloader", 'appSettings'); ?>
    -

    t('NCDownloader Admin Settings'));?>

    +

    ncDownloader admin Settings

    +
    + +