From 08835e7e3a4774a1c0dd8f77f32a70affb319a2e Mon Sep 17 00:00:00 2001 From: Clemens Neubauer Date: Mon, 27 May 2024 10:09:56 +0200 Subject: [PATCH] SendToMyJD2: first version (#21) New feed entries could be add automatically to you own instance of jDownloader2 by the official JDownloader2 API and service my.jdownloader.org --- README.md | 7 +- crowdin.yml | 2 + xExtension-SendToMyJD2/LICENSE | 21 + xExtension-SendToMyJD2/README.md | 66 +++ xExtension-SendToMyJD2/configure.phtml | 50 +++ xExtension-SendToMyJD2/extension.php | 339 +++++++++++++++ xExtension-SendToMyJD2/i18n/de/ext.php | 21 + xExtension-SendToMyJD2/i18n/en/ext.php | 21 + xExtension-SendToMyJD2/metadata.json | 8 + .../static/myjdapi_class.php | 399 ++++++++++++++++++ 10 files changed, 933 insertions(+), 1 deletion(-) create mode 100644 xExtension-SendToMyJD2/LICENSE create mode 100644 xExtension-SendToMyJD2/README.md create mode 100644 xExtension-SendToMyJD2/configure.phtml create mode 100644 xExtension-SendToMyJD2/extension.php create mode 100644 xExtension-SendToMyJD2/i18n/de/ext.php create mode 100644 xExtension-SendToMyJD2/i18n/en/ext.php create mode 100644 xExtension-SendToMyJD2/metadata.json create mode 100644 xExtension-SendToMyJD2/static/myjdapi_class.php diff --git a/README.md b/README.md index 1797e08..269f525 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,15 @@ This plug-in helps you to filter feed entries by keywords parsed by the feed ent This plug-in remove emojis in the title of newly added feed entries. +### SendToMyJD2 + +Send links of new feed entries to your jDownloader2 instance\ +Have a look into the [DOCUMENTATION](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-SendToMyJD2#documentation) + ### YouTubeChannel2RssFeed Transfer on the fly a YouTube URL into an RSS Feed\ -Have a look into the [DOCUMENTATION](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-YouTubeChannel2RssFeed) +Have a look into the [DOCUMENTATION](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-YouTubeChannel2RssFeed#documentation) ## Translations diff --git a/crowdin.yml b/crowdin.yml index d68c126..7e70857 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -7,5 +7,7 @@ files: translation: /xExtension-FeedTitleBuilder/i18n/%two_letters_code%/ext.php - source: /xExtension-FilterTitle/i18n/en/ext.php translation: /xExtension-FilterTitle/i18n/%two_letters_code%/ext.php + - source: /xExtension-SendToMyJD2/i18n/en/ext.php + translation: /xExtension-SendToMyJD2/i18n/%two_letters_code%/ext.php - source: /xExtension-YouTubeChannel2RssFeed/i18n/en/ext.php translation: /xExtension-YouTubeChannel2RssFeed/i18n/%two_letters_code%/ext.php diff --git a/xExtension-SendToMyJD2/LICENSE b/xExtension-SendToMyJD2/LICENSE new file mode 100644 index 0000000..97449ae --- /dev/null +++ b/xExtension-SendToMyJD2/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Clemens Neubauer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/xExtension-SendToMyJD2/README.md b/xExtension-SendToMyJD2/README.md new file mode 100644 index 0000000..3a57799 --- /dev/null +++ b/xExtension-SendToMyJD2/README.md @@ -0,0 +1,66 @@ +# FreshRSS SendToMyJD2 + +This add on for FreshRSS send links to your myJDownloader2 instance you defined in the config.\ +Required FreshRSS version at least v1.20 + +## Documentation + +You can define several configurations line by line in a text field. The search is performed based on your configuration within a category or feed and, if necessary, combined with the defined RegEx check. The base URL of the feed entry is used as the link that is sent to jDownloader2 if the defined checks apply. + +### Definations for configuration + +Seperator chars: `=` and `;`. How to see in the section [Some examples](#L36) + +There are some key chars available: + +- `k` for key: Define which data should be checked. + - `c` for category + - `f` for feed + - `*` (star) define that each new entry should be checked. In this case the parameter `r` will probably be required. +- `v` for value: Here you define the ID of the category or feed, depending on the definition of the key. If your key is the `*`, this param is ignored. + +The following chars are optional: + +- `r` for regex pattern: The data from this parameter is evaluated using the PHP function [`preg_match`](https://www.php.net/manual/en/function.preg-match). The pattern is applied to the title of the new feed entry to be checked. +- `p` for package name: This text is sent as package name to your jDownloader2. Default: `empty` - in this case jDownloader build the package name +- `m`: Mark the entry as readed if it was sent to myJD2. Allowed values: `0,1`. Default value: `0` +- `a` for autostart: Set to `1` if the added link is to be downloaded immediately. Allowed values: `0,1`. Default value: `0` + +**The parameters `k`, `v` and `r` are checked with an AND operation!** + +For the param `p` are 2 additional special words available: + +- `{userid}`: Will be replaced into the user id of the current logged in user of FreshRSS +- `{date}Ymd{/date}`: The pattern inside the date content will be executed with the php function [`date`](https://www.php.net/manual/en/function.date.php) + +### Some examples + +```text +k=c;v=28;p=Category28_{date}Ydm{/date} by {userid}; +k=f;v=79;r=/\bjohn\b/i;p=anytext {date}Ydm{/date} by {userid} +k=*;r=/\bfreshrss\b/i;p=anytext {date}Ydm{/date} by {userid};m=1;a=1 +``` + +### How to find out the ID of category or feed? + +Have a look to the extension [ShowFeedID](https://github.com/FreshRSS/Extensions/tree/master/xExtension-showFeedID). + +## Translations + +- English +- German + +[![Crowdin](https://badges.crowdin.net/cntools-freshrssextensions/localized.svg)](https://crowdin.com/project/cntools-freshrssextensions) + +You can help me to translate my extensions to a couple of languages on [Crowdin](https://crowdin.com/project/cntools-freshrssextensions). Or send me a new translation as pull request. I am happy to see! + +## Special thanks + +- [Anatoliy Kultenko](https://github.com/tofika) for [`my.jdownloader.org-api-php-class`](https://github.com/tofika/my.jdownloader.org-api-php-class) + +## Installation + +To install an extension, download the extension archive first and extract it on your PC.\ +Then, upload the specific extension(s) you want on your server. + +Extensions must be in the ./extensions directory of your FreshRSS installation. diff --git a/xExtension-SendToMyJD2/configure.phtml b/xExtension-SendToMyJD2/configure.phtml new file mode 100644 index 0000000..b1d35a4 --- /dev/null +++ b/xExtension-SendToMyJD2/configure.phtml @@ -0,0 +1,50 @@ +
+ + +
+ +
+ " required> +
+
+ +
+ +
+ " required> +
+
+
+ +
+ +
+ "> + getDevicesDatalistHTML(); ?> +
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ + LINK +
+
+ +
+
+ + +
+
+
diff --git a/xExtension-SendToMyJD2/extension.php b/xExtension-SendToMyJD2/extension.php new file mode 100644 index 0000000..2b2e900 --- /dev/null +++ b/xExtension-SendToMyJD2/extension.php @@ -0,0 +1,339 @@ +registerTranslates(); + $this->registerHook('entry_before_insert', array($this, 'CntEntryBeforeInsert')); + } + + /*---- handleConfigureAction ----*/ + public function handleConfigureAction() { + Minz_Log::debug('SendToMyJD2 - handleConfigureAction: start'); + $this->registerTranslates(); + + if (Minz_Request::isPost()) { + Minz_Log::debug('SendToMyJD2 - handleConfigureAction: isPost=TRUE'); + $data = []; + $data['email'] = strval(Minz_Request::param('SendToMyJD2_email', '')); + $data['password_plain'] = strval(Minz_Request::param('SendToMyJD2_password', '')); + $data['sendtestdata'] = strval(Minz_Request::paramString('SendToMyJD2_sendtestdata')); + $data['device'] = strval(Minz_Request::param('SendToMyJD2_device', '')); + $data['patterns'] = array_filter(Minz_Request::paramTextToArray('SendToMyJD2_patterns', [])); + + Minz_Log::debug('SendToMyJD2 - handleConfigureAction: data2save=' . var_export($data, true)); + + if (isset($this->cntMyJdMD)) { + Minz_Log::debug('SendToMyJD2 - handleConfigureAction: unset old myJD model'); + unset($this->cntMyJdMD); + } + + Minz_Log::debug('SendToMyJD2 - handleConfigureAction: try connecting to myJD instance...'); + try { + $lMD = $this->getMyJdMD(); + if (!isset($lMD)) { + Minz_Log::warning('SendToMyJD2 - handleConfigureAction: connection problem!'); + } else { + if (Minz_Request::paramString('SendToMyJD2_sendtestdata') == '1') { + Minz_Log::debug('SendToMyJD2 - handleConfigureAction: send test data...'); + $lMD->addLinks(self::CNT_TEST_YT_LINK, 'FreshRSS-SendToMyJD2-TestLink_' . date('Ymd') . '_' . date('His')/*packageName*/, false/*autostart*/); + } + Minz_Log::debug('SendToMyJD2 - handleConfigureAction: getLastPostFields=' . serialize($lMD->getProtokoll())); + Minz_Log::debug('SendToMyJD2 - handleConfigureAction: connecting OK'); + } + } catch (Exception $e) { + Minz_Log::error('SendToMyJD2-try connecting myJD: ' . $e->getMessage()); + } + + FreshRSS_Context::$user_conf->SendToMyJD2 = $data; + FreshRSS_Context::$user_conf->save(); + } + Minz_Log::debug('SendToMyJD2 - handleConfigureAction: end'); + } + + /*---- install ----*/ + public function install() { + if (version_compare(strval(FRESHRSS_VERSION), strval(self::CNT_REQUIRDED_FRESHRSS_VERSION) , '<')){ + $this->registerTranslates(); + return _t('ext.SendToMyJD2.install.bad_freshrss', self::CNT_REQUIRDED_FRESHRSS_VERSION, FRESHRSS_VERSION); + } + return true; + } + + /*---- getDataPatterns ----*/ + public function getDataPatterns() { + return implode(PHP_EOL, FreshRSS_Context::$user_conf->SendToMyJD2['patterns'] ?? []); + } + + /*---- getPatternDataAsArray():array ----*/ + private function getPatternDataAsArray($input=NULL): array { + Minz_Log::debug('SendToMyJD2 - getPatternDataAsArray: input=' . serialize($input)); + + $result = []; + + if (!isset($input)) { + $input = FreshRSS_Context::$user_conf->SendToMyJD2['patterns'] ?? []; + Minz_Log::debug('SendToMyJD2 - getPatternDataAsArray: readConfig=' . serialize($input)); + } + + // transfer pattern into array + foreach ($input as &$line) { + $lineResult = []; + $lineArray = str_getcsv($line, ';'); + + foreach ($lineArray as $cell) { + $cellArray = str_getcsv($cell, '='); + if (count($cellArray) === 2) { + $lineResult[$cellArray[0]] = $cellArray[1]; + } + } + + if (array_key_exists('t', $lineResult) && ($lineResult['t'] != '')) { + $lineResult['t'] = str_getcsv($lineResult['t'], '|'); + } + + $result[] = $lineResult; + } + unset($line); + + Minz_Log::debug('SendToMyJD2 - getPatternDataAsArray: result=' . var_export($result, true)); + + return $result; + } + + /*---- getCatIDandFeedID ----*/ + private function getCatIDandFeedID($entry):array { + $catID = null; + $feedID = null; + + if (isset($entry) && is_object($entry)) { + if (null !== $entry->feed()) { + $feedID = $entry->feed()->id(); + $feedID = strtolower(strval($feedID)); + Minz_Log::debug('SendToMyJD2 - getCatIDandFeedID: FeedID=' . $feedID); + + if (null !== $entry->feed()->category()) { + $catID = $entry->feed()->category()->id(); + $catID = strtolower(strval($catID)); + Minz_Log::debug('SendToMyJD2 - getCatIDandFeedID: CatID=' . $catID); + } + } + } + + $found = isset($feedID) || isset($catID); + + Minz_Log::debug('SendToMyJD2 - getCatIDandFeedID: found=' . var_export($found, true) . ', cadID=' . var_export($catID, true) . ', feedID=' . var_export($feedID, true)); + return [$found, $catID, $feedID]; + } + + /*---- CntEntryBeforeInsert ----*/ + public function CntEntryBeforeInsert($entry) { + Minz_Log::debug('SendToMyJD2 - CntEntryBeforeInsert: start'); + if ((isset($entry)) && is_object($entry)) { + list($found, $catID, $feedID) = $this->getCatIDandFeedID($entry); + + if ($found) { + Minz_Log::debug('SendToMyJD2 - CntEntryBeforeInsert: entry=' . serialize($entry)); + $patterns = $this->getPatternDataAsArray(); + +/* +k=key +v=value +r=regex => title of new feed entry only +p=packagename +m=markasread +t=tags +a=autostart +*/ + + foreach ($patterns as &$elem) { + Minz_Log::debug('SendToMyJD2 - CntEntryBeforeInsert: elem=' . serialize($elem)); + if (array_key_exists('k', $elem)) { + $key = strtolower(strval($elem['k'])); + + $value = null; + if (array_key_exists('v', $elem)) { + $value = strtolower(strval($elem['v'])); + } + + switch ($key) { + case "*": // all + Minz_Log::debug('SendToMyJD2 - CntEntryBeforeInsert: key=* (star)'); + $this->prepareData($entry, $elem); + break; + case "c": // category + Minz_Log::debug('SendToMyJD2 - CntEntryBeforeInsert: check by catID: need=' . strval($value) . ', current=' . strval($catID)); + if (isset($value) && isset($catID) && ($value == $catID)) { + $this->prepareData($entry, $elem); + } + break; + case "f": // feed + Minz_Log::debug('SendToMyJD2 - CntEntryBeforeInsert: check by feedID: need=' . strval($value) . ', current=' . strval($feedID)); + if (isset($value) && isset($feedID) && ($value == $feedID)) { + $this->prepareData($entry, $elem); + } + break; + } + } + } + } + } + + Minz_Log::debug('SendToMyJD2 - CntEntryBeforeInsert: end'); + return $entry; + } + + /*---- prepareData ----*/ + private function prepareData($entry, $data): void { + Minz_Log::debug('SendToMyJD2 - prepareData: start'); + if (isset($entry) && isset($data) && is_array($data)) { + + // check regex pattern + if (array_key_exists('r', $data)) { + $regexPattern = strval(trim($data['r'])); + Minz_Log::debug('SendToMyJD2 - prepareData: regex=' . serialize($regexPattern)); + if ($regexPattern != '') { + if (1 !== preg_match($regexPattern, $entry->title())) { + // regex expression not found => sending to MyJD2 is not allowed + Minz_Log::debug('SendToMyJD2 - prepareData: regex expression not found'); + return; + } + } + } + + // check and build packageName + $packageName = null; + if (array_key_exists('p', $data)) { + $packageName = strval($data['p']); + + // transfer userid + $packageName = str_replace('{userid}', trim(Minz_Session::param('currentUser')), $packageName); + + // transfer date variables + $packageName = preg_replace_callback(self::CNT_DATE_REGEX, array($this, 'renderDateRegEx'), $packageName); + + $packageName = trim($packageName); + if ($packageName == '') { + unset($packageName); + } + Minz_Log::debug('SendToMyJD2 - prepareData: packageName=' . strval($packageName)); + } + + // check flag autostart + $autostart = (array_key_exists('a', $data) && (strval($data['a']) == '1')) ? true : false; + + // send links + if ($this->doSendLink($entry, $entry->link(), $packageName, $autostart) === true) { + if (array_key_exists('m', $data) && (strval($data['m']) == '1')) { + Minz_Log::notice('SendToMyJD2 - sent to JD2 and marked as read; entryID=' . strval($entry->id())); + $entry->_isRead(true); + } else { + Minz_Log::notice('SendToMyJD2 - sent to JD2; entryID=' . strval($entry->id())); + } + +/* --- TAGS NOT WORKING PROPERLY YET ----------------------------------------------------------------------------------------------------------------------------- + if (array_key_exists('t', $data)) { + $tags = $data['t']; + if (is_array($tags)) { + $resultTags = []; + foreach ($tags as $tag) { + // transfer userid + $workTag = str_replace('{userid}', trim(Minz_Session::param('currentUser')), $tag); + + // transfer date variables + $resultTags[] = preg_replace_callback(self::CNT_DATE_REGEX, array($this, 'renderDateRegEx'), $workTag); + } + if (count($resultTags) > 0){ + $entry->_tags(array_unique(array_merge($resultTags, $entry->tags()))); + } + } + } +--- TAGS NOT WORKING PROPERLY YET -----------------------------------------------------------------------------------------------------------------------------*/ + } + } + } + + /*---- doSendLink ----*/ + private function doSendLink($entry, $url, $packageName = null, $autostart = false): bool { + Minz_Log::debug('SendToMyJD2 - doSendLink: start - url=' . $url); + $result = false; + if (isset($entry) && ($url != '')) { + $lMD = $this->getMyJdMD(); + if (isset($lMD) && ($lMD->addLinks($url, $packageName, ($autostart == true)) === true)) { + Minz_Log::debug('SendToMyJD2 - doSendLink: done'); + $result = true; + } + Minz_Log::debug('SendToMyJD2 - doSendLink: getLastPostFields=' . serialize($lMD->getProtokoll())); + } + + Minz_Log::debug('SendToMyJD2 - doSendLink: end - ' . ($result ? 'true' : 'false')); + return $result; + } + + /*---- getMyJdMD ----*/ + private function getMyJdMD(){ + $lMD = $this->cntMyJdMD; + + if (isset($lMD)){ + Minz_Log::debug('SendToMyJD2 - getMyJdMD: befor reconnect'); + if (!$lMD->reconnect()){ + Minz_Log::debug('SendToMyJD2 - getMyJdMD: reconnect faild'); + unset($lMD); + } + } + + if (!isset($lMD)){ + Minz_Log::debug('SendToMyJD2 - getMyJdMD: create model'); + $email = FreshRSS_Context::$user_conf->SendToMyJD2['email']; + $password = FreshRSS_Context::$user_conf->SendToMyJD2['password_plain']; + $device = FreshRSS_Context::$user_conf->SendToMyJD2['device']; + $appkey = 'FreshRSS-' . trim(Minz_Session::param('currentUser')) . '-' . uniqid(''); + + Minz_Log::debug('SendToMyJD2 - getMyJdMD: email=' . $email . ';pass=' . $password . ';device=' . $device . ';appkey=' . $appkey); + + $lMD = new MYJDAPI($email, $password, $device, $appkey, true); + if($lMD == false) { + // connection error + Minz_Log::error('SendToMyJD2 - getMyJdMD: connection error!'); + unset($lMD); + $this->cntMyJdMD = $lMD; + } + } + + Minz_Log::debug('SendToMyJD2 - getMyJdMD: model=' . serialize($lMD)); + return $lMD; + } + + /*---- renderDateRegEx ----*/ + private function renderDateRegEx(&$matches){ + return date(trim($matches[2])); + } + + /*----- getYouTubeTestLink ----*/ + public function getYouTubeTestLink() { + return self::CNT_TEST_YT_LINK; + } + + /*----- getDevicesDatalistHTML ----*/ + public function getDevicesDatalistHTML() { + $result = ''; + $lMD = $this->getMyJdMD(); + if (isset($lMD)) { + $devices = $lMD->getAvailableDevices(); + if (is_array($devices)) { + $result = ''; + foreach ($devices as $device) { + $result .= ''; + } + $result .= ''; + } + } + return $result; + } +} diff --git a/xExtension-SendToMyJD2/i18n/de/ext.php b/xExtension-SendToMyJD2/i18n/de/ext.php new file mode 100644 index 0000000..140c218 --- /dev/null +++ b/xExtension-SendToMyJD2/i18n/de/ext.php @@ -0,0 +1,21 @@ + array( + 'config' => array( + 'email_label' => 'Email', + 'password_label' => 'Passwort', + 'password_placeholder' => 'Tragen sie hier das Passwort ein', + 'password_danger' => 'ACHTUNG: Das Passwort wird als Klartext in der User-Konfigurationsdatei gespeichert!', + 'device_label' => 'Gerät', + 'data_label' => 'Daten', + 'data_help' => 'Eine detaillierte Hilfe zur Konfiguration steht auf GitHub zur Verfügung. Folge dem Link:', + 'data_help_lnk_txt' => 'Hilfe', + 'testdata' => 'Testdaten senden', + 'testdata_hint' => 'Beim Klick auf Speichern wird einmalig folgender YouTube-Link an die definierte jDownloader-Instanz gesendet: ', + ), + 'install' => array( + 'bad_freshrss' => 'Für "SendToMyJD2" ist mindestens die FreshRSS-Version `%s` erforderlich. (Sie verwenden die FreshRSS-Version `%s`)', + ), + ), +); diff --git a/xExtension-SendToMyJD2/i18n/en/ext.php b/xExtension-SendToMyJD2/i18n/en/ext.php new file mode 100644 index 0000000..fa1db6f --- /dev/null +++ b/xExtension-SendToMyJD2/i18n/en/ext.php @@ -0,0 +1,21 @@ + array( + 'config' => array( + 'email_label' => 'Email', + 'password_label' => 'Password', + 'password_placeholder' => 'Tragen sie hier das Passwort ein', + 'password_danger' => 'ATTENTION: The password is saved as plain text in the user configuration file!', + 'device_label' => 'Device', + 'data_label' => 'Data', + 'data_help' => 'Detailed configuration help is available on GitHub. Follow the link:', + 'data_help_lnk_txt' => 'Documentation', + 'testdata' => 'Send test data', + 'testdata_hint' => 'When you click on Save, the following YouTube link is sent once to the defined jDownloader instance: ', + ), + 'install' => array( + 'bad_freshrss' => 'SendToMyJD2 requires at least FreshRSS version `%s`. (You are using FreshRSS version `%s`)', + ), + ), +); diff --git a/xExtension-SendToMyJD2/metadata.json b/xExtension-SendToMyJD2/metadata.json new file mode 100644 index 0000000..d3b01ef --- /dev/null +++ b/xExtension-SendToMyJD2/metadata.json @@ -0,0 +1,8 @@ +{ + "name": "SendToMyJD2", + "author": "CNTools | Clemens Neubauer", + "description": "Send links to a jDownloader2 instance with the myJDownloader2 API", + "version": "0.0.1-alpha", + "entrypoint": "SendToMyJD2", + "type": "user" +} diff --git a/xExtension-SendToMyJD2/static/myjdapi_class.php b/xExtension-SendToMyJD2/static/myjdapi_class.php new file mode 100644 index 0000000..4c28fe5 --- /dev/null +++ b/xExtension-SendToMyJD2/static/myjdapi_class.php @@ -0,0 +1,399 @@ + api_url = str_replace('http://', 'https://', $this -> api_url); + } + $this -> protokollLength = $protokollLength; + $this -> rid_counter = time(); + $this -> appkey = $appkey; + if( ($email != "") && ($password != "")) { + $res = $this -> connect( $email, $password); + if( $res === false) { + return false; + } + } + + $this -> setDeviceName( $device_name); + } + + public function getVersion() { + return $this -> version; + } + + //Set AppKey name + public function setAppKeyName( $appkey) { + if( !is_null( $appkey) && is_string( $appkey)) { + $this -> appkey = $appkey; + return true; + } + return false; + } + + //Get AppKey name + public function getAppKeyName() { + return $this -> appkey; + } + + //Set device name + public function setDeviceName( $device_name) { + if( !is_null( $device_name) && is_string( $device_name)) { + $this -> device_name = $device_name; + return true; + } + return false; + } + + //Get device name + public function getDeviceName() { + return $this -> device_name; + } + + // Connect to api.jdownloader.org + // if success - setup loginSecret, deviceSecret, sessiontoken, regaintoken, serverEncryptionToken, deviceEncryptionToken + // input: email, password + // return: true or false + public function connect( $email, $password) { + $this -> loginSecret = $this -> createSecret( $email, $password, $this -> SERVER_DOMAIN); + $this -> deviceSecret = $this -> createSecret( $email, $password, $this -> DEVICE_DOMAIN); + $query = "/my/connect?email=".urlencode( $email)."&appkey=".urlencode( $this -> appkey); + $res = $this -> callServer( $query, $this -> loginSecret); + if( $res === false) { + return false; + } + $content_json = json_decode( $res, true); + $this -> sessiontoken = $content_json["sessiontoken"]; + $this -> regaintoken = $content_json["regaintoken"]; + $this -> serverEncryptionToken = $this -> updateEncryptionToken( $this -> loginSecret, $this -> sessiontoken); + $this -> deviceEncryptionToken = $this -> updateEncryptionToken( $this -> deviceSecret, $this -> sessiontoken); + return true; + } + + // Reconnect to api.jdownloader.org + // if success - setup sessiontoken, regaintoken, serverEncryptionToken, deviceEncryptionToken + // return: true or false + public function reconnect() { + $query = "/my/reconnect?appkey=".urlencode( $this -> appkey)."&sessiontoken=".urlencode( $this -> sessiontoken)."®aintoken=".urlencode( $this -> regaintoken); + $res = $this -> callServer( $query, $this -> serverEncryptionToken); + if( $res === false) { + return false; + } + $content_json = json_decode( $res, true); + $this -> sessiontoken = $content_json["sessiontoken"]; + $this -> regaintoken = $content_json["regaintoken"]; + $this -> serverEncryptionToken = $this -> updateEncryptionToken( $this -> serverEncryptionToken, $this -> sessiontoken); + $this -> deviceEncryptionToken = $this -> updateEncryptionToken( $this -> deviceSecret, $this -> sessiontoken); + return true; + } + + // Disconnect from api.jdownloader.org + // if success - cleanup sessiontoken, regaintoken, serverEncryptionToken, deviceEncryptionToken + // return: true or false + public function disconnect() { + $query = "/my/disconnect?sessiontoken=".urlencode( $this -> sessiontoken); + $res = $this -> callServer( $query, $this -> serverEncryptionToken); + if( $res === false) { + return false; + } + $content_json = json_decode( $res, true); + $this -> sessiontoken = ""; + $this -> regaintoken = ""; + $this -> serverEncryptionToken = ""; + $this -> deviceEncryptionToken = ""; + return true; + } + + // Enumerate Devices connected to my.jdownloader.org + // if success - setup devices + // call getDirectConnectionInfos to setup devices + // return: true or false + public function enumerateDevices() { + $query = "/my/listdevices?sessiontoken=".urlencode( $this -> sessiontoken); + $res = $this -> callServer( $query, $this -> serverEncryptionToken); + if( $res === false) { + return false; + } + $content_array = json_decode( $res, true); + $this -> devices = $content_array["list"]; + $res = $this -> getDirectConnectionInfos(); + if( $res === false) { + return false; + } + return true; + } + + public function getAvailableDevices() { + if (!isset($this -> devices)) { + $this -> enumerateDevices(); + } + + if (isset($this -> devices)) { + return $this -> devices; + } + + return false; + } + + // Call action "/device/getDirectConnectionInfos" for each devices + // if success - setup devices with infos + // return: true or false + public function getDirectConnectionInfos() { + foreach( $this -> devices as $i => &$ivalue) { + $res = $this -> callAction( "/device/getDirectConnectionInfos"); + if( $res === false) { + return false; + } + $content_array = json_decode( $res, true); + $this -> devices[$i]["infos"] = $content_array["data"]["infos"]; + } + return true; + } + + // Send links to device using action /linkgrabberv2/addLinks + // input: device - name of device, links - array or string of links, package_name - custom package name + // {"url":"/linkgrabberv2/addLinks", + // "params":["{\"priority\":\"DEFAULT\",\"links\":\"YOURLINK\",\"autostart\":YOURVALUE, \"packageName\": \"YOURPKGNAME\"}"], + // "rid":YOURREQUESTID,"apiVer":1} + public function addLinks( $links, $package_name = null, $autostart = true) { + if( !is_array( $this -> devices)) { + $this -> enumerateDevices(); + } + if( is_array( $links)) { + $links = implode( ",", $links); + } + $params = '\"priority\":\"DEFAULT\"'; + $params .= ', \"links\": \"'.$links.'\"'; + $params .= ( $autostart == true) ? ',\"autostart\":true' : ',\"autostart\":false'; + if( isset( $package_name) && is_string( $package_name)) { + $package_name = trim( $package_name); + if ($package_name != '') { + $params .= ', \"packageName\": \"'.$package_name.'\"'; + $params .= ', \"overwritePackagizerRules\":true'; + } + } + $res = $this -> callAction( "/linkgrabberv2/addLinks", $params); + if( $res === false) { + return false; + } + return true; + } + + // Retrive links + public function queryLinks( $params = []) { + //taken from: https://docs.google.com/document/d/1IGeAwg8bQyaCTeTl_WyjLyBPh4NBOayO0_MAmvP5Mu4/edit# (LinkQueryStorable) + $params_default = [ + "bytesTotal" => true, + "comment" => true, + "status" => true, + "enabled" => true, + "maxResults" => -1, + "startAt" => 0, + "packageUUIDs" => null, + "host" => true, + "url" => true, + "bytesLoaded" => true, + "speed" => true, + "eta" => true, + "finished" => true, + "priority" => true, + "running" => true, + "skipped" => true, + "extractionStatus" => true + ]; + + $params = array_merge( $params_default, $params); + + $res = $this -> callAction( "/downloadsV2/queryLinks", $params); + return $res; + } + // Make a call to my.jdownloader.org + // input: query - path+params, key - key for encryption, params - additional params + // return: result from server or false + private function callServer( $query, $key, $params = false) { + if( $params != "") { + if( $key != "") { + $params = $this -> encrypt( $params, $key); + } + $rid = $this -> rid_counter; + } else { + $rid = $this -> getUniqueRid(); + } + if( strpos( $query, "?") !== false) { $query = $query."&"; } else { $query = $query."?"; } + $query = $query."rid=".$rid; + $signature = $this -> sign( $key, $query); + $query = $query."&signature=".$signature; + $url = $this -> api_url.$query; + $this -> addIntoProtokoll(['call' => 'callServer', 'url' => $url, 'params' => $params, 'key' => $key]); + if( $params != "") { + $res = $this -> postQuery( $url, $params, $key); + } else { + $res = $this -> postQuery( $url, "", $key); + } + if( $res === false) { + return false; + } + $content_json = json_decode( $res, true); + if( $content_json["rid"] != $this -> rid_counter) { + return false; + } + return $res; + } + + // Make a call to API function on my.jdownloader.org + // input: device_name - name of device to send action, action - action pathname, params - additional params + // return: result from server or false + public function callAction( $action, $params = false) { + if( !is_array( $this -> devices)) { + $this -> enumerateDevices(); + } + + if( !is_array( $this -> devices) || ( count( $this -> devices) == 0)) { + return false; + } + + foreach( $this -> devices as $i => &$ivalue) { + if( $this -> devices[$i]["name"] == $this->getDeviceName()) { + $device_id = $this -> devices[$i]["id"]; + } + } + if( !isset( $device_id)) { + return false; + } + $query = "/t_".urlencode( $this -> sessiontoken)."_".urlencode( $device_id).$action; + if( $params != "") { + if(is_array($params)) { + $params = str_replace('"', '\"', substr(json_encode($params),1,-1)); + } + $json_data = '{"url":"'.$action.'","params":["{'.$params.'}"],"rid":'.$this -> getUniqueRid().',"apiVer":'.$this -> apiVer.'}'; + } else { + $json_data = '{"url":"'.$action.'","rid":'.$this -> getUniqueRid().',"apiVer":'.$this -> apiVer.'}'; + } + + $this -> addIntoProtokoll(['call' => 'callAction', 'url' => $action, 'json_data' => $json_data]); + + $json_data = $this -> encrypt( $json_data, $this -> deviceEncryptionToken); + $url = $this -> api_url.$query; + $res = $this -> postQuery( $url, $json_data, $this -> deviceEncryptionToken); + if( $res === false) { + return false; + } + $content_json = json_decode( $res, true); + if( $content_json["rid"] != $this -> rid_counter) { + return false; + } + return $res; + } + + // Genarate new unique rid + // return new rid_counter + public function getUniqueRid() { + $this -> rid_counter++; + return $this -> rid_counter; + } + + // Return current rid_counter + public function getRid() { + return $this -> rid_counter; + } + + private function createSecret( $username, $password, $domain) { + return hash( "sha256", strtolower( $username) . $password . strtolower( $domain), true); + } + + private function sign( $key, $data) { + return hash_hmac( "sha256", $data, $key); + } + + private function decrypt( $data, $iv_key) { + $iv = substr( $iv_key, 0, strlen( $iv_key)/2); + $key = substr( $iv_key, strlen( $iv_key)/2); + return openssl_decrypt( base64_decode( $data), "aes-128-cbc", $key, OPENSSL_RAW_DATA, $iv); + } + + private function encrypt( $data, $iv_key) { + $iv = substr( $iv_key, 0, strlen( $iv_key)/2); + $key = substr( $iv_key, strlen( $iv_key)/2); + return base64_encode( openssl_encrypt( $data, "aes-128-cbc", $key, OPENSSL_RAW_DATA, $iv)); + } + + private function updateEncryptionToken( $oldToken, $updateToken) { + return hash( "sha256", $oldToken.pack( "H*", $updateToken), true); + } + + // postQuery( $url, $postfields, $iv_key) + // Make Get or Post Request to $url ( $postfields) + // Send Payload data if $postfields not null + // return plain response or decrypted response if $iv_key not null + private function postQuery( $url, $postfields = false, $iv_key = false) { + $this -> addIntoProtokoll(['call' => 'postQuery', 'url' => $url, 'postfields' => $postfields]); + + $ch = curl_init(); + curl_setopt( $ch, CURLOPT_URL, $url); + curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true); + if( $postfields) { + $headers[] = "Content-Type: application/aesjson-jd; charset=utf-8"; + curl_setopt( $ch, CURLOPT_POST, true); + curl_setopt( $ch, CURLOPT_POSTFIELDS, $postfields); + curl_setopt( $ch, CURLOPT_HEADER, true); + curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers); + } + $response = array(); + $response["text"] = curl_exec( $ch); + $response["info"] = curl_getinfo( $ch); + $response["code"] = $response["info"]["http_code"]; + if( $response["code"] != 200) { + return false; + } + if( $postfields) { + $response["body"] = substr( $response["text"], $response["info"]["header_size"]); + } else { + $response["body"] = $response["text"]; + } + if( $iv_key) { + $response["body"] = $this -> decrypt( $response["body"], $iv_key); + } + curl_close( $ch); + return $response["body"]; + } + + private function addIntoProtokoll($data):void { + $this -> protokoll[date( 'Ymd-His-u') . '_' . uniqid()] = $data; + $this -> protokoll = array_slice( $this -> protokoll, ( $this -> protokollLength * -1)); + } + + public function getProtokoll( $aClear=true) { + $result = $this -> protokoll; + if ( $aClear) { + unset( $this -> protokoll); + } + return $result; + } +} \ No newline at end of file