diff --git a/_art/2024-10-09-x_DSC-0999.md b/_art/2024-10-09-x_DSC-0999.md index 874ab5c..01d413f 100644 --- a/_art/2024-10-09-x_DSC-0999.md +++ b/_art/2024-10-09-x_DSC-0999.md @@ -3,8 +3,8 @@ layout: post title: x_DSC-0999.jpg date: 2024-10-09 categories: art +anchor: "Kazakhstan - World Nomad Games" --- ![x](/img/arts/kazakhstan-2024/x_DSC-0999.jpg) - diff --git a/_art/2024-10-25-Taq-e-Bostan_000521270020.md b/_art/2024-10-25-Taq-e-Bostan_000521270020.md index 94e18bd..750741c 100644 --- a/_art/2024-10-25-Taq-e-Bostan_000521270020.md +++ b/_art/2024-10-25-Taq-e-Bostan_000521270020.md @@ -3,6 +3,7 @@ layout: post title: Taq-e-Bostan_000521270020.jpg date: 2024-10-25 categories: art +anchor: "Iran - Hamadan, Kermanshah, Tabriz" --- ![Taq e Bostan](/img/arts/iran-analog-2024/Taq-e-Bostan_000521270020.jpg) diff --git a/_art/2026-09-19-photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db.md b/_art/2026-09-19-photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db-00.md similarity index 89% rename from _art/2026-09-19-photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db.md rename to _art/2026-09-19-photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db-00.md index 30ee8ad..c2abb14 100644 --- a/_art/2026-09-19-photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db.md +++ b/_art/2026-09-19-photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db-00.md @@ -1,7 +1,7 @@ --- layout: post title: "photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db" -date: "2026-09-19T12:10:40.964254+00:00" +date: "2026-09-19T12:29:32.999986+00:00" categories: art --- diff --git a/_art/2026-09-19-photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c.md b/_art/2026-09-19-photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c-00.md similarity index 89% rename from _art/2026-09-19-photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c.md rename to _art/2026-09-19-photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c-00.md index ae50f4a..ec5c4f3 100644 --- a/_art/2026-09-19-photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c.md +++ b/_art/2026-09-19-photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c-00.md @@ -1,7 +1,7 @@ --- layout: post title: "photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c" -date: "2026-09-19T12:29:33.758101+00:00" +date: "2026-09-19T12:29:32.999994+00:00" categories: art --- diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-00.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-00.md new file mode 100644 index 0000000..2da03d0 --- /dev/null +++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-00.md @@ -0,0 +1,9 @@ +--- +layout: post +title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-00" +date: "2026-09-19T12:29:32.999993+00:00" +categories: art +anchor: "Romania" +--- + +

diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-01.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-01.md new file mode 100644 index 0000000..c92ec89 --- /dev/null +++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-01.md @@ -0,0 +1,8 @@ +--- +layout: post +title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-01" +date: "2026-09-19T12:29:32.999992+00:00" +categories: art +--- + +

diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-02.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-02.md new file mode 100644 index 0000000..50d2481 --- /dev/null +++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-02.md @@ -0,0 +1,8 @@ +--- +layout: post +title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-02" +date: "2026-09-19T12:29:32.999984+00:00" +categories: art +--- + +

diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-03.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-03.md new file mode 100644 index 0000000..50817d0 --- /dev/null +++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-03.md @@ -0,0 +1,8 @@ +--- +layout: post +title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-03" +date: "2026-09-19T12:29:32.999983+00:00" +categories: art +--- + +

diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-04.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-04.md new file mode 100644 index 0000000..5727132 --- /dev/null +++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-04.md @@ -0,0 +1,8 @@ +--- +layout: post +title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-04" +date: "2026-09-19T12:29:32.999995+00:00" +categories: art +--- + +

diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-05.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-05.md new file mode 100644 index 0000000..0eaf6a9 --- /dev/null +++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-05.md @@ -0,0 +1,8 @@ +--- +layout: post +title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-05" +date: "2026-09-19T12:29:32.999996+00:00" +categories: art +--- + +

diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-06.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-06.md new file mode 100644 index 0000000..67c1fd9 --- /dev/null +++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-06.md @@ -0,0 +1,8 @@ +--- +layout: post +title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-06" +date: "2026-09-19T12:29:32.999997+00:00" +categories: art +--- + +

diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-07.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-07.md new file mode 100644 index 0000000..4dee853 --- /dev/null +++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-07.md @@ -0,0 +1,9 @@ +--- +layout: post +title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-07" +date: "2026-09-19T12:29:32.999998+00:00" +categories: art +--- + +

