feat(api): enhance image module management and export functionality
- Added read-write access to the darktable SQLite catalog for module parameter edits. - Implemented new endpoints for exporting images and retrieving full-resolution images. - Introduced PATCH method for updating module parameters with optional preview. - Added comprehensive tests for new API functionalities, including module retrieval and error handling. - Updated CORS headers to support new HTTP methods. - Created detailed documentation for new API endpoints and module operations.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
-- Read-only access to the darktable SQLite catalog.
|
||||
-- Darktable SQLite catalog access (read-only for image queries; read-write for
|
||||
-- module-parameter edits via the history table).
|
||||
-- Schema inspected from an actual darktable library (recent version):
|
||||
--
|
||||
-- images(id, group_id, film_id, filename, width, height, aspect_ratio,
|
||||
@@ -27,9 +28,10 @@ local unpack = unpack or table.unpack -- Lua 5.1 vs 5.2+ compatibility
|
||||
|
||||
local M = {}
|
||||
|
||||
-- ── Connection (single, lazy, read-only) ─────────────────────────────────────
|
||||
-- ── Connections ───────────────────────────────────────────────────────────────
|
||||
|
||||
local _db
|
||||
local _db -- read-only, lazy
|
||||
local _db_rw -- read-write, opened on first write
|
||||
|
||||
local function get_db()
|
||||
if _db then return _db end
|
||||
@@ -37,12 +39,22 @@ local function get_db()
|
||||
if not _db then
|
||||
error("Cannot open darktable library: " .. config.library_db)
|
||||
end
|
||||
-- Attach data.db for the canonical tags table (darktable ≥ 2.4)
|
||||
local data_db = config.data_db:gsub("'", "''")
|
||||
_db:exec(string.format("ATTACH DATABASE '%s' AS data", data_db))
|
||||
return _db
|
||||
end
|
||||
|
||||
local function get_db_rw()
|
||||
if _db_rw then return _db_rw end
|
||||
_db_rw = sqlite3.open(config.library_db) -- read-write
|
||||
if not _db_rw then
|
||||
error("Cannot open darktable library for writing: " .. config.library_db)
|
||||
end
|
||||
-- WAL mode: allow concurrent reads from the darktable process.
|
||||
_db_rw:exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;")
|
||||
return _db_rw
|
||||
end
|
||||
|
||||
-- ── Query helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
local function query(sql, params)
|
||||
@@ -384,4 +396,129 @@ function M.list_films(filters)
|
||||
return films
|
||||
end
|
||||
|
||||
-- ── Module history ────────────────────────────────────────────────────────────
|
||||
-- The history table stores one or more entries per (imgid, operation).
|
||||
-- Only the entry with the highest `num` is the active one.
|
||||
-- `module` column = the params-struct version number for that row.
|
||||
|
||||
local modules_mod -- lazy-loaded to avoid circular requires
|
||||
|
||||
local function get_modules_mod()
|
||||
if not modules_mod then modules_mod = require "modules" end
|
||||
return modules_mod
|
||||
end
|
||||
|
||||
local function history_row_to_module(row)
|
||||
local mods = get_modules_mod()
|
||||
local blob = row.op_params or ""
|
||||
local ver = row.module or 0
|
||||
local params = mods.decode(row.operation, ver, blob)
|
||||
return {
|
||||
operation = row.operation,
|
||||
enabled = row.enabled ~= 0,
|
||||
module_version = ver,
|
||||
multi_name = row.multi_name or "",
|
||||
multi_priority = row.multi_priority or 0,
|
||||
history_num = row.num,
|
||||
params = params, -- nil for unknown modules
|
||||
params_b64 = params and nil or require("base64").encode(blob),
|
||||
}
|
||||
end
|
||||
|
||||
-- Return the active (highest-num) history entry for every operation on imgid.
|
||||
function M.list_modules(imgid)
|
||||
local rows = query([[
|
||||
SELECT h.operation, h.module, h.enabled, h.op_params,
|
||||
h.multi_name, h.multi_priority, h.num
|
||||
FROM history h
|
||||
INNER JOIN (
|
||||
SELECT operation, MAX(num) AS max_num
|
||||
FROM history WHERE imgid = ?
|
||||
GROUP BY operation
|
||||
) latest ON h.operation = latest.operation AND h.num = latest.max_num
|
||||
WHERE h.imgid = ?
|
||||
ORDER BY h.operation
|
||||
]], { tonumber(imgid), tonumber(imgid) })
|
||||
|
||||
local result = {}
|
||||
for _, row in ipairs(rows) do
|
||||
result[#result+1] = history_row_to_module(row)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Return the raw op_params binary blob for one operation (latest history entry).
|
||||
function M.get_module_blob(imgid, operation)
|
||||
local row = query_one([[
|
||||
SELECT op_params FROM history
|
||||
WHERE imgid = ? AND operation = ?
|
||||
ORDER BY num DESC LIMIT 1
|
||||
]], { tonumber(imgid), operation })
|
||||
return row and row.op_params or nil
|
||||
end
|
||||
|
||||
-- Return the single active history entry for one operation.
|
||||
local row = query_one([[
|
||||
SELECT h.operation, h.module, h.enabled, h.op_params,
|
||||
h.multi_name, h.multi_priority, h.num
|
||||
FROM history h
|
||||
WHERE h.imgid = ? AND h.operation = ?
|
||||
ORDER BY h.num DESC LIMIT 1
|
||||
]], { tonumber(imgid), operation })
|
||||
if not row then return nil end
|
||||
return history_row_to_module(row)
|
||||
end
|
||||
|
||||
-- Update the active history entry for an operation.
|
||||
-- `new_blob` (string|nil) – new binary op_params; nil to leave unchanged
|
||||
-- `new_enabled` (bool|nil) – new enabled flag; nil to leave unchanged
|
||||
-- Returns true on success, or false + error message.
|
||||
function M.set_module_params(imgid, operation, new_enabled, new_blob)
|
||||
-- Find the current latest entry and its num.
|
||||
local row = query_one([[
|
||||
SELECT num, module, op_params, enabled, blendop_params, blendop_version,
|
||||
multi_priority, multi_name, multi_name_hand_edited
|
||||
FROM history
|
||||
WHERE imgid = ? AND operation = ?
|
||||
ORDER BY num DESC LIMIT 1
|
||||
]], { tonumber(imgid), operation })
|
||||
|
||||
if not row then
|
||||
return false, "module '" .. operation .. "' not found in history for image " .. imgid
|
||||
end
|
||||
|
||||
local d = get_db_rw()
|
||||
|
||||
-- Find the global max num for this image so we can insert after it.
|
||||
local max_row = query_one("SELECT MAX(num) AS m FROM history WHERE imgid=?", {tonumber(imgid)})
|
||||
local new_num = (max_row and max_row.m or 0) + 1
|
||||
|
||||
local enabled_val = (new_enabled == nil) and row.enabled or (new_enabled and 1 or 0)
|
||||
local blob_val = new_blob or row.op_params
|
||||
|
||||
-- Insert a new history entry (preserves darktable's undo history).
|
||||
local sql = [[
|
||||
INSERT INTO history
|
||||
(imgid, num, module, operation, op_params, enabled,
|
||||
blendop_params, blendop_version, multi_priority, multi_name, multi_name_hand_edited)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
]]
|
||||
local stmt = d:prepare(sql)
|
||||
if not stmt then
|
||||
return false, "SQL prepare failed: " .. (d:errmsg() or "?")
|
||||
end
|
||||
stmt:bind_values(
|
||||
tonumber(imgid), new_num, row.module, operation, blob_val, enabled_val,
|
||||
row.blendop_params, row.blendop_version,
|
||||
row.multi_priority, row.multi_name, row.multi_name_hand_edited
|
||||
)
|
||||
local rc = stmt:step()
|
||||
stmt:finalize()
|
||||
|
||||
if rc ~= sqlite3.DONE then
|
||||
return false, "INSERT failed (rc=" .. tostring(rc) .. "): " .. (d:errmsg() or "?")
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
Reference in New Issue
Block a user