diff --git a/.gitignore b/.gitignore index 82dcddb98..38fb9aff5 100755 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ Temporary Items # Files from my local computer php_errors.log +local-dev # Backup Files .pydio_id @@ -178,6 +179,7 @@ api/plugins/* !api/plugins/speedTest/ !api/plugins/shuck-stop/ api/plugins/shuck-stop/drives.json +!api/plugins/komga/ # ========================= # Custom files diff --git a/.htaccess b/.htaccess new file mode 100644 index 000000000..092865abb --- /dev/null +++ b/.htaccess @@ -0,0 +1,5 @@ +RewriteEngine On +RewriteBase /api/v2 +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^ /api/v2/index.php [QSA,L] diff --git a/api/classes/organizr.class.php b/api/classes/organizr.class.php index f3ccc3231..7c66c2ba5 100644 --- a/api/classes/organizr.class.php +++ b/api/classes/organizr.class.php @@ -41,6 +41,7 @@ class Organizr use ProwlarrHomepageItem; use JDownloaderHomepageItem; use JellyfinHomepageItem; + use KomgaHomepageItem; use LidarrHomepageItem; use MiscHomepageItem; use MonitorrHomepageItem; diff --git a/api/functions/sso-functions.php b/api/functions/sso-functions.php index 768eca81e..c7a1c260c 100644 --- a/api/functions/sso-functions.php +++ b/api/functions/sso-functions.php @@ -169,7 +169,7 @@ public function getKomgaToken($email, $password, $fallback = false) $credentials = array('auth' => new Requests_Auth_Digest(array($email, $password))); $url = $this->qualifyURL($this->config['komgaURL']); $options = $this->requestOptions($url, $this->getSSOTimeout(), true, false, $credentials); - $response = Requests::get($url . '/api/v1/users/me', ['X-Auth-Token' => 'organizrSSO'], $options); + $response = Requests::get($url . '/api/v2/users/me', ['X-Auth-Token' => 'organizrSSO'], $options); if ($response->success) { if ($response->headers['x-auth-token']) { $this->setLoggerChannel('Komga')->info('Grabbed token'); @@ -395,4 +395,4 @@ public function getPetioToken($username, $password, $oAuthToken = null, $fallbac return false; } } -} \ No newline at end of file +} diff --git a/api/homepage/komga.php b/api/homepage/komga.php new file mode 100644 index 000000000..ab946ef59 --- /dev/null +++ b/api/homepage/komga.php @@ -0,0 +1,58 @@ + 'Komga', + 'enabled' => true, + 'image' => 'plugins/images/komga.svg', + 'category' => 'Entertainment', + 'settingsArray' => __FUNCTION__ + ]; + if ($infoOnly) { + return $homepageInformation; + } + $homepageSettings = [ + 'debug' => true, + 'settings' => [ + 'Enable' => [ + $this->settingsOption('enable', 'homepageKomgaEnabled', ['label' => 'Activate Komga', 'help' => 'Display the Komga module on the home page']), + $this->settingsOption('auth', 'homepageKomgaAuth', ['label' => 'Authentification']), + ], + ] + ]; + return array_merge($homepageInformation, $homepageSettings); + } + + public function komgaHomepagePermissions($key = null) + { + $permissions = [ + 'main' => [ + 'enabled' => [ + 'homepageKomgaEnabled' + ], + 'auth' => [ + 'homepageKomgaAuth' + ] + ] + ]; + return $this->homepageCheckKeyPermissions($key, $permissions); + } + + public function homepageOrderKomga() + { + if ($this->homepageItemPermissions($this->komgaHomepagePermissions('main'))) { + + return ' +
+
+

Loading Komga Books...

+
+
+ '; + } + } + +} \ No newline at end of file diff --git a/api/plugins/komga/api.php b/api/plugins/komga/api.php new file mode 100644 index 000000000..26f19e819 --- /dev/null +++ b/api/plugins/komga/api.php @@ -0,0 +1,109 @@ +get('/plugins/komga/books/latest', function ($request, $response, $args) { + $komgaPlugin = new KomgaPlugin(); + $GLOBALS['api']['response']['data'] = []; + + if ($komgaPlugin->checkRoute($request)) { + if ($komgaPlugin->qualifyRequest($komgaPlugin->config['KOMGA-minAuth'], true)) { + $url = $komgaPlugin->config['KOMGA-url'] ?? ''; + $apiKey = $komgaPlugin->config['KOMGA-apikey'] ?? ''; + + // Check for group override + $groupId = $komgaPlugin->user['groupID'] ?? null; + $libraries = $komgaPlugin->config['KOMGA-libraries'] ?? 'all'; + + if ($groupId !== null) { + $groupOverride = $komgaPlugin->config['KOMGA-library-group-' . $groupId] ?? 'default'; + if ($groupOverride !== 'default') { + $libraries = $groupOverride; + } + } + + if ($url && $apiKey) { + $endpoint = '/api/v1/books?sort=createdDate,desc&size=20'; + if ($libraries && $libraries !== 'all') { + $endpoint .= '&library_id=' . $libraries; + } + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, rtrim($url, '/') . $endpoint); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "X-API-Key: $apiKey", + "Accept: application/json" + ]); + $res = curl_exec($ch); + curl_close($ch); + + if ($res) { + $data = json_decode($res, true); + // Provide settings to JS via our API response + $GLOBALS['api']['response']['data'] = [ + 'title' => $komgaPlugin->config['KOMGA-title'] ?? 'Recently added books', + 'baseUrl' => rtrim($url, '/') . '/book/', + 'books' => (isset($data['content']) ? $data['content'] : $data), + 'tabName' => $komgaPlugin->config['KOMGA-tab-name'] ?? 'Komga' + ]; + } + } + } + } + + $response->getBody()->write(jsonE($GLOBALS['api'])); + return $response + ->withHeader('Content-Type', 'application/json;charset=UTF-8') + ->withStatus($GLOBALS['responseCode']); +}); + +$app->get('/plugins/komga/image', function ($request, $response, $args) { + $komgaPlugin = new KomgaPlugin(); + + if ($komgaPlugin->checkRoute($request)) { + if ($komgaPlugin->qualifyRequest(999, true)) { + $apiUrl = $komgaPlugin->config['KOMGA-url'] ?? ''; + $apiKey = $komgaPlugin->config['KOMGA-apikey'] ?? ''; + + // Allow full URL (if passed via query string) or just append to Komga API URL + $urlParams = $request->getQueryParams(); + $thumbnailUrl = $urlParams['url'] ?? ''; + + if ($apiUrl && $apiKey && $thumbnailUrl) { + // Determine if thumbnailUrl is absolute or relative + if (!preg_match('~^(?:f|ht)tps?://~i', $thumbnailUrl)) { + $thumbnailUrl = rtrim($apiUrl, '/') . '/' . ltrim($thumbnailUrl, '/'); + } + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $thumbnailUrl); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "X-API-Key: $apiKey" + ]); + $res = curl_exec($ch); + $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + curl_close($ch); + + if ($res) { + $response->getBody()->write($res); + return $response + ->withHeader('Content-Type', $contentType ?: 'image/jpeg'); + } + } + } + } + + return $response->withStatus(404); +}); + +$app->get('/plugins/komga/settings', function ($request, $response, $args) { + $komgaPlugin = new KomgaPlugin(); + if ($komgaPlugin->checkRoute($request)) { + if ($komgaPlugin->qualifyRequest(1, true)) { + $GLOBALS['api']['response']['data'] = $komgaPlugin->_pluginGetSettings(); + } + } + $response->getBody()->write(jsonE($GLOBALS['api'])); + return $response + ->withHeader('Content-Type', 'application/json;charset=UTF-8') + ->withStatus($GLOBALS['responseCode']); +}); \ No newline at end of file diff --git a/api/plugins/komga/config.php b/api/plugins/komga/config.php new file mode 100644 index 000000000..c3dc7113d --- /dev/null +++ b/api/plugins/komga/config.php @@ -0,0 +1,12 @@ + false, + 'KOMGA-minAuth' => 1, + 'KOMGA-url' => '', + 'KOMGA-apikey' => '', + 'KOMGA-libraries' => 'all', + 'KOMGA-title' => 'Recently added books' +); \ No newline at end of file diff --git a/api/plugins/komga/main.js b/api/plugins/komga/main.js new file mode 100644 index 000000000..59c6d1bb3 --- /dev/null +++ b/api/plugins/komga/main.js @@ -0,0 +1,125 @@ +let KOMGA_TAB_NAME = 'Komga-(Livres)'; + +window.komgaFetchAuthImage = async function (url, elementId) { + try { + const proxyUrl = `api/v2/plugins/komga/image?url=${encodeURIComponent(url)}`; + const res = await fetch(proxyUrl); + if (!res.ok) throw new Error(); + const blob = await res.blob(); + const objectURL = URL.createObjectURL(blob); + const el = document.getElementById(elementId); + if (el) el.style.backgroundImage = `url('${objectURL}')`; + } catch (err) { + console.error("Error loading secure image", err); + } +} + +window.komgaLoadLatestBooks = async function () { + try { + const response = await fetch('api/v2/plugins/komga/books/latest', { + method: 'GET', + headers: { 'Accept': 'application/json' } + }); + const res = await response.json(); + + if (response.ok && res.response?.data) { + const data = res.response.data; + if (data.tabName) KOMGA_TAB_NAME = data.tabName; + const books = Array.isArray(data.books) ? data.books : []; + if (books.length > 0) { + renderKomgaBooks(books, data.title, data.baseUrl); + } + } + } catch (e) { + console.error('Error Komga API:', e); + } +} + +window.komgaOpenBook = function (komgaBaseUrl, bookId) { + const targetUrl = `${komgaBaseUrl}${bookId}`; + const sideBarLinks = document.querySelectorAll('.sidebar-nav a, .tab-menu-item, .nav-link'); + let found = false; + sideBarLinks.forEach(link => { + if (link.textContent.trim() === KOMGA_TAB_NAME || link.textContent.includes('Komga')) { + link.click(); + found = true; + } + }); + if (!found) window.location.hash = `#${KOMGA_TAB_NAME}`; + + setTimeout(() => { + const iframe = document.querySelector('iframe[src*="komga"]'); + if (iframe) iframe.src = targetUrl; + }, 700); +}; + +function renderKomgaBooks(books, titleDisplay, komgaBaseUrl) { + const container = document.getElementById('homepage-items'); + if (!container) return; + + let html = ` +
+
+
+ + ${titleDisplay} +
+
`; + + books.forEach((book, index) => { + const imgId = `komga-img-${index}`; + const metadata = book.metadata || {}; + const title = metadata.title || book.name || window.lang.translate('Untitled'); + const series = book.seriesTitle || ''; + const number = metadata.number ? `n°${metadata.number}` : ''; + + html += ` +
+
+
+
+ ${title} + ${series} ${number} +
+
`; + + const thumbUrl = book.thumbnailUrl || `/api/v1/books/${book.id}/thumbnail`; + window.komgaFetchAuthImage(thumbUrl, imgId); + }); + + html += `
`; + + let hookContainer = document.getElementById('komgaLatestBookContainer'); + if (!hookContainer) { + hookContainer = document.createElement('div'); + hookContainer.id = 'komgaLatestBookContainer'; + hookContainer.className = 'homepage-item'; + + const activeSessions = document.querySelector('.active-sessions-item'); + if (activeSessions) { + activeSessions.after(hookContainer); + } else { + container.appendChild(hookContainer); + } + } + + hookContainer.innerHTML = html; + hookContainer.classList.remove('hidden'); +} + +// Inject custom CSS +const style = document.createElement('style'); +style.innerHTML = ` + .komga-card-hover:hover { transform: scale(1.05) !important; } + .komga-container::-webkit-scrollbar { display: none; } + .komga-container { scrollbar-width: none; } +`; +document.head.appendChild(style); + +// Initial load handler +setTimeout(() => { + if (document.getElementById('homepage-items')) { + window.komgaLoadLatestBooks(); + } +}, 800); diff --git a/api/plugins/komga/plugin.json b/api/plugins/komga/plugin.json new file mode 100644 index 000000000..d888ce006 --- /dev/null +++ b/api/plugins/komga/plugin.json @@ -0,0 +1,14 @@ +{ + "Komga": { + "repo": "https://github.com/JamesDAdams/organizrv2-plugin-komga", + "author": "JamesAdams", + "category": "Entertainment", + "description": "Add the latest Komga books to the homepage", + "icon": "plugins/images/komga.svg", + "version": "1.0.9", + "minimum_organizr_version": "2.1.0", + "github_folder": "komga", + "project_folder": "komga", + "license": "personal" + } +} \ No newline at end of file diff --git a/api/plugins/komga/plugin.php b/api/plugins/komga/plugin.php new file mode 100644 index 000000000..ee9ba3b18 --- /dev/null +++ b/api/plugins/komga/plugin.php @@ -0,0 +1,141 @@ + 'Komga', + 'author' => 'JamesAdams', + 'category' => 'Entertainment', + 'link' => '', + 'license' => 'personal', + 'idPrefix' => 'KOMGA', + 'configPrefix' => 'KOMGA', + 'version' => '1.0.9', + 'image' => 'plugins/images/komga.svg', + 'settings' => true, + 'bind' => true, + 'api' => 'api/v2/plugins/komga/settings', + 'homepage' => true +); + +class KomgaPlugin extends Organizr +{ + public function __construct() + { + parent::__construct(); + } + + public function _pluginGetSettings() + { + $libraries = [ + ['name' => 'All Libraries', 'value' => 'all'] + ]; + + $url = $this->config['KOMGA-url'] ?? ''; + $apiKey = $this->config['KOMGA-apikey'] ?? ''; + + if ($url && $apiKey) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, rtrim($url, '/') . '/api/v1/libraries'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "X-API-Key: $apiKey", + "Accept: application/json" + ]); + // 2 second timeout so settings load doesn't hang forever + curl_setopt($ch, CURLOPT_TIMEOUT, 2); + $response = curl_exec($ch); + curl_close($ch); + + if ($response) { + $data = json_decode($response, true); + if (is_array($data) && !isset($data['status'])) { + // Komga might wrap in 'content' array + $items = isset($data['content']) ? $data['content'] : $data; + foreach ($items as $lib) { + if (isset($lib['id']) && isset($lib['name'])) { + $libraries[] = [ + 'name' => $lib['name'], + 'value' => $lib['id'] + ]; + } + } + } + } + } + + $groupsData = $this->getAllGroups(); + $groups = isset($groupsData['groups']) ? $groupsData['groups'] : $groupsData; + $groupSettings = []; + $groupLibraryOptions = array_merge([['name' => 'Default (Follow Global Setting)', 'value' => 'default']], $libraries); + + if (is_array($groups)) { + foreach ($groups as $group) { + $groupID = $group['group_id'] ?? $group['id']; + $groupName = $group['group'] ?? $group['name'] ?? 'Group ' . $groupID; + $groupSettings[] = array( + 'type' => 'select2', + 'class' => 'form-control', + 'id' => 'komga-select-library-group-' . $groupID, + 'name' => 'KOMGA-library-group-' . $groupID, + 'label' => 'Library for ' . $groupName, + 'value' => (string)($this->config['KOMGA-library-group-' . $groupID] ?? 'default'), + 'options' => $groupLibraryOptions + ); + } + } + + return array( + 'Information' => array( + array( + 'type' => 'html', + 'label' => 'Description', + 'html' => 'Configure your Komga server integration to display a tab of your recently added books on the Organizr homepage.' + ) + ), + 'Komga Settings' => array( + array( + 'type' => 'select', + 'name' => 'KOMGA-minAuth', + 'label' => 'Minimum authentication to view component', + 'value' => (string)($this->config['KOMGA-minAuth'] ?? '1'), + 'options' => $this->groupSelect() + ), + array( + 'type' => 'input', + 'name' => 'KOMGA-url', + 'label' => 'Komga URL', + 'placeholder' => 'ex: https://komga.domain.com', + 'value' => (string)($this->config['KOMGA-url'] ?? '') + ), + array( + 'type' => 'password-alt', + 'name' => 'KOMGA-apikey', + 'label' => 'Komga API Key', + 'value' => (string)($this->config['KOMGA-apikey'] ?? '') + ), + array( + 'type' => 'input', + 'name' => 'KOMGA-title', + 'label' => 'Homepage component title', + 'value' => (string)($this->config['KOMGA-title'] ?? 'Recently added books') + ), + array( + 'type' => 'input', + 'name' => 'KOMGA-tab-name', + 'label' => 'Organizr Komga Tab Name', + 'help' => 'This is the tab name that will be opened when clicking on a book.', + 'placeholder' => 'ex: Komga-(Livres)', + 'value' => (string)($this->config['KOMGA-tab-name'] ?? 'Komga') + ), + array( + 'type' => 'select2', + 'class' => 'form-control', + 'id' => 'komga-select-library', + 'name' => 'KOMGA-libraries', + 'label' => 'Global Specific Library', + 'value' => (string)($this->config['KOMGA-libraries'] ?? 'all'), + 'options' => $libraries + ) + ), + 'Group Overrides' => $groupSettings + ); + } +} \ No newline at end of file diff --git a/plugins/images/komga.svg b/plugins/images/komga.svg new file mode 100644 index 000000000..af9f28d4c --- /dev/null +++ b/plugins/images/komga.svg @@ -0,0 +1,113 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + +