Skip to content

Commit

Permalink
Update to latest Immich API, link to photos, replace photos on republish
Browse files Browse the repository at this point in the history
  • Loading branch information
midzelis committed Sep 18, 2024
1 parent 1946ff8 commit 90666d2
Show file tree
Hide file tree
Showing 12 changed files with 3,827 additions and 68 deletions.
12 changes: 6 additions & 6 deletions Info.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
--
-- This file is part of LR-Immich.
--
-- Foobar is free software: you can redistribute it and/or modify it under the terms
-- of the GNU General Public License as published by the Free Software Foundation,
-- Foobar is free software: you can redistribute it and/or modify it under the terms
-- of the GNU General Public License as published by the Free Software Foundation,
-- either version 3 of the License, or (at your option) any later version.
--
-- LR-Immich is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-- without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-- LR-Immich is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-- without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-- See the GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License along with Foobar.
-- You should have received a copy of the GNU General Public License along with Foobar.
-- If not, see <https://www.gnu.org/licenses/>.
--

Expand All @@ -27,5 +27,5 @@ return {
file = "publisher.lua",
},
LrInitPlugin = "init.lua",
VERSION = { major = 1, minor = 0, revision = 0, build = "0", },
VERSION = { major = 2, minor = 1, revision = 0, build = "0", },
}
27 changes: 14 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
# Immich Publisher [im]

A plugin for Lightroom Classic to publish collections from Lightroom into Immich Albums.
A plugin for Lightroom Classic to publish collections from Lightroom into Immich Albums.

This plugin never deletes images!
This plugin never deletes images!

This plugin creates a relationship between local photos and collections and immich assets and albums. If the same photo is present in multiple collections, that relationship is maintained in immich - there are no additional copies created, just memberships.
This plugin creates a relationship between local photos and collections and immich assets and albums. If the same photo is present in multiple collections, that relationship is maintained in immich - there are no additional copies created, just memberships.

Exports will upload photos to immich without adding them to an alubm. However, if you later create a published collection with those previously exported pictures, the photos will not be re-uploaded, only the memberships will be added.
Exports will upload photos to Immich without adding them to an alubm. However, if you later create a published collection with those previously exported pictures, the photos will not be re-uploaded, only the memberships will be added.

Export and Publish functions work.
Both "Export" and "Publish" functions work.

### Known issues
* Immich doesn't support overwriting an image for the time being. So, if you make edits to an image in Lightroom, and try to republish, it will not show up on immich
* I'll be enhancing Immich in the future to support this.
* If you remove a photo from a Lightroom collection, I haven't been able to figure out how to get notified.
* Workaround
* Mark a file for republishing. This will cause the plugin to run, and it will check that all the local files are synced to the remote.

- If you remove a photo from a published Lightroom collection - the "Publish" action will not remove the photo right away. It appears that Lightroom does not invoke the Publishing plugin when this happens - at least I haven't been able to figure this out yet.
- Workaround
- Mark a file for republishing. This will cause the plugin to run, and it will check that all the local files are synced to the remote.

### Advanced
Turn on Logging if you want to see more information about what is being send to the server. Verbose logging is very versbose, and normal logging is probably the most you want.

Turn on Logging if you want to see more information about what is being send to the server. Verbose logging is very versbose, and normal logging is probably the most you want.

### Future
I plan on adding keywords, comments, likes, and additional enhancements in the future.

PRs Welcome!
I plan on adding keywords, comments, likes, and additional enhancements in the future.

PRs Welcome!
97 changes: 86 additions & 11 deletions immich.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
--

Immich = {}
local LrFileUtils = import 'LrFileUtils'

function Immich:new(server, api_key)
local immich = {}
Expand Down Expand Up @@ -46,9 +47,13 @@ end
function _invoke(func)
local result = func()
if not result then
LrErrors.throwUserError("error")
log:trace("error!!", result)
LrErrors.throwUserError("Error sending HTTP(S) request")
end
log:trace("HTTP Response: ", result)
if (result == '') then
return {}
end
return json.decode(result)
end

Expand All @@ -64,17 +69,27 @@ function Immich:_send(url, body, method)
return _invoke(function() return LrHttp.post(url, encoded, headers, method) end)
end

function Immich:_postMultipart(url, formdata)
function Immich:_postMultipart(url, formdata, method)
log:trace('POST (multipart) ' .. url)
return _invoke(function() return LrHttp.postMultipart(url, formdata, self:_authHeaders()) end)
return _invoke(function() return LrHttp.postMultipart(url, formdata, self:_authHeaders(), method) end)
end

function Immich:_url(path)
return self.server .. path
end

function Immich:asset_trash(identifiers)
local url = self:_url('/api/assets')
local body = {
ids = identifiers,
}
local response = self:_send(url, body, 'DELETE')
local set = Set:new(response.existingIds)
return set.items
end

