This guide explains how to create plugins for Komga Enhanced.
Plugins are registered in the database and configured via the Plugin Manager UI (Settings → Plugins). Each plugin has:
my-custom-plugin)| Type | Purpose | Example |
|---|---|---|
DOWNLOAD |
Download manga from external sources | gallery-dl-downloader, mangadex-subscription |
METADATA |
Fetch metadata from external APIs | mangadex-metadata, anilist-metadata |
TASK |
Custom scheduled tasks | — |
PROCESSOR |
Content processing | — |
NOTIFIER |
Notifications | — |
ANALYZER |
Content analysis | — |
Register your plugin in PluginInitializer.kt:
// application/startup/PluginInitializer.kt
Plugin(
id = "my-downloader",
name = "My Custom Downloader",
version = "1.0.0",
author = "Your Name",
description = "Downloads manga from example.com",
enabled = false,
pluginType = PluginType.DOWNLOAD,
entryPoint = "org.gotson.komga.infrastructure.download.MyDownloader",
sourceUrl = "https://example.com",
installedDate = LocalDateTime.now(),
lastUpdated = LocalDateTime.now(),
configSchema = """
{
"type": "object",
"properties": {
"api_key": {
"type": "string",
"title": "API Key",
"description": "Your example.com API key"
},
"max_concurrent": {
"type": "integer",
"title": "Max Concurrent Downloads",
"default": 3
}
},
"required": ["api_key"]
}
""".trimIndent(),
dependencies = null,
)
The configSchema is auto-updated — if you change it, PluginInitializer updates the database on next startup.
The config schema generates the settings form in the Plugin Manager UI.
{
"type": "object",
"properties": {
"username": {
"type": "string",
"title": "Username"
},
"password": {
"type": "string",
"title": "Password",
"format": "password"
},
"language": {
"type": "string",
"title": "Language",
"default": "en",
"enum": ["en", "de", "fr", "it", "ja"]
},
"interval": {
"type": "integer",
"title": "Check Interval (minutes)",
"default": 30,
"description": "How often to check for updates"
}
},
"required": ["username", "password"]
}
| Schema Property | UI Element |
|---|---|
"type": "string" |
Text field |
"format": "password" |
Password field (masked) |
"enum": [...] |
Dropdown select |
"type": "integer" |
Number field |
"default": value |
Pre-filled default |
"description": "..." |
Hint text below field |
"required": [...] |
Required field validation |
If your plugin needs the same language as other plugins, read from the gallery-dl-downloader config instead of adding your own:
val language =
pluginConfigRepository
.findByPluginIdAndKey("gallery-dl-downloader", "default_language")
?.configValue ?: "en"
@Component
class MyDownloader(
private val pluginConfigRepository: PluginConfigRepository,
private val pluginLogRepository: PluginLogRepository,
) {
private val pluginId = "my-downloader"
// Read all config as a Map
private fun loadConfig(): Map<String, String?> =
pluginConfigRepository
.findByPluginId(pluginId)
.associate { it.configKey to it.configValue }
// Read a single config value
private fun getApiKey(): String? =
pluginConfigRepository
.findByPluginIdAndKey(pluginId, "api_key")
?.configValue
}
Plugin logs are visible in the Plugin Manager UI (Settings → Plugins → Logs tab):
private fun logToDatabase(
level: LogLevel,
message: String,
) {
pluginLogRepository.insert(
PluginLog(
id = UUID.randomUUID().toString(),
pluginId = pluginId,
logLevel = level,
message = message,
),
)
}
// Usage
logToDatabase(LogLevel.INFO, "Download started for manga XYZ")
logToDatabase(LogLevel.ERROR, "API returned 403: ${response.body()}")
Log levels: DEBUG, INFO, WARN, ERROR
private fun isEnabled(): Boolean =
try {
pluginRepository.findByIdOrNull(pluginId)?.enabled == true
} catch (_: Exception) {
false
}
Base interfaces are defined in infrastructure/plugin/PluginApi.kt:
For plugins that fetch metadata from external sources:
interface MetadataProviderPlugin : KomgaPlugin {
fun searchSeries(query: String, language: String?): List<SeriesSearchResult>
fun getSeriesMetadata(seriesId: String, sourceUrl: String): SeriesMetadataResult?
fun getBookMetadata(bookId: String, sourceUrl: String): BookMetadataResult?
fun getSupportedLanguages(): List<String>
fun canHandle(url: String): Boolean
}
For plugins that download content:
interface DownloadProviderPlugin : KomgaPlugin {
fun canHandleUrl(url: String): Boolean
fun getAvailableChapters(sourceUrl: String): List<ChapterInfo>
fun startDownload(request: DownloadRequest): DownloadQueue
fun cancelDownload(queueId: String): Boolean
fun getProgress(queueId: String): DownloadProgress
fun checkForUpdates(sourceUrl: String, lastKnownChapter: String?): UpdateCheckResult
}
If your plugin runs a background scheduler, register it for restart when config is saved. In PluginController.kt:
// In updatePluginConfig()
if (id == "my-downloader") {
myDownloader.restart()
}
Implement restart() in your plugin:
fun restart() {
stopScheduler()
// Reset state
startIfEnabled() // Reloads config
}
All plugin endpoints require ADMIN role.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/plugins |
List all plugins |
| GET | /api/v1/plugins/{id} |
Get plugin details |
| PATCH | /api/v1/plugins/{id} |
Enable/disable ({"enabled": true}) |
| DELETE | /api/v1/plugins/{id} |
Uninstall plugin |
| GET | /api/v1/plugins/{id}/config |
Get config as Map<String, String> |
| POST | /api/v1/plugins/{id}/config |
Save config (replaces all keys) |
| GET | /api/v1/plugins/{id}/logs |
Get logs (paginated) |
| DELETE | /api/v1/plugins/{id}/logs |
Clear logs |
| Table | Purpose |
|---|---|
plugin |
Plugin registration (id, name, type, schema, enabled) |
plugin_config |
Key-value config per plugin |
plugin_log |
Plugin log entries with level and timestamp |
plugin_permission |
Permission grants (future use) |
@Component
class MyMetadataProvider(
private val pluginConfigRepository: PluginConfigRepository,
) {
private val pluginId = "my-metadata"
fun searchSeries(query: String): List<SeriesSearchResult> {
val apiKey =
pluginConfigRepository
.findByPluginIdAndKey(pluginId, "api_key")
?.configValue ?: return emptyList()
val response = httpClient.send(
HttpRequest
.newBuilder()
.uri(URI.create("https://api.example.com/search?q=$query&key=$apiKey"))
.GET()
.build(),
HttpResponse.BodyHandlers.ofString(),
)
return parseSearchResults(response.body())
}
}
Register in PluginInitializer.kt:
Plugin(
id = "my-metadata",
name = "Example Metadata",
version = "1.0.0",
author = "Your Name",
description = "Fetches metadata from example.com",
enabled = false,
pluginType = PluginType.METADATA,
entryPoint = "org.gotson.komga.infrastructure.metadata.MyMetadataProvider",
sourceUrl = "https://example.com",
installedDate = LocalDateTime.now(),
lastUpdated = LocalDateTime.now(),
configSchema = """
{
"type": "object",
"properties": {
"api_key": {
"type": "string",
"title": "API Key",
"description": "Get your key at example.com/settings"
}
},
"required": ["api_key"]
}
""".trimIndent(),
dependencies = null,
)
configSchema in PluginInitializer, restart Komga.it vs lt).findByPluginIdAndKey() for reading single values, findByPluginId() for loading all config at once.pluginLogRepository for user-visible logs in the UI.