Build the immich description block and tags from a sidecar

Immich has no copyright or source fields, so the photographer, licence,
flickr page and every wiki page using the photo go into a marked block in
the description. The marker lets a later run find and replace only that
block, leaving anything written by hand alone. Tags mirror licence and
photographer for filtering.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bobandClaude Fable 5.1 committed 2026-09-10 12:07:45 +01:00
1 parent f7884b943e
commit 2774267425
2 files changed
+259

No files matched your search

+135
View File
@@ -601,3 +601,138 @@ class Sidecars(TestCase):
def test_utcnow_shape(self):
self.assertRegex(rb.utcnow(), r"^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ$")
class Licences(TestCase):
def test_label_from_url(self):
self.assertEqual(rb.licencelabel("https://creativecommons.org/licenses/by-nc-sa/2.0/deed.en_GB"), "CC BY-NC-SA 2.0")
self.assertEqual(rb.licencelabel("https://creativecommons.org/licenses/by/2.0/"), "CC BY 2.0")
self.assertEqual(rb.licencelabel("http://creativecommons.org/publicdomain/zero/1.0/"), "CC0 1.0")
self.assertIsNone(rb.licencelabel(""))
self.assertIsNone(rb.licencelabel("https://example.com/terms"))
def test_slug(self):
self.assertEqual(rb.licenceslug("CC BY-NC-SA 2.0"), "cc-by-nc-sa-2.0")
self.assertEqual(rb.licenceslug("All rights reserved"), "all-rights-reserved")
def test_rgl_licence_wins(self):
side = sidecar()
self.assertEqual(rb.licencefor(side),
("CC BY-SA 2.0", "https://creativecommons.org/licenses/by-sa/2.0/", "rgl"))
def test_flickr_fills_a_gap(self):
side = sidecar(licence="")
label, url, origin = rb.licencefor(side)
self.assertEqual((label, origin), ("CC BY-SA 2.0", "flickr"))
self.assertIn("by-sa/2.0", url)
def test_a_licence_url_we_cannot_name_beats_flickr(self):
side = sidecar(licence="https://example.com/our-own-terms")
self.assertEqual(rb.licencefor(side),
(None, "https://example.com/our-own-terms", "wiki"))
self.assertIn("Licence: https://example.com/our-own-terms (from the wiki)",
rb.describeblock(side))
self.assertNotIn("from Flickr", rb.describeblock(side))
self.assertIn("rgl/licence/unknown", rb.tagsfor(side))
def test_unknown_when_neither(self):
side = sidecar(licence="")
side["flickr"]["licence_id"] = None
self.assertEqual(rb.licencefor(side), (None, None, None))
def test_flickr_table_covers_published_ids(self):
self.assertEqual(rb.FLICKR_LICENCES[0][0], "All rights reserved")
self.assertEqual(rb.FLICKR_LICENCES[4][0], "CC BY 2.0")
self.assertEqual(rb.FLICKR_LICENCES[9][0], "CC0 1.0")
self.assertEqual(rb.FLICKR_LICENCES[10][0], "Public Domain Mark 1.0")
class Describing(TestCase):
def test_block_shape(self):
block = rb.describeblock(sidecar())
self.assertEqual(block.splitlines(), [
rb.MARKER,
"Photo: A title",
"Source: https://www.flickr.com/photos/12345678@N00/111/",
"Photographer: P",
"Licence: CC BY-SA 2.0 https://creativecommons.org/licenses/by-sa/2.0/",
"Used on:",
" A, E1 https://london.randomness.org.uk/wiki.cgi?A%2C_E1",
])
def test_photographer_falls_back_to_flickr(self):
side = sidecar(copyright="")
self.assertEqual(rb.photographer(side), "Real Name")
side["flickr"]["owner_realname"] = ""
self.assertEqual(rb.photographer(side), "handle")
def test_licence_from_flickr_is_marked_and_disagreement_noted(self):
side = sidecar(licence="")
self.assertIn("Licence: CC BY-SA 2.0 https://creativecommons.org/licenses/by-sa/2.0/ (from Flickr)",
rb.describeblock(side))
side = sidecar(licence="https://creativecommons.org/licenses/by-nc-sa/2.0/")
block = rb.describeblock(side)
self.assertIn("Licence: CC BY-NC-SA 2.0 https://creativecommons.org/licenses/by-nc-sa/2.0/", block)
self.assertIn("Note: Flickr lists this photo as CC BY-SA 2.0", block)
def test_no_licence_says_unknown(self):
side = sidecar(licence="")
side["flickr"]["licence_id"] = None
self.assertIn("Licence: unknown", rb.describeblock(side))
def test_non_flickr_photo(self):
side = sidecar(flickr=False)
block = rb.describeblock(side)
self.assertNotIn("Photo:", block)
self.assertIn("Source: https://www.geograph.org.uk/photos/1.jpg", block)
def test_several_pages(self):
side = sidecar(nodes=[
{"param": "A", "name": "A", "url": "https://x/A", "lat": "", "long": ""},
{"param": "B", "name": "B", "url": "https://x/B", "lat": "", "long": ""}])
self.assertTrue(rb.describeblock(side).endswith("Used on:\n A https://x/A\n B https://x/B"))
class Merging(TestCase):
block = "%s\nPhoto: x\nUsed on:\n A https://x/A" % rb.MARKER
def test_empty_or_boilerplate_is_written(self):
self.assertEqual(rb.mergedescription("", self.block), (self.block, "write"))
self.assertEqual(rb.mergedescription("OLYMPUS DIGITAL CAMERA", self.block), (self.block, "write"))
def test_existing_text_is_kept_above(self):
text, action = rb.mergedescription("My caption", self.block)
self.assertEqual(action, "append")
self.assertEqual(text, "My caption\n\n" + self.block)
def test_same_block_is_quiet(self):
held = "My caption\n\n" + self.block
self.assertEqual(rb.mergedescription(held, self.block), (held, "same"))
def test_changed_block_is_replaced_in_place(self):
held = "My caption\n\n" + self.block + "\n\nTrailing note"
newblock = self.block + "\n B https://x/B"
text, action = rb.mergedescription(held, newblock)
self.assertEqual(action, "replace")
self.assertEqual(text, "My caption\n\n" + newblock + "\n\nTrailing note")
def test_isboilerplate(self):
self.assertTrue(rb.isboilerplate("SONY DSC"))
self.assertFalse(rb.isboilerplate("The 100 Club"))
class Tags(TestCase):
def test_tags(self):
self.assertEqual(rb.tagsfor(sidecar()),
["rgl", "rgl/licence/cc-by-sa-2.0", "rgl/photographer/P"])
def test_unknown_licence_and_blank_photographer(self):
side = sidecar(licence="", copyright="")
side["flickr"]["licence_id"] = None
side["flickr"]["owner_realname"] = ""
side["flickr"]["owner_name"] = ""
self.assertEqual(rb.tagsfor(side), ["rgl", "rgl/licence/unknown"])
def test_photographer_whitespace_and_slashes(self):
side = sidecar(copyright=" Some / One ")
self.assertIn("rgl/photographer/Some - One", rb.tagsfor(side))