function Immich:asset_exists(identifiers)
local url = self:_url('/api/asset/exist')
local url = self:_url('/api/assets/exist')
local body = {
deviceAssetIds = identifiers,
deviceId = self.deviceId,
Expand All @@ -85,12 +100,15 @@ function Immich:asset_exists(identifiers)
end

function Immich:asset_upload(photo, rendered)
local url = self:_url('/api/asset/upload')
local url = self:_url('/api/assets')
local name = LrPathUtils.leafName(rendered)
local date = photo:getRawMetadata("dateTimeOriginalISO8601")
-- local fileSize = LrFileUtils.fileAttributes('/tmp/out.jpg').fileSize
-- log:debug("fileSize", fileSize)
rendered = string.gsub(rendered, " ", "\\ ")

local formdata = {
{ name = 'assetData', filePath = rendered, fileName = name },
{ name = 'assetData', filePath = rendered, fileName = name, contentType = 'application/octet-stream' },
{ name = 'deviceAssetId', value = photo.localIdentifier },
{ name = 'deviceId', value = self.deviceId },
{ name = 'fileCreatedAt', value = date },
Expand All @@ -100,13 +118,62 @@ function Immich:asset_upload(photo, rendered)
return self:_postMultipart(url, formdata)
end

function Immich:_cmdline_quote()
if WIN_ENV then
return '"'
elseif MAC_ENV then
return ''
else
return ''
end
end

function Immich:_curl_cmd()
if WIN_ENV then
return '"' .. LrPathUtils.child(LrPathUtils.child(_PLUGIN.path, "win64-curl"), "curl.exe") .. '"'
elseif MAC_ENV then
return 'curl'
else
return ''
end
end

function Immich:asset_replace_upload(assetId, photo, rendered)
local url = self:_url('/api/assets/' .. assetId .. '/original')
local name = LrPathUtils.leafName(rendered)
local date = photo:getRawMetadata("dateTimeOriginalISO8601")
rendered = string.gsub(rendered, " ", "\\ ")

local curl = self:_curl_cmd();

local cmd = string.format(
'%s -L -X PUT "%s" ' ..
'-H "x-api-key: %s" ' ..
'-H "Content-Type: multipart/form-data" ' ..
'-H "Accept: application/json" ' ..
'-F "fileCreatedAt=\"%s\"" ' ..
'-F "fileModifiedAt=\"%s\"" ' ..
'-F "deviceId=\"%s\"" ' ..
'-F "deviceAssetId=\"%s\"" ' ..
'-F "assetData=@\"%s;filename=%s\""',
curl, url, self.api_key, date, date, self.deviceId, photo.localIdentifier, rendered, name
)

local fullCmd = self:_cmdline_quote() .. cmd .. self:_cmdline_quote()
log:info('Executing: ' .. fullCmd)
-- LrDialogs.confirm('hi')
-- LrErrors.throwUserError("abort");
local code = LrTasks.execute(fullCmd)
log:info('Result from curl PUT', code)
end

function Immich:album_info(id)
local url = self:_url('/api/album/' .. id)
local url = self:_url('/api/albums/' .. id)
return self:_get(url)
end

function Immich:create_album(albumName, description, assetIds)
local url = self:_url('/api/album')
local url = self:_url('/api/albums')
local body = {
albumName = albumName,
description = description,
Expand All @@ -116,8 +183,8 @@ function Immich:create_album(albumName, description, assetIds)
end

function Immich:add_album_assets(albumId, assetIds)
log:trace('add_album_assets', albumId, assetIds)
local url = self:_url('/api/album/' .. albumId .. '/assets')
log:trace('add_album_assets', albumId, Set:new(assetIds))
local url = self:_url('/api/albums/' .. albumId .. '/assets')
local body = {
ids = assetIds,
}
Expand All @@ -126,7 +193,7 @@ end

function Immich:remove_album_assets(albumId, assetIds)
log:trace('remove_album_assets', albumId, assetIds)
local url = self:_url('/api/album/' .. albumId .. '/assets')
local url = self:_url('/api/albums/' .. albumId .. '/assets')
local body = {
ids = assetIds,
}
Expand All @@ -142,4 +209,12 @@ function Immich:albumWebUrl(albumId)
return self:_url('/albums/' .. albumId)
end

function Immich:albumAssetWebUrl(albumId, assetId)
return self:_url('/albums/' .. albumId .. '/photos/' .. assetId)
end

function Immich:assetWebUrl(assetId)
return self:_url('/photos/' .. assetId)
end

return Immich
6 changes: 3 additions & 3 deletions plugininfo.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
-- If not, see <https://www.gnu.org/licenses/>.

function handle(properties, key, newValue)
prefs[key]=newValue
prefs[key] = newValue
end

local function sectionsForTopOfDialog(f, props)
Expand All @@ -26,7 +26,7 @@ local function sectionsForTopOfDialog(f, props)
props:addObserver('verbose', handle);

local section = {
bind_to_object= props,
bind_to_object = props,
title = "Immich Options",
f:row {
f:static_text {
Expand All @@ -53,7 +53,7 @@ local function sectionsForTopOfDialog(f, props)
width = 20,
},
f:checkbox {
enabled = bind( {key= 'debug'}),
enabled = bind({ key = 'debug' }),
margin_left = 5,
title = 'Verbose',
spacing = 10,
Expand Down
Loading

0 comments on commit 90666d2

Please sign in to comment.