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 @@
+
+