Files
lua-scripts-w-api/contrib/darktable-api/darktable-api-bridge.lua
stephen schuresko bd40b4f850 Add darktable REST API implementation
- Introduced a new Lua-based REST API for darktable, allowing external applications to interact with the darktable library.
- Implemented core functionalities including image listing, retrieval, and preview serving.
- Added support for OAuth 2.0 authentication with token generation.
- Created a simple HTTP router to handle API requests.
- Included JSON encoding/decoding utilities for API responses.
- Developed a script to launch the API server alongside darktable, ensuring seamless integration.
- Added necessary configuration and setup for the API server, including CORS support and error handling.
2026-06-10 15:02:19 -04:00

193 lines
6.6 KiB
Lua

--[[
darktable-api-bridge.lua
Companion plugin for the darktable REST API server.
Runs inside a live darktable instance and watches a queue file written by the
standalone LuaJIT server. When image IDs appear in the queue this plugin
calls image:drop_cache() and image:generate_cache() so that darktable's
thumbnail panel reflects the latest on-disk state.
Install: add the following line to ~/.config/darktable/luarc
require "contrib/darktable-api/darktable-api-bridge"
Or let the startup script (start-with-api.sh) do it automatically.
Queue file format (written by the server's /api/v1/images/:id/refresh):
One image ID (integer) per line, e.g.:
42
7
100
--]]
local dt = require "darktable"
local du = require "lib/dtutils"
du.check_min_api_version("7.0.0", "darktable-api-bridge")
local MODULE = "darktable-api-bridge"
-- ── Configuration ─────────────────────────────────────────────────────────────
-- Must match config.refresh_queue in config.lua (or the DT_REFRESH_QUEUE env var).
local QUEUE_FILE = os.getenv("DT_REFRESH_QUEUE") or "/tmp/dt-api-refresh.queue"
local POLL_MS = 1000 -- poll interval in milliseconds
-- Mipmap range to regenerate: 0 = smallest thumbnail, 8 = full resolution.
-- Levels 0-4 cover all GUI thumbnail sizes (~67 px … ~1080 px).
local MIP_MIN = 0
local MIP_MAX = 4
-- ── Logging ───────────────────────────────────────────────────────────────────
local function log(msg)
dt.print_log("[" .. MODULE .. "] " .. msg)
end
local function notify(msg)
dt.print("[" .. MODULE .. "] " .. msg)
log(msg)
end
-- ── Image lookup ──────────────────────────────────────────────────────────────
-- Build a fast id→image map from dt.database.
-- dt.database is a table-like object; iterating it is O(n) but fast enough
-- for typical library sizes (thousands of images).
local function build_id_map()
local map = {}
for _, img in ipairs(dt.database) do
map[img.id] = img
end
return map
end
-- ── Queue file handling ───────────────────────────────────────────────────────
-- Atomically claim the queue file by renaming it, then read all IDs from it.
-- Returns a set table { [id] = true } of unique image IDs, or {} if none.
local function drain_queue()
local processing = QUEUE_FILE .. ".processing"
-- Rename is atomic on POSIX filesystems: the server's append-only writes
-- will start a fresh QUEUE_FILE after we claim this one.
local ok = os.rename(QUEUE_FILE, processing)
if not ok then
return {} -- queue file doesn't exist yet
end
local ids = {}
local f = io.open(processing, "r")
if f then
for line in f:lines() do
local id = tonumber(line)
if id then ids[id] = true end
end
f:close()
os.remove(processing)
end
return ids
end
-- ── Cache regeneration ────────────────────────────────────────────────────────
local function refresh_image(img)
local label = string.format("id=%d %s/%s", img.id, img.path, img.filename)
log("refreshing " .. label)
-- Drop the current cached thumbnails so darktable knows they're stale.
img:drop_cache()
-- Regenerate thumbnails for mip levels MIP_MIN..MIP_MAX.
-- full=false uses embedded JPEG thumbnails when available (fast);
-- set full=true to force a complete RAW render (accurate but slow).
local full = false
img:generate_cache(full, MIP_MIN, MIP_MAX)
log("done " .. label)
end
-- Process all pending IDs in one pass.
local function process_queue()
local ids = drain_queue()
local count = 0
for id, _ in pairs(ids) do
count = count + 1
end
if count == 0 then return end
log(string.format("processing %d queued refresh request(s)", count))
local id_map = build_id_map()
for id, _ in pairs(ids) do
local img = id_map[id]
if img then
local ok, err = pcall(refresh_image, img)
if not ok then
log("error refreshing id=" .. id .. ": " .. tostring(err))
end
else
log("image id=" .. id .. " not found in database (may have been removed)")
end
end
-- Nudge the lighttable to redraw so the user sees updated thumbnails
-- without having to scroll away and back.
dt.gui.views.lighttable.is_culling_mode = dt.gui.views.lighttable.is_culling_mode
end
-- ── Background polling loop ───────────────────────────────────────────────────
-- dt.control.dispatch runs the function asynchronously in darktable's worker
-- thread pool, leaving the UI thread free. We loop inside with sleep() so
-- one dispatch call drives the whole bridge.
local running = true
local function poll_loop()
notify(string.format(
"started; polling %s every %d ms", QUEUE_FILE, POLL_MS))
while running do
local ok, err = pcall(process_queue)
if not ok then
log("process_queue error: " .. tostring(err))
end
dt.control.sleep(POLL_MS)
end
notify("stopped")
end
-- ── Plugin lifecycle ──────────────────────────────────────────────────────────
local script_data = {}
script_data.metadata = {
name = "darktable API bridge",
purpose = "Watch the REST API refresh queue and regenerate image thumbnails",
author = "darktable-api",
help = "https://github.com/darktable-org/lua-scripts",
}
local function destroy()
running = false
-- Give the poll loop one final chance to exit cleanly.
dt.control.sleep(POLL_MS + 200)
dt.destroy_event(MODULE, "shortcut")
end
script_data.destroy = destroy
-- Register a manual-trigger shortcut so the user can force an immediate poll
-- without waiting for the next timer tick.
dt.register_event(MODULE, "shortcut",
function(event, shortcut)
log("manual poll triggered via shortcut")
local ok, err = pcall(process_queue)
if not ok then notify("error during manual poll: " .. tostring(err)) end
end,
"poll refresh queue now"
)
-- Start the background loop.
dt.control.dispatch(poll_loop)
return script_data