- 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.
193 lines
6.6 KiB
Lua
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
|