Transmission of Flesh by Erfan Ashourioun

+Transmission of Flesh by Erfan Ashourioun diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-08.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-08.md new file mode 100644 index 0000000..ffe7201 --- /dev/null +++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-08.md @@ -0,0 +1,8 @@ +--- +layout: post +title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-08" +date: "2026-09-19T12:29:32.999999+00:00" +categories: art +--- + +

diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-09.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-09.md new file mode 100644 index 0000000..d869d87 --- /dev/null +++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-09.md @@ -0,0 +1,9 @@ +--- +layout: post +title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d" +date: "2026-09-19T12:29:33+00:00" +categories: art +anchor: "Armenia" +--- + +

diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d.md deleted file mode 100644 index 531aeeb..0000000 --- a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -layout: post -title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d" -date: "2026-09-19T12:27:27.038898+00:00" -categories: art ---- - -
- -

-

-

-

-

-

-

-

-

-

diff --git a/_art/2026-09-19-photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673.md b/_art/2026-09-19-photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673-00.md similarity index 89% rename from _art/2026-09-19-photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673.md rename to _art/2026-09-19-photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673-00.md index f8ed6b3..c38d5bf 100644 --- a/_art/2026-09-19-photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673.md +++ b/_art/2026-09-19-photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673-00.md @@ -1,7 +1,7 @@ --- layout: post title: "photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673" -date: "2026-09-19T12:03:11.135424+00:00" +date: "2026-09-19T12:29:32.999985+00:00" categories: art --- diff --git a/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-00.md b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-00.md new file mode 100644 index 0000000..0cc3f80 --- /dev/null +++ b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-00.md @@ -0,0 +1,10 @@ +--- +layout: post +title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b" +date: "2026-09-19T12:29:32.999991+00:00" +categories: art +--- + +
+ +

diff --git a/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-01.md b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-01.md new file mode 100644 index 0000000..c55b96f --- /dev/null +++ b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-01.md @@ -0,0 +1,8 @@ +--- +layout: post +title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-01" +date: "2026-09-19T12:29:32.999990+00:00" +categories: art +--- + +

diff --git a/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-02.md b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-02.md new file mode 100644 index 0000000..3f578c6 --- /dev/null +++ b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-02.md @@ -0,0 +1,8 @@ +--- +layout: post +title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-02" +date: "2026-09-19T12:29:32.999989+00:00" +categories: art +--- + +

diff --git a/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-03.md b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-03.md new file mode 100644 index 0000000..ac93ea0 --- /dev/null +++ b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-03.md @@ -0,0 +1,8 @@ +--- +layout: post +title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-03" +date: "2026-09-19T12:29:32.999988+00:00" +categories: art +--- + +

diff --git a/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-04.md b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-04.md new file mode 100644 index 0000000..e4e0071 --- /dev/null +++ b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-04.md @@ -0,0 +1,8 @@ +--- +layout: post +title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-04" +date: "2026-09-19T12:29:32.999987+00:00" +categories: art +--- + +

diff --git a/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b.md b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b.md deleted file mode 100644 index 4afb7f6..0000000 --- a/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -layout: post -title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b" -date: "2026-09-19T12:26:31.454056+00:00" -categories: art ---- - -
- -

-

-

-

-

diff --git a/photo-upload/README.md b/photo-upload/README.md index 678510a..1ccae1f 100644 --- a/photo-upload/README.md +++ b/photo-upload/README.md @@ -150,10 +150,13 @@ Create **Publish to blog**: only `published` means the deployed site's receipt contains this album. The ZIP route accepts 1–20 images in archive-entry order and keeps them as one -upload. Each photo renders as its own gallery `
  • `, including photos in older -batches. The caption appears on each photo; the first image is the highlight target. Metadata sidecars -under `__MACOSX/` and `.DS_Store` are ignored. Ordinary API clients can also send -repeated `photos` parts; send either `archive` or `photos`, never both. +upload job. Each photo is stored as its own `_art` document and renders as its +own gallery `
  • `. The shared caption is repeated on each photo and an optional +highlight begins on the first photo. Photos remain contiguous in the selection order. +Older batch documents are still split at render time for backward compatibility. +Metadata sidecars under `__MACOSX/` and `.DS_Store` are ignored. Ordinary API +clients can also send repeated `photos` parts; send either `archive` or `photos`, +never both. Select still photos; videos/Live Photo video components aren't supported. Dates are upload time in UTC; captions are plain text. A caption is optional. An optional highlight name (up to 200 characters) adds one link to **Highlights**, diff --git a/photo-upload/app.py b/photo-upload/app.py index ba2f3ff..67d6c6b 100644 --- a/photo-upload/app.py +++ b/photo-upload/app.py @@ -10,7 +10,7 @@ from pathlib import Path import re import shutil import tempfile -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone import warnings import zipfile @@ -79,6 +79,26 @@ def web_image(raw, destination): Image.Resampling.LANCZOS) +def photo_entry(job, date, index, count, caption, highlight): + # Give each photo its own collection document. Descending dates preserve the + # selection order because the gallery renders the newest document first. + item_date = datetime.fromisoformat(date) + timedelta(microseconds=count - index) + title = f"photo-{job}" if index == 0 else f"photo-{job}-{index:02}" + content = ["---", "layout: post", f'title: "{title}"', + f'date: "{item_date.isoformat()}"', "categories: art"] + if index == 0 and highlight: + content.append(f"anchor: {json.dumps(highlight)}") + content.extend(["---", ""]) + if index == 0 and not highlight: + content.extend([f'
    ', ""]) + content.append(f'

    ') + if caption: + content.append('' + + public_text(caption).replace("\n", "
    ") + "
    ") + return "\n".join(content) + "\n" + + def create_app(data_dir=None, token=None): app = Flask(__name__) app.config.update(MAX_CONTENT_LENGTH=100 * 1024 * 1024, @@ -261,21 +281,15 @@ def create_app(data_dir=None, token=None): if kind == "snippet": manifest.update(kind=kind, text=text) atomic_json(temporary / "manifest.json", manifest) - content = ["---", "layout: post", f'title: "photo-{job}"', - f'date: "{date}"', "categories: art"] - if highlight: - content.append(f"anchor: {json.dumps(highlight)}") - content.extend(["---", ""]) - # The gallery already renders an anchor before the first photo for highlights. - if not highlight: - content.extend([f'
    ', ""]) - for index in range(len(photos)): - content.append(f'

    ') - if caption: - content.append('' + - public_text(caption).replace("\n", "
    ") + "
    ") - if kind == "snippet": + if kind == "photo": + (temporary / "entries").mkdir() + for index in range(len(photos)): + (temporary / "entries" / f"{index:02}.md").write_text( + photo_entry(job, date, index, len(photos), caption, highlight)) + # Retain the old filename for compatibility with publisher versions + # that predate one-document-per-photo submissions. + content = [(temporary / "entries/00.md").read_text()] + else: content = [f'
    ', ""] for paragraph in (text, caption): if paragraph: diff --git a/photo-upload/publisher.py b/photo-upload/publisher.py index 3446570..22a14f0 100644 --- a/photo-upload/publisher.py +++ b/photo-upload/publisher.py @@ -86,9 +86,20 @@ class Publisher: entry = Path("_art") / f"{manifest['date'][:10]}-photo-{job}.md" images = Path("img/arts/uploads") / job (self.repo / entry).parent.mkdir(parents=True, exist_ok=True) - shutil.copyfile(submission / "entry.md", self.repo / entry) + entries = submission / "entries" + if entries.is_dir(): + # Remove the former batch document when a queued submission + # is republished by a newer service version. + self.git("rm", "--ignore-unmatch", "--", str(entry)) + for source in sorted(entries.glob("*.md")): + destination = Path("_art") / f"{manifest['date'][:10]}-photo-{job}-{source.stem}.md" + shutil.copyfile(source, self.repo / destination) + self.git("add", "--", str(destination)) + else: + shutil.copyfile(submission / "entry.md", self.repo / entry) + self.git("add", "--", str(entry)) shutil.copytree(submission / "images", self.repo / images, dirs_exist_ok=True) - self.git("add", "--", str(entry), str(images)) + self.git("add", "--", str(images)) if self.git("diff", "--cached", "--name-only"): self.git("commit", "-m", f"Publish {len(jobs)} submission(s)") self.git("push", "origin", f"HEAD:refs/heads/{self.branch}") diff --git a/photo-upload/tests/test_upload.py b/photo-upload/tests/test_upload.py index 93cae9a..c86a4b4 100644 --- a/photo-upload/tests/test_upload.py +++ b/photo-upload/tests/test_upload.py @@ -116,10 +116,12 @@ def test_album_caption_order_and_retry_deduplication(service): assert response.status_code == 202 job = response.json["id"] folder = data / "submissions" / job - entry = (folder / "entry.md").read_text() - assert "