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.

Vielleicht gefällt dir auch das:

Hinterlasse einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert