Record an album or tag write immich refused

immich answers a bulk album or tag write with HTTP 200 and one result per
asset, so a key without album permission produced "filing 0 asset(s)",
no recorded failure and exit 0. Refusals other than duplicate now come
back from the client and are recorded against the photo and its wiki
pages. The album and tag stages also get separate error handling, so a
failed album write no longer skips tagging for the run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bobandClaude Fable 5.1 committed 2026-09-14 10:20:42 +01:00
1 parent fbd77fc91b
commit 3b3c87e608
2 files changed
+91 -13

No files matched your search

+55 -4
View File
@@ -1119,13 +1119,13 @@ class ImmichClient(TestCase):
self.assertEqual(im.albums("u1"), {"RGL": "al1"})
self.assertEqual(im.createalbum("X"), "al2")
self.assertEqual(im.albumassets("al1"), {"a1", "a2"})
self.assertEqual(im.addtoalbum("al1", ["a3", "a1"]), 1)
self.assertEqual(im.addtoalbum("al1", ["a3", "a1"]), (1, []))
def test_tags(self):
im = self.client(FakeResponse([{"value": "rgl", "id": "t1"}, {"value": "rgl/licence/x", "id": "t2"}]),
FakeResponse([{"success": True}, {"success": False, "error": "duplicate"}]))
self.assertEqual(im.tagids(["rgl", "rgl/licence/x"]), {"rgl": "t1", "rgl/licence/x": "t2"})
self.assertEqual(im.tagassets("t1", ["a1", "a2"]), (1, 1))
self.assertEqual(im.tagassets("t1", ["a1", "a2"]), (1, 1, []))
def test_tagids_skips_malformed_entries(self):
im = self.client(FakeResponse([{"value": "rgl", "id": "t1"}, {"value": "rgl/broken"}]))
@@ -1136,10 +1136,30 @@ class ImmichClient(TestCase):
im = self.client(FakeResponse([{"success": True}] * 500),
FakeResponse([{"success": True}] * 500),
FakeResponse([{"success": True}] * 200))
self.assertEqual(im.addtoalbum("al", ids), 1200)
self.assertEqual(im.addtoalbum("al", ids), (1200, []))
self.assertEqual(len(rb.OPEN.requests), 3)
class ImmichRefusals(TestCase):
def client(self, *responses):
rb.OPEN = FakeOpen(*responses)
return rb.Immich("https://immich.example/api", "KEY")
def test_album_refusals_are_returned_not_dropped(self):
im = self.client(FakeResponse([{"id": "a1", "success": False, "error": "no_permission"},
{"id": "a2", "success": False, "error": "duplicate"},
{"id": "a3", "success": True}]))
self.assertEqual(im.addtoalbum("al1", ["a1", "a2", "a3"]),
(1, [("a1", "no_permission")]))
def test_tag_refusals_are_returned_not_dropped(self):
im = self.client(FakeResponse([{"id": "a1", "success": False, "error": "not_found"},
{"id": "a2", "success": False, "error": "duplicate"},
{"id": "a3", "success": True}]))
self.assertEqual(im.tagassets("t1", ["a1", "a2", "a3"]),
(1, 1, [("a1", "not_found")]))
class Hashing(TestCase):
def test_checksum_is_base64_sha1(self):
path = self.write("f", b"hello")
@@ -1401,12 +1421,43 @@ class SyncPhase(TestCase):
FakeResponse({"assets": {"items": [], "nextPage": None}}),
FakeResponse({"id": "a1", "status": "created"}, status=201), # upload 111
http_error(500), # albums
FakeResponse([{"value": "rgl", "id": "t1"},
{"value": "rgl/licence/cc-by-sa-2.0", "id": "t2"},
{"value": "rgl/photographer/P", "id": "t3"}]), # tags still run
FakeResponse([{"success": True}]), FakeResponse([{"success": True}]),
FakeResponse([{"success": True}]),
FakeResponse({"id": "a1", "exifInfo": {}}), # re-read a1
FakeResponse(b"")) # update a1
failures = rb.syncphase(self.conf(limit=1), rb.Immich("https://i/api", "k"))
self.assertEqual(len(failures), 1)
reason = failures.items[0][2]
key, names, reason = failures.items[0]
self.assertEqual(key, "album")
self.assertTrue("albums" in reason or "HTTP 500" in reason)
self.assertEqual(rb.OPEN.requests[4].full_url, "https://i/api/tags")
def test_album_refusal_is_recorded_against_the_photo(self):
rb.OPEN = FakeOpen(
FakeResponse({"id": "u1"}),
FakeResponse({"assets": {"items": [], "nextPage": None}}),
FakeResponse({"id": "a1", "status": "created"}, status=201), # upload 111
FakeResponse([{"albumName": "RGL", "id": "al1", "ownerId": "u1"}]),
FakeResponse({"assets": []}),
FakeResponse([{"id": "a1", "success": False, "error": "no_permission"}]),
FakeResponse([{"value": "rgl", "id": "t1"},
{"value": "rgl/licence/cc-by-sa-2.0", "id": "t2"},
{"value": "rgl/photographer/P", "id": "t3"}]),
FakeResponse([{"id": "a1", "success": False, "error": "no_permission"}]),
FakeResponse([{"success": True}]), FakeResponse([{"success": True}]),
FakeResponse({"id": "a1", "exifInfo": {}}),
FakeResponse(b""))
failures = rb.syncphase(self.conf(limit=1), rb.Immich("https://i/api", "k"))
self.assertEqual(len(failures), 2)
(k1, names1, r1), (k2, names2, r2) = failures.items
self.assertEqual((k1, names1), ("111", ["A, E1"]))
self.assertIn("album RGL: no_permission", r1)
self.assertEqual((k2, names2), ("111", ["A, E1"]))
self.assertIn("tag rgl: no_permission", r2)
self.assertIn("no_permission", self.stderr.getvalue())
class Run(TestCase):