diff --git a/root/app/www/public/ajax/customapps.php b/root/app/www/public/ajax/customapps.php
new file mode 100644
index 0000000..056a052
--- /dev/null
+++ b/root/app/www/public/ajax/customapps.php
@@ -0,0 +1,201 @@
+ 'png', IMAGETYPE_JPEG => 'jpg', IMAGETYPE_WEBP => 'webp'];
+const CUSTOM_APP_ICON_MAX_SIZE = 262144;
+
+if ($_POST['m'] == 'getCustomApps') {
+ $customApps = $proxyDb->getCustomApps();
+
+ ?>
+
- = $endpointInfo['label'] ?> = $endpoint ?> |
+ = htmlspecialchars($endpointInfo['label']) ?> = htmlspecialchars($endpoint) ?> |
- id="endpoint-counter-= $counter ?>" data-endpoint="= $endpoint ?>" data-method="= $method ?>" type="checkbox" class="form-check-input endpoint-= $method ?>">
+ id="endpoint-counter-= $counter ?>" data-endpoint="= htmlspecialchars($endpoint) ?>" data-method="= $method ?>" type="checkbox" class="form-check-input endpoint-= $method ?>">
getAllAppsList());
+ } elseif (!in_array($request['starr'], $starr->getAllAppsList())) {
+ $requestError = 'starr field is not valid, should be one of: ' . implode(', ', $starr->getAllAppsList());
} elseif (!$request['url']) {
$requestError = 'url field is required, should be the local url to the starr app';
} elseif (!$request['apikey']) {
diff --git a/root/app/www/public/classes/Database.php b/root/app/www/public/classes/Database.php
index d6b2830..d9b28c5 100644
--- a/root/app/www/public/classes/Database.php
+++ b/root/app/www/public/classes/Database.php
@@ -19,6 +19,7 @@ class Database
use Settings;
use Starrs;
use Usage;
+ use CustomApps;
public $db;
public $dbName;
diff --git a/root/app/www/public/classes/Starr.php b/root/app/www/public/classes/Starr.php
index 2174556..35ede09 100644
--- a/root/app/www/public/classes/Starr.php
+++ b/root/app/www/public/classes/Starr.php
@@ -16,12 +16,14 @@ class Starr
use EndpointOverrides;
public $cache;
+ public $customApps;
public function __construct()
{
global $cache;
- $this->cache = $cache ?: new Cache();
+ $this->cache = $cache ?: new Cache();
+ $this->customApps = null;
}
public function __tostring()
@@ -29,6 +31,60 @@ public function __tostring()
return 'Class loaded: Starr';
}
+ public function loadCustomApps()
+ {
+ global $proxyDb;
+
+ if ($this->customApps === null && $proxyDb) {
+ $this->customApps = $proxyDb->getCustomApps();
+ }
+
+ return $this->customApps ?: [];
+ }
+
+ public function isHttpUrl($url)
+ {
+ return in_array(strtolower(parse_url($url, PHP_URL_SCHEME) ?: ''), ['http', 'https']) && parse_url($url, PHP_URL_HOST);
+ }
+
+ public function getAllAppsList()
+ {
+ $list = StarrApps::LIST;
+ $customApps = $this->loadCustomApps();
+
+ foreach ($customApps as $customApp) {
+ $list[] = strtolower($customApp['name']);
+ }
+
+ return $list;
+ }
+
+ public function isCustomApp($name)
+ {
+ $customApps = $this->loadCustomApps();
+
+ foreach ($customApps as $customApp) {
+ if (strtolower($customApp['name']) == strtolower($name)) {
+ return $customApp;
+ }
+ }
+
+ return false;
+ }
+
+ public function getCustomAppFromId($id)
+ {
+ $customApps = $this->loadCustomApps();
+
+ foreach ($customApps as $customApp) {
+ if ($customApp['id'] + StarrApps::CUSTOM_ID_OFFSET == $id) {
+ return $customApp;
+ }
+ }
+
+ return false;
+ }
+
public function getStarrInterfaceIdFromName($name)
{
switch ($name) {
@@ -45,6 +101,13 @@ public function getStarrInterfaceIdFromName($name)
case 'whisparr':
return StarrApps::WHISPARR_ID;
}
+
+ $customApp = $this->isCustomApp($name);
+ if ($customApp) {
+ return $customApp['id'] + StarrApps::CUSTOM_ID_OFFSET;
+ }
+
+ return null;
}
public function getStarrInterfaceNameFromId($id)
@@ -63,6 +126,42 @@ public function getStarrInterfaceNameFromId($id)
case StarrApps::WHISPARR_ID:
return strtolower(StarrApps::WHISPARR_LABEL);
}
+
+ $customApp = $this->getCustomAppFromId($id);
+ if ($customApp) {
+ return strtolower($customApp['name']);
+ }
+
+ return null;
+ }
+
+ public function getStarrInterfaceLabelFromId($id)
+ {
+ $name = $this->getStarrInterfaceNameFromId($id);
+ if (!$name) {
+ return null;
+ }
+
+ $customApp = $this->isCustomApp($name);
+ if ($customApp) {
+ return $customApp['label'];
+ }
+
+ return ucfirst($name);
+ }
+
+ public function getAppIcon($name)
+ {
+ $customApp = $this->isCustomApp($name);
+ if ($customApp && $customApp['icon']) {
+ return 'images/custom/' . $customApp['icon'];
+ }
+
+ if (in_array($name, StarrApps::LIST)) {
+ return 'images/logos/' . $name . '.png';
+ }
+
+ return '';
}
public function apiVersion($app)
@@ -77,11 +176,19 @@ public function apiVersion($app)
case 'whisparr':
return 'v3';
}
+
+ $customApp = $this->isCustomApp($app);
+ if ($customApp) {
+ return $customApp['api_version'];
+ }
+
+ return 'v1';
}
public function testConnection($app, $url, $apikey)
{
- $url = $url . '/api/' . $this->apiVersion($app) . '/config/host';
+ //-- CUSTOM APPS ARE NOT GUARANTEED TO HAVE config/host, system/status IS COMMON TO EVERY *ARR API
+ $url = $url . '/api/' . $this->apiVersion($app) . ($this->isCustomApp($app) ? '/system/status' : '/config/host');
$headers = ['x-api-key:' . $apikey];
$curl = curl($url, $headers);
@@ -121,7 +228,13 @@ public function getEndpoints($app)
$openapi = 'https://raw.githubusercontent.com/Whisparr/Whisparr/eros/src/Whisparr.Api.V3/openapi.json';
break;
default:
- return $endpoints;
+ $customApp = $this->isCustomApp($app);
+ if (!$customApp || !$this->isHttpUrl($customApp['openapi_url'])) {
+ return $endpoints;
+ }
+
+ $openapi = $customApp['openapi_url'];
+ break;
}
$openapi = curl($openapi);
@@ -289,7 +402,7 @@ public function requestMatchesEndpointTemplate($requestEndpoint, $templateEndpoi
public function findWildcardEndpoint($starrApp, $endpoint)
{
- foreach (StarrApps::LIST as $listStarr) {
+ foreach ($this->getAllAppsList() as $listStarr) {
if (strtolower($listStarr) != strtolower($starrApp)) {
continue;
}
diff --git a/root/app/www/public/classes/interfaces/Starr.php b/root/app/www/public/classes/interfaces/Starr.php
index 595f162..bb9fd5a 100644
--- a/root/app/www/public/classes/interfaces/Starr.php
+++ b/root/app/www/public/classes/interfaces/Starr.php
@@ -22,4 +22,5 @@ interface StarrApps
public const SONARR_LABEL = 'Sonarr';
public const WHISPARR_ID = 6;
public const WHISPARR_LABEL = 'Whisparr';
+ public const CUSTOM_ID_OFFSET = 100;
}
diff --git a/root/app/www/public/classes/traits/Database/CustomApps.php b/root/app/www/public/classes/traits/Database/CustomApps.php
new file mode 100644
index 0000000..9da3cf3
--- /dev/null
+++ b/root/app/www/public/classes/traits/Database/CustomApps.php
@@ -0,0 +1,87 @@
+db->prepare($q);
+ if (!$stmt) {
+ return false;
+ }
+
+ foreach ($params as $param => $value) {
+ $stmt->bindValue(':' . $param, $value, is_int($value) ? SQLITE3_INTEGER : SQLITE3_TEXT);
+ }
+
+ return $stmt->execute();
+ }
+
+ public function getCustomApps()
+ {
+ $apps = [];
+ $r = $this->customAppsQuery("SELECT * FROM custom_apps ORDER BY label ASC");
+
+ while ($r && $row = $r->fetchArray(SQLITE3_ASSOC)) {
+ $apps[] = $row;
+ }
+
+ return $apps;
+ }
+
+ public function getCustomApp($id)
+ {
+ $r = $this->customAppsQuery("SELECT * FROM custom_apps WHERE id = :id", ['id' => intval($id)]);
+
+ return ($r ? $r->fetchArray(SQLITE3_ASSOC) : []) ?: [];
+ }
+
+ public function addCustomApp($fields = [])
+ {
+ $r = $this->customAppsQuery("INSERT INTO custom_apps (name, label, api_version, openapi_url) VALUES (:name, :label, :api_version, :openapi_url)", [
+ 'name' => $fields['name'],
+ 'label' => $fields['label'],
+ 'api_version' => $fields['api_version'],
+ 'openapi_url' => $fields['openapi_url']
+ ]);
+
+ return $r ? '' : 'addCustomApp() :: ' . $this->db->lastErrorMsg();
+ }
+
+ public function updateCustomApp($id, $fields = [])
+ {
+ $columns = array_intersect(array_keys($fields), ['label', 'api_version', 'openapi_url', 'icon']);
+ if (!$columns) {
+ return;
+ }
+
+ $set = implode(', ', array_map(fn($column) => $column . ' = :' . $column, $columns));
+ $r = $this->customAppsQuery("UPDATE custom_apps SET " . $set . " WHERE id = :id", array_intersect_key($fields, array_flip($columns)) + ['id' => intval($id)]);
+
+ return $r ? '' : 'updateCustomApp() :: ' . $this->db->lastErrorMsg();
+ }
+
+ public function deleteCustomApp($id)
+ {
+ $r = $this->customAppsQuery("DELETE FROM custom_apps WHERE id = :id", ['id' => intval($id)]);
+
+ return $r ? '' : 'deleteCustomApp() :: ' . $this->db->lastErrorMsg();
+ }
+
+ public function setCustomAppNavSetting($name, $value)
+ {
+ $this->customAppsQuery("INSERT OR REPLACE INTO " . SETTINGS_TABLE . " (name, value) VALUES (:name, :value)", ['name' => 'uiHeaderCustom_' . $name, 'value' => $value ? '1' : '0']);
+ }
+
+ public function deleteCustomAppNavSetting($name)
+ {
+ $this->customAppsQuery("DELETE FROM " . SETTINGS_TABLE . " WHERE name = :name", ['name' => 'uiHeaderCustom_' . $name]);
+ }
+}
diff --git a/root/app/www/public/functions/common.php b/root/app/www/public/functions/common.php
index de31382..4074c45 100644
--- a/root/app/www/public/functions/common.php
+++ b/root/app/www/public/functions/common.php
@@ -32,16 +32,20 @@ function loadClassExtras($class)
function automation()
{
+ global $starr;
+
$shell = $shell ?? new Shell();
//-- CREATE DIRECTORIES
- $createFolders = [LOGS_PATH, LOGS_PATH . 'system/', LOGS_PATH . 'notifications/', BACKUP_PATH, APP_USER_TEMPLATES_PATH, DATABASE_PATH, MIGRATIONS_PATH];
+ $createFolders = [LOGS_PATH, LOGS_PATH . 'system/', LOGS_PATH . 'notifications/', BACKUP_PATH, APP_USER_TEMPLATES_PATH, DATABASE_PATH, MIGRATIONS_PATH, ABSOLUTE_PATH . 'images/custom/'];
foreach ($createFolders as $createFolder) {
if (!is_dir($createFolder)) {
createDirectoryTree($createFolder);
}
}
- foreach (StarrApps::LIST as $starrApp) {
+ //-- INCLUDES CUSTOM APPS WHEN THE STARR CLASS IS LOADED
+ $allApps = $starr ? $starr->getAllAppsList() : StarrApps::LIST;
+ foreach ($allApps as $starrApp) {
if (!is_dir(APP_USER_TEMPLATES_PATH . $starrApp)) {
createDirectoryTree(APP_USER_TEMPLATES_PATH . $starrApp);
}
diff --git a/root/app/www/public/includes/header.php b/root/app/www/public/includes/header.php
index 5f091a9..4a83f6b 100644
--- a/root/app/www/public/includes/header.php
+++ b/root/app/www/public/includes/header.php
@@ -74,15 +74,29 @@
}
?>
-
loadCustomApps() as $customApp) {
+ $customName = strtolower($customApp['name']);
+ $customId = $starr->getStarrInterfaceIdFromName($customName);
+ $active = $app == $customName;
+ $starrAppCount = count(array_filter($starrsTable ?: [], fn($starrDbApp) => $starrDbApp['starr'] == $customId));
+ $icon = $customApp['icon'] ? 'images/custom/' . $customApp['icon'] : 'images/logo-16.png';
+
+ ?>
+
+
|