Was mir noch fehlt war ein einfaches Plugin, mit dem ich aus Adobe Lightroom meine Bilder auf den WordPress Blog hochladen kann. Also statt in einen Ordner exportieren und dann manuell auf WordPress hochladen, einfach direkt hochladen.
Findige Leser werden jetzt behaupten, das gibts doch bereits. Ja und nein. Ja, es gibt bereits Lösungen, aber die haben alle unnötige Zusatzfunktionen. Ich brauche wirklich nur das Hochladen.
Das habe ich nun mit Hilfe von KI und Ausprobieren und Anpassen erstellt. Vielleicht will die eine oder andere Person das Plugin auch nutzen 😎.
Info.lua
return {
LrSdkVersion = 6.0,
LrSdkMinimumVersion = 6.0,
LrToolkitIdentifier = "ch.krokodilbirne.lr-wp-uploader",
LrPluginName = "WP Media Uploader (Simple)",
-- Einstellungen im Zusatzmodul-Manager
LrPluginInfoProvider = "PluginInfoProvider.lua",
LrExportServiceProvider = {
title = "Upload to WordPress Media Library",
file = "ExportServiceProvider.lua",
},
VERSION = { major=1, minor=0, revision=0, build=1 },
}
ExportServiceProvider.lua
local LrDialogs = import 'LrDialogs'
local LrView = import 'LrView'
local LrPrefs = import 'LrPrefs'
local LrProgressScope = import 'LrProgressScope'
local LrPathUtils = import 'LrPathUtils'
local prefs = LrPrefs.prefsForPlugin()
local ExportServiceProvider = {}
function ExportServiceProvider.startDialog(propertyTable)
-- Defaults aus Prefs, aber im Dialog überschreibbar
propertyTable.wpUrl = propertyTable.wpUrl or prefs.wpUrl or ""
propertyTable.wpUser = propertyTable.wpUser or prefs.wpUser or ""
-- Passwort NICHT im Exportdialog anzeigen.
-- Falls ein altes Preset noch wpAppPass enthält, lassen wir es in propertyTable stehen.
propertyTable.wpAppPass = propertyTable.wpAppPass or ""
end
function ExportServiceProvider.sectionsForTopOfDialog(f, propertyTable)
local bind = LrView.bind
-- Bildpfad (muss im Plugin-Ordner liegen)
local helpImagePath = LrPathUtils.child(_PLUGIN.path, "helper.png")
local hasHelpImage = false
pcall(function()
-- LrFileUtils wäre möglich, aber wir vermeiden zusätzliche imports.
-- Wir versuchen einfach das Bild zu verwenden; falls es fehlt, zeigen wir Text.
hasHelpImage = (helpImagePath ~= nil and helpImagePath ~= "")
end)
local rows = {
{
title = "WordPress Upload",
f:row {
spacing = f:control_spacing(),
f:static_text { title = "WordPress URL", width = 120 },
f:static_text { title = bind 'wpUrl', width_in_chars = 40 },
},
f:row {
spacing = f:control_spacing(),
f:static_text { title = "Username", width = 120 },
f:static_text { title = bind 'wpUser', width_in_chars = 40 },
},
f:spacer { height = 10 },
f:static_text {
title =
"Die WordPress-Zugangsdaten werden im Zusatzmodul-Manager (siehe Grafik) konfiguriert.",
width_in_chars = 75,
},
f:spacer { height = 10 },
}
}
-- Grafik einblenden (falls vorhanden)
-- Wenn helper.png fehlt, bleibt wenigstens der Text sichtbar.
table.insert(rows[1], f:picture {
value = helpImagePath,
width = 520, -- bei Bedarf anpassen
height = 110, -- bei Bedarf anpassen
})
return rows
end
function ExportServiceProvider.processRenderedPhotos(functionContext, exportContext)
local Upload = require "Upload"
local exportSession = exportContext.exportSession
local props = exportContext.propertyTable
local wpUrl = props.wpUrl or ""
local wpUser = props.wpUser or ""
-- Passwort kommt aus den gespeicherten Einstellungen (Zusatzmodul-Manager)
local wpPass = prefs.wpAppPass or ""
-- Fallback: falls ein altes Export-Preset noch ein Passwort mitgibt
if (wpPass == "" or wpPass == nil) and props.wpAppPass and props.wpAppPass ~= "" then
wpPass = props.wpAppPass
end
if wpUrl == "" or wpUser == "" or wpPass == "" then
LrDialogs.message(
"Fehlende Angaben",
"Bitte WordPress URL und Username ausfüllen.\n\n" ..
"Das App Password wird im Zusatzmodul-Manager gespeichert:\n" ..
"Im Export-Dialog unten links auf „Zusatzmodul-Manager…“ klicken → Plugin auswählen.",
"critical"
)
return
end
-- URL/User merken (Passwort NICHT aus dem Exportdialog überschreiben)
prefs.wpUrl = wpUrl
prefs.wpUser = wpUser
local total = exportSession:countRenditions()
local progress = LrProgressScope{ title = "Upload zu WordPress" }
progress:setCancelable(true)
local okCount, failCount = 0, 0
local failLines = {}
for i, rendition in exportSession:renditions() do
if progress:isCanceled() then break end
progress:setPortionComplete(i - 1, total)
progress:setCaption(string.format("Exportiere & lade hoch (%d/%d)…", i, total))
local success, pathOrMessage = rendition:waitForRender()
if success then
local exportedPath = pathOrMessage
local ok, mediaId, mediaUrl, err = Upload.uploadFile(wpUrl, wpUser, wpPass, exportedPath, nil)
if ok then
okCount = okCount + 1
else
failCount = failCount + 1
table.insert(failLines, "- " .. LrPathUtils.leafName(exportedPath) .. ": " .. tostring(err))
end
else
failCount = failCount + 1
table.insert(failLines, "- Rendition fehlgeschlagen: " .. tostring(pathOrMessage))
end
end
progress:done()
local msg = string.format("Fertig.\n\nErfolgreich: %d\nFehlgeschlagen: %d", okCount, failCount)
if failCount > 0 then
msg = msg .. "\n\nDetails:\n" .. table.concat(failLines, "\n")
LrDialogs.message("Upload abgeschlossen (mit Fehlern)", msg, "warning")
else
LrDialogs.message("Upload abgeschlossen", msg, "info")
end
end
return ExportServiceProvider
Upload.lua
local LrHttp = import "LrHttp"
local LrFileUtils = import "LrFileUtils"
local LrPathUtils = import "LrPathUtils"
local json = require "json"
local Upload = {}
local function base64encode(data)
local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
return ((data:gsub(".", function(x)
local r, byte = "", x:byte()
for i = 8, 1, -1 do
r = r .. ((byte % 2^i - byte % 2^(i-1) > 0) and "1" or "0")
end
return r
end) .. "0000"):gsub("%d%d%d?%d?%d?%d?", function(x)
if (#x < 6) then return "" end
local c = 0
for i = 1, 6 do
c = c + ((x:sub(i,i) == "1") and 2^(6-i) or 0)
end
return b:sub(c+1, c+1)
end) .. ({ "", "==", "=" })[#data % 3 + 1])
end
local function normalizeBaseUrl(url)
if not url or url == "" then return "" end
url = url:gsub("^%s+", ""):gsub("%s+$", "")
url = url:gsub("/+$", "")
return url
end
local function guessMimeType(filePath)
local ext = (LrPathUtils.extension(filePath) or ""):lower()
if ext == "jpg" or ext == "jpeg" then return "image/jpeg" end
if ext == "png" then return "image/png" end
if ext == "webp" then return "image/webp" end
if ext == "tif" or ext == "tiff" then return "image/tiff" end
return "application/octet-stream"
end
local function wpErrorMessage(body)
if not body or body == "" then return nil end
local decoded
local ok = pcall(function() decoded = json.decode(body) end)
if ok and type(decoded) == "table" then
if decoded.message then return tostring(decoded.message) end
if decoded.code then return tostring(decoded.code) end
end
return tostring(body)
end
-- Returns: ok(bool), mediaId(number|nil), mediaUrl(string|nil), errMsg(string|nil)
function Upload.uploadFile(wpBaseUrl, username, appPassword, filePath, titleOverride)
wpBaseUrl = normalizeBaseUrl(wpBaseUrl)
if wpBaseUrl == "" then return false, nil, nil, "WordPress-URL ist leer." end
if not username or username == "" then return false, nil, nil, "Username ist leer." end
if not appPassword or appPassword == "" then return false, nil, nil, "App Password ist leer." end
local endpoint = wpBaseUrl .. "/wp-json/wp/v2/media"
local fileName = LrPathUtils.leafName(filePath)
local fileData = LrFileUtils.readFile(filePath)
if not fileData then
return false, nil, nil, "Datei kann nicht gelesen werden: " .. tostring(filePath)
end
local auth = "Basic " .. base64encode(username .. ":" .. appPassword)
local mimeType = guessMimeType(filePath)
local headers = {
{ field = "Authorization", value = auth },
{ field = "Content-Type", value = mimeType },
{ field = "Content-Disposition", value = 'attachment; filename="' .. fileName .. '"' },
}
local body, respHeaders = LrHttp.post(endpoint, fileData, headers)
local statusCode = nil
if type(respHeaders) == "table" then
statusCode = respHeaders.statusCode
if not statusCode and respHeaders.status then
statusCode = tonumber(tostring(respHeaders.status):match("^(%d%d%d)"))
end
end
if statusCode ~= 201 then
local msg = "Upload fehlgeschlagen."
if statusCode then msg = msg .. " HTTP " .. tostring(statusCode) end
local m = wpErrorMessage(body)
if m then msg = msg .. " Antwort: " .. m end
return false, nil, nil, msg
end
local decoded
local okDecode, decodeErr = pcall(function()
decoded = json.decode(body)
end)
if not okDecode or type(decoded) ~= "table" then
return false, nil, nil, "Upload war 201, aber JSON konnte nicht gelesen werden: " .. tostring(decodeErr or body)
end
local mediaId = decoded.id
local mediaUrl = decoded.source_url
-- Optionaler Titel-Update
if titleOverride and titleOverride ~= "" and mediaId then
local patchUrl = wpBaseUrl .. "/wp-json/wp/v2/media/" .. tostring(mediaId)
local patchBody = json.encode({ title = titleOverride })
local patchHeaders = {
{ field = "Authorization", value = auth },
{ field = "Content-Type", value = "application/json" },
}
pcall(function()
LrHttp.post(patchUrl, patchBody, patchHeaders)
end)
end
return true, mediaId, mediaUrl, nil
end
return Upload
PluginInfoProvider.lua
local LrView = import 'LrView'
local LrDialogs = import 'LrDialogs'
local LrPrefs = import 'LrPrefs'
local prefs = LrPrefs.prefsForPlugin()
return {
sectionsForTopOfDialog = function(f)
local bind = LrView.bind
return {
{
title = "Gespeicherte WordPress Zugangsdaten",
f:row {
spacing = f:control_spacing(),
f:static_text { title = "WordPress URL", width = 120 },
f:edit_field { value = bind { key = 'wpUrl', object = prefs }, width_in_chars = 40 },
},
f:row {
spacing = f:control_spacing(),
f:static_text { title = "Username", width = 120 },
f:edit_field { value = bind { key = 'wpUser', object = prefs }, width_in_chars = 40 },
},
f:row {
spacing = f:control_spacing(),
f:static_text { title = "App Password", width = 120 },
f:password_field { value = bind { key = 'wpAppPass', object = prefs }, width_in_chars = 40 },
},
f:row {
spacing = f:control_spacing(),
f:push_button {
title = "Zuruecksetzen",
action = function()
if LrDialogs.confirm("Wirklich loeschen?", "URL / Username / App Password werden entfernt.") == "ok" then
prefs.wpUrl = ""
prefs.wpUser = ""
prefs.wpAppPass = ""
end
end
},
},
}
}
end,
}
json.lua
-- Minimal JSON (decode + encode) for Lightroom Lua environments without LrHttp.decodeJson/encodeJson
-- Based on a small public-domain style implementation (trimmed).
local json = {}
local function decodeError(str, idx, msg)
error("JSON decode error at " .. tostring(idx) .. ": " .. msg .. " near '" .. str:sub(idx, idx+20) .. "'")
end
local function skipWhitespace(str, idx)
local _, e = str:find("^[ \n\r\t]+", idx)
return (e and e + 1) or idx
end
local function parseNull(str, idx)
if str:sub(idx, idx+3) == "null" then return nil, idx+4 end
decodeError(str, idx, "expected null")
end
local function parseTrue(str, idx)
if str:sub(idx, idx+3) == "true" then return true, idx+4 end
decodeError(str, idx, "expected true")
end
local function parseFalse(str, idx)
if str:sub(idx, idx+4) == "false" then return false, idx+5 end
decodeError(str, idx, "expected false")
end
local function parseNumber(str, idx)
local s, e = str:find("^-?%d+%.?%d*[eE]?[+-]?%d*", idx)
if not s then decodeError(str, idx, "expected number") end
local n = tonumber(str:sub(s, e))
if n == nil then decodeError(str, idx, "invalid number") end
return n, e+1
end
local escapes = {
['"'] = '"', ['\\'] = '\\', ['/'] = '/',
['b'] = '\b', ['f'] = '\f', ['n'] = '\n', ['r'] = '\r', ['t'] = '\t'
}
local function parseString(str, idx)
if str:sub(idx, idx) ~= '"' then decodeError(str, idx, "expected string") end
idx = idx + 1
local out = {}
while idx <= #str do
local c = str:sub(idx, idx)
if c == '"' then
return table.concat(out), idx + 1
elseif c == "\\" then
local esc = str:sub(idx+1, idx+1)
if esc == "u" then
local hex = str:sub(idx+2, idx+5)
if not hex:match("%x%x%x%x") then decodeError(str, idx, "bad unicode escape") end
local code = tonumber(hex, 16)
-- UTF-8 encode
if code <= 0x7F then
table.insert(out, string.char(code))
elseif code <= 0x7FF then
table.insert(out, string.char(0xC0 + math.floor(code/0x40)))
table.insert(out, string.char(0x80 + (code % 0x40)))
else
table.insert(out, string.char(0xE0 + math.floor(code/0x1000)))
table.insert(out, string.char(0x80 + (math.floor(code/0x40) % 0x40)))
table.insert(out, string.char(0x80 + (code % 0x40)))
end
idx = idx + 6
else
local repl = escapes[esc]
if not repl then decodeError(str, idx, "bad escape") end
table.insert(out, repl)
idx = idx + 2
end
else
table.insert(out, c)
idx = idx + 1
end
end
decodeError(str, idx, "unterminated string")
end
local parseValue
local function parseArray(str, idx)
if str:sub(idx, idx) ~= "[" then decodeError(str, idx, "expected [") end
idx = idx + 1
local res = {}
idx = skipWhitespace(str, idx)
if str:sub(idx, idx) == "]" then return res, idx+1 end
local n = 1
while true do
local val; val, idx = parseValue(str, idx)
res[n] = val; n = n + 1
idx = skipWhitespace(str, idx)
local c = str:sub(idx, idx)
if c == "]" then return res, idx+1 end
if c ~= "," then decodeError(str, idx, "expected , or ]") end
idx = skipWhitespace(str, idx+1)
end
end
local function parseObject(str, idx)
if str:sub(idx, idx) ~= "{" then decodeError(str, idx, "expected {") end
idx = idx + 1
local res = {}
idx = skipWhitespace(str, idx)
if str:sub(idx, idx) == "}" then return res, idx+1 end
while true do
local key; key, idx = parseString(str, idx)
idx = skipWhitespace(str, idx)
if str:sub(idx, idx) ~= ":" then decodeError(str, idx, "expected :") end
idx = skipWhitespace(str, idx+1)
local val; val, idx = parseValue(str, idx)
res[key] = val
idx = skipWhitespace(str, idx)
local c = str:sub(idx, idx)
if c == "}" then return res, idx+1 end
if c ~= "," then decodeError(str, idx, "expected , or }") end
idx = skipWhitespace(str, idx+1)
end
end
parseValue = function(str, idx)
idx = skipWhitespace(str, idx)
local c = str:sub(idx, idx)
if c == "{" then return parseObject(str, idx) end
if c == "[" then return parseArray(str, idx) end
if c == '"' then return parseString(str, idx) end
if c == "n" then return parseNull(str, idx) end
if c == "t" then return parseTrue(str, idx) end
if c == "f" then return parseFalse(str, idx) end
return parseNumber(str, idx)
end
function json.decode(str)
if type(str) ~= "string" then error("json.decode expects string") end
local val, idx = parseValue(str, 1)
idx = skipWhitespace(str, idx)
return val
end
-- Minimal encoder (genug für {title="..."} )
local function encodeString(s)
s = s:gsub('\\', '\\\\'):gsub('"','\\"'):gsub("\n","\\n"):gsub("\r","\\r"):gsub("\t","\\t")
return '"' .. s .. '"'
end
local function encodeValue(v)
local t = type(v)
if v == nil then return "null" end
if t == "string" then return encodeString(v) end
if t == "number" or t == "boolean" then return tostring(v) end
if t == "table" then
local isArray = (#v > 0)
if isArray then
local parts = {}
for i=1,#v do parts[i] = encodeValue(v[i]) end
return "[" .. table.concat(parts, ",") .. "]"
else
local parts = {}
for k,val in pairs(v) do
table.insert(parts, encodeString(tostring(k)) .. ":" .. encodeValue(val))
end
return "{" .. table.concat(parts, ",") .. "}"
end
end
error("json.encode unsupported type: " .. t)
end
function json.encode(v)
return encodeValue(v)
end
return json
In WordPress einfach einen neuen User (der wird als technischer User gebraucht) anlegen und ein App-Passowrt dafür erstellen. Das Credential dann verwenden fürs Plugin.

