Support analog photo dates and correct gallery years

This commit is contained in:
2026-09-20 01:39:42 +03:30
parent dad62df66f
commit cf46d62ede
55 changed files with 183 additions and 75 deletions
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-18a628ae4ea72d0f9a68de6acfbc698449fe10e49ecf990a8ddced0b2e32949d" title: "photo-18a628ae4ea72d0f9a68de6acfbc698449fe10e49ecf990a8ddced0b2e32949d"
date: "2026-09-19T21:41:03.801419+00:00" date: "2025-09-19T21:42:30+00:00"
categories: art categories: art
anchor: "Geneva" anchor: "Geneva"
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-18a628ae4ea72d0f9a68de6acfbc698449fe10e49ecf990a8ddced0b2e32949d-01" title: "photo-18a628ae4ea72d0f9a68de6acfbc698449fe10e49ecf990a8ddced0b2e32949d-01"
date: "2026-09-19T21:41:02.801419+00:00" date: "2025-09-19T21:42:29+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-18a628ae4ea72d0f9a68de6acfbc698449fe10e49ecf990a8ddced0b2e32949d-02" title: "photo-18a628ae4ea72d0f9a68de6acfbc698449fe10e49ecf990a8ddced0b2e32949d-02"
date: "2026-09-19T21:41:01.801419+00:00" date: "2025-09-19T21:42:28+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-1bb19ce5b6a5570ddda1754bb77583dbfb2b4c534ee88e85a69f511ab47b1a14" title: "photo-1bb19ce5b6a5570ddda1754bb77583dbfb2b4c534ee88e85a69f511ab47b1a14"
date: "2026-09-19T21:29:48.036428+00:00" date: "2026-09-19T12:20:30+00:00"
categories: art categories: art
anchor: "Foto 64" anchor: "Foto 64"
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-1bb19ce5b6a5570ddda1754bb77583dbfb2b4c534ee88e85a69f511ab47b1a14-01" title: "photo-1bb19ce5b6a5570ddda1754bb77583dbfb2b4c534ee88e85a69f511ab47b1a14-01"
date: "2026-09-19T21:29:47.036428+00:00" date: "2026-09-19T12:20:29+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-1bb19ce5b6a5570ddda1754bb77583dbfb2b4c534ee88e85a69f511ab47b1a14-02" title: "photo-1bb19ce5b6a5570ddda1754bb77583dbfb2b4c534ee88e85a69f511ab47b1a14-02"
date: "2026-09-19T21:29:46.036428+00:00" date: "2026-09-19T12:20:28+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-1bb19ce5b6a5570ddda1754bb77583dbfb2b4c534ee88e85a69f511ab47b1a14-03" title: "photo-1bb19ce5b6a5570ddda1754bb77583dbfb2b4c534ee88e85a69f511ab47b1a14-03"
date: "2026-09-19T21:29:45.036428+00:00" date: "2026-09-19T12:20:27+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db" title: "photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db"
date: "2026-09-19T12:29:19+00:00" date: "2026-09-19T12:40:26+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c" title: "photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c"
date: "2026-09-19T12:29:27+00:00" date: "2026-09-19T12:40:18+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-00" title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d"
date: "2026-09-19T12:29:26+00:00" date: "2026-09-19T12:40:33+00:00"
categories: art categories: art
anchor: "Romania" anchor: "Romania"
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-01" title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-01"
date: "2026-09-19T12:29:25+00:00" date: "2026-09-19T12:40:32+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-02" title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-02"
date: "2026-09-19T12:29:17+00:00" date: "2026-09-19T12:40:17+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-03" title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-03"
date: "2026-09-19T12:29:16+00:00" date: "2026-09-19T12:40:16+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-04" title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-04"
date: "2026-09-19T12:29:28+00:00" date: "2026-09-19T12:40:19+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-05" title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-05"
date: "2026-09-19T12:29:29+00:00" date: "2026-09-19T12:40:20+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-06" title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-06"
date: "2026-09-19T12:29:30+00:00" date: "2026-09-19T12:40:21+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-07" title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-07"
date: "2026-09-19T12:29:31+00:00" date: "2026-09-19T12:40:22+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-08" title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-08"
date: "2026-09-19T12:29:32+00:00" date: "2026-09-19T12:40:23+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d" title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-09"
date: "2026-09-19T12:29:33+00:00" date: "2026-09-19T12:40:24+00:00"
categories: art categories: art
anchor: "Armenia" anchor: "Armenia"
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e" title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e"
date: "2026-09-19T21:41:17+00:00" date: "2025-09-19T21:41:17+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-01" title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-01"
date: "2026-09-19T21:41:16+00:00" date: "2025-09-19T21:41:16+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-02" title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-02"
date: "2026-09-19T21:41:15+00:00" date: "2025-09-19T21:41:15+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-03" title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-03"
date: "2026-09-19T21:41:14+00:00" date: "2025-09-19T21:41:14+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-04" title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-04"
date: "2026-09-19T21:41:13+00:00" date: "2025-09-19T21:41:13+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-05" title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-05"
date: "2026-09-19T21:41:12+00:00" date: "2025-09-19T21:41:12+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-06" title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-06"
date: "2026-09-19T21:41:11+00:00" date: "2025-09-19T21:41:11+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-07" title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-07"
date: "2026-09-19T21:41:10+00:00" date: "2025-09-19T21:41:10+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-08" title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-08"
date: "2026-09-19T21:41:09+00:00" date: "2025-09-19T21:41:09+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-09" title: "photo-85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e-09"
date: "2026-09-19T21:41:08+00:00" date: "2025-09-19T21:41:08+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673" title: "photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673"
date: "2026-09-19T12:29:18+00:00" date: "2026-09-19T12:40:25+00:00"
categories: art categories: art
--- ---
@@ -1,9 +1,9 @@
--- ---
layout: post layout: post
title: "photo-bf23ab31ef7b33150e9e99cb31f14b1bc27bc8143293a32bb1d4ac2193a493c6" title: "photo-bf23ab31ef7b33150e9e99cb31f14b1bc27bc8143293a32bb1d4ac2193a493c6"
date: "2026-09-19T21:43:42.048113+00:00" date: "2025-09-19T21:43:42+00:00"
categories: art categories: art
anchor: "Iranian Martyrs\u2019 Memorial Cemetery" anchor: "Iranian Martyrs’ Memorial Cemetery"
--- ---
<p><img src="/img/arts/uploads/bf23ab31ef7b33150e9e99cb31f14b1bc27bc8143293a32bb1d4ac2193a493c6/00.jpg" alt="Martyrs" loading="lazy"></p> <p><img src="/img/arts/uploads/bf23ab31ef7b33150e9e99cb31f14b1bc27bc8143293a32bb1d4ac2193a493c6/00.jpg" alt="Martyrs" loading="lazy"></p>
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-bf23ab31ef7b33150e9e99cb31f14b1bc27bc8143293a32bb1d4ac2193a493c6-01" title: "photo-bf23ab31ef7b33150e9e99cb31f14b1bc27bc8143293a32bb1d4ac2193a493c6-01"
date: "2026-09-19T21:43:41.048113+00:00" date: "2025-09-19T21:43:41+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b" title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b"
date: "2026-09-19T12:29:24+00:00" date: "2026-09-19T12:40:31+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-01" title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-01"
date: "2026-09-19T12:29:23+00:00" date: "2026-09-19T12:40:30+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-02" title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-02"
date: "2026-09-19T12:29:22+00:00" date: "2026-09-19T12:40:29+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-03" title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-03"
date: "2026-09-19T12:29:21+00:00" date: "2026-09-19T12:40:28+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-04" title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-04"
date: "2026-09-19T12:29:20+00:00" date: "2026-09-19T12:40:27+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f" title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f"
date: "2026-09-19T21:41:30+00:00" date: "2025-09-19T21:41:30+00:00"
categories: art categories: art
anchor: "Paris & Lille" anchor: "Paris & Lille"
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-01" title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-01"
date: "2026-09-19T21:41:29+00:00" date: "2025-09-19T21:41:29+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-02" title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-02"
date: "2026-09-19T21:41:28+00:00" date: "2025-09-19T21:41:28+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-03" title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-03"
date: "2026-09-19T21:41:27+00:00" date: "2025-09-19T21:41:27+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-04" title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-04"
date: "2026-09-19T21:41:26+00:00" date: "2025-09-19T21:41:26+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-05" title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-05"
date: "2026-09-19T21:41:25+00:00" date: "2025-09-19T21:41:25+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-06" title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-06"
date: "2026-09-19T21:41:24+00:00" date: "2025-09-19T21:41:24+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-07" title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-07"
date: "2026-09-19T21:41:23+00:00" date: "2025-09-19T21:41:23+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-08" title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-08"
date: "2026-09-19T21:41:22+00:00" date: "2025-09-19T21:41:22+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-09" title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-09"
date: "2026-09-19T21:41:21+00:00" date: "2025-09-19T21:41:21+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-10" title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-10"
date: "2026-09-19T21:41:20+00:00" date: "2025-09-19T21:41:20+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-11" title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-11"
date: "2026-09-19T21:41:19+00:00" date: "2025-09-19T21:41:19+00:00"
categories: art categories: art
--- ---
@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-12" title: "photo-e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f-12"
date: "2026-09-19T21:41:18+00:00" date: "2025-09-19T21:41:18+00:00"
categories: art categories: art
--- ---
+10 -4
View File
@@ -136,11 +136,14 @@ Create **Publish to blog**:
**ISO 8601**. After **End Repeat**, **Combine Text** from **Repeat Results** **ISO 8601**. After **End Repeat**, **Combine Text** from **Repeat Results**
with **New Lines** and save it as **Photo Dates**. This preserves capture with **New Lines** and save it as **Photo Dates**. This preserves capture
dates even when Shortcuts strips them from the exported JPEG data. dates even when Shortcuts strips them from the exported JPEG data.
5. **Make Archive** from **Shortcut Input**, format **ZIP**. Keep this output in 5. Add **Ask for Input**, type **Text**, with the prompt **Date Taken (optional;
YYYY, YYYY-MM-DD, or ISO timestamp)**. Save its response as **Date Taken**.
This is useful for analog scans; leave it blank for digital photos.
6. **Make Archive** from **Shortcut Input**, format **ZIP**. Keep this output in
a variable named **Photo Archive**. This packages every selected image into a variable named **Photo Archive**. This packages every selected image into
one upload; using an image list directly in a File form field can send only one upload; using an image list directly in a File form field can send only
the first image on some Shortcuts versions. the first image on some Shortcuts versions.
6. **Get Contents of URL**: `https://theread.me/api/photos`. 7. **Get Contents of URL**: `https://theread.me/api/photos`.
- Method: **POST**. - Method: **POST**.
- Header: `Authorization` = `Bearer YOUR_TOKEN`. - Header: `Authorization` = `Bearer YOUR_TOKEN`.
- Request Body: **Form**. - Request Body: **Form**.
@@ -149,8 +152,9 @@ Create **Publish to blog**:
- `highlight`: type **Text**, value the highlight-name response. Select each - `highlight`: type **Text**, value the highlight-name response. Select each
prompt's specific output variable so the caption and highlight stay separate. prompt's specific output variable so the caption and highlight stay separate.
- `photo_dates`: type **Text**, value **Photo Dates**. - `photo_dates`: type **Text**, value **Photo Dates**.
- `date_taken`: type **Text**, value **Date Taken**.
- Do not set Content-Type; Shortcuts supplies the multipart boundary. - Do not set Content-Type; Shortcuts supplies the multipart boundary.
7. Show the actual response and check its `count` against the number of selected 8. Show the actual response and check its `count` against the number of selected
images. Only show “Uploaded; publishing” when there is no `error`. Optionally poll the returned `status_url` images. Only show “Uploaded; publishing” when there is no `error`. Optionally poll the returned `status_url`
with the same header. `pushed` means “GitHub Actions is building/deploying”; with the same header. `pushed` means “GitHub Actions is building/deploying”;
only `published` means the deployed site's receipt contains this album. only `published` means the deployed site's receipt contains this album.
@@ -167,7 +171,9 @@ Select still photos; videos/Live Photo video components aren't supported. The
service reads EXIF `DateTimeOriginal` automatically when it is present. Because service reads EXIF `DateTimeOriginal` automatically when it is present. Because
iOS Shortcuts can blank that EXIF field while creating the ZIP, `photo_dates` iOS Shortcuts can blank that EXIF field while creating the ZIP, `photo_dates`
supplies the same Photos-library metadata explicitly, one ISO 8601 value per supplies the same Photos-library metadata explicitly, one ISO 8601 value per
image in archive order. An upload without either source is rejected instead of image in archive order. `date_taken` overrides both sources for the whole batch;
it accepts a year, calendar date, or ISO timestamp and is intended for analog
photos. An upload without any date source is rejected instead of
being placed under the upload year. Re-sharing the same images, caption, and being placed under the upload year. Re-sharing the same images, caption, and
highlight with dates replaces the earlier undated upload. Captions are plain highlight with dates replaces the earlier undated upload. Captions are plain
text and optional. text and optional.
+29 -7
View File
@@ -6,6 +6,7 @@ import html
import io import io
import json import json
import os import os
from collections import Counter
from pathlib import Path from pathlib import Path
import re import re
import shutil import shutil
@@ -79,14 +80,30 @@ def submitted_photo_dates(raw, count):
return dates return dates
def submitted_batch_date(raw):
"""Parse one optional date override for an entire analog-photo batch."""
value = raw.strip()
if not value:
return None
try:
if re.fullmatch(r"\d{4}", value):
return datetime(int(value), 1, 1)
return datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError as error:
raise ValueError("date_taken must be a year, ISO date, or ISO timestamp") from error
def distinct_photo_dates(dates): def distinct_photo_dates(dates):
"""Keep exact capture times, using earlier seconds only to break true ties.""" """Keep exact capture times, using later seconds only to break true ties."""
result = [] result = []
used = set() used = set()
remaining = Counter(taken.isoformat() for taken in dates)
for taken in dates: for taken in dates:
candidate = taken key = taken.isoformat()
candidate = taken + timedelta(seconds=remaining[key] - 1)
remaining[key] -= 1
while candidate.isoformat() in used: while candidate.isoformat() in used:
candidate -= timedelta(seconds=1) candidate += timedelta(seconds=1)
result.append(candidate) result.append(candidate)
used.add(candidate.isoformat()) used.add(candidate.isoformat())
return result return result
@@ -321,6 +338,8 @@ def create_app(data_dir=None, token=None):
try: try:
supplied_dates = (submitted_photo_dates(request.form.get("photo_dates", ""), len(photos)) supplied_dates = (submitted_photo_dates(request.form.get("photo_dates", ""), len(photos))
if kind == "photo" else None) if kind == "photo" else None)
batch_date = (submitted_batch_date(request.form.get("date_taken", ""))
if kind == "photo" else None)
except ValueError as error: except ValueError as error:
abort(400, str(error)) abort(400, str(error))
# Ordered byte hashes + caption make retries idempotent, even across restarts. # Ordered byte hashes + caption make retries idempotent, even across restarts.
@@ -339,7 +358,8 @@ def create_app(data_dir=None, token=None):
try: try:
embedded_date = web_image(raw, temporary / "images" / f"{index:02}.jpg") embedded_date = web_image(raw, temporary / "images" / f"{index:02}.jpg")
if kind == "photo": if kind == "photo":
capture_dates.append(supplied_dates[index] if supplied_dates else embedded_date) capture_dates.append(batch_date or
(supplied_dates[index] if supplied_dates else embedded_date))
except (UnidentifiedImageError, OSError, ValueError, SyntaxError, except (UnidentifiedImageError, OSError, ValueError, SyntaxError,
Image.DecompressionBombError, Image.DecompressionBombWarning, Image.DecompressionBombError, Image.DecompressionBombWarning,
ImageCms.PyCMSError): ImageCms.PyCMSError):
@@ -348,14 +368,16 @@ def create_app(data_dir=None, token=None):
if highlight: if highlight:
digest.update(b"\0highlight\0" + highlight.encode()) digest.update(b"\0highlight\0" + highlight.encode())
undated_job = digest.hexdigest() undated_job = digest.hexdigest()
if supplied_dates: if batch_date:
digest.update(b"\0date_taken\0" + batch_date.isoformat().encode())
elif supplied_dates:
normalized_dates = "\n".join(taken.isoformat() for taken in supplied_dates) normalized_dates = "\n".join(taken.isoformat() for taken in supplied_dates)
digest.update(b"\0photo_dates\0" + normalized_dates.encode()) digest.update(b"\0photo_dates\0" + normalized_dates.encode())
job = digest.hexdigest() job = digest.hexdigest()
date = datetime.now(timezone.utc).isoformat() date = datetime.now(timezone.utc).isoformat()
manifest = {"id": job, "date": date, "caption": caption, manifest = {"id": job, "date": date, "caption": caption,
"highlight": highlight, "count": len(photos)} "highlight": highlight, "count": len(photos)}
if (supplied_dates and undated_job != job and if ((batch_date or supplied_dates) and undated_job != job and
(data / "submissions" / undated_job / "manifest.json").is_file()): (data / "submissions" / undated_job / "manifest.json").is_file()):
manifest["replaces"] = undated_job manifest["replaces"] = undated_job
if kind == "snippet": if kind == "snippet":
@@ -363,7 +385,7 @@ def create_app(data_dir=None, token=None):
atomic_json(temporary / "manifest.json", manifest) atomic_json(temporary / "manifest.json", manifest)
if kind == "photo": if kind == "photo":
if any(taken is None for taken in capture_dates): if any(taken is None for taken in capture_dates):
abort(400, "Photo capture date is missing; send photo_dates from the Photos library") abort(400, "Photo capture date is missing; send date_taken or photo_dates")
capture_dates = distinct_photo_dates(capture_dates) capture_dates = distinct_photo_dates(capture_dates)
manifest["capture_dates"] = [taken.isoformat() for taken in capture_dates] manifest["capture_dates"] = [taken.isoformat() for taken in capture_dates]
atomic_json(temporary / "manifest.json", manifest) atomic_json(temporary / "manifest.json", manifest)
+22
View File
@@ -195,6 +195,28 @@ def test_photo_dates_can_come_from_photos_library_metadata(service):
assert "capture date is missing" in missing.json["error"] assert "capture date is missing" in missing.json["error"]
def test_batch_date_taken_overrides_metadata_for_analog_photos(service):
_, client, data = service
response = client.post("/api/photos", headers=AUTH, data={
"photos": [
(io.BytesIO(photo(exif=Image.Exif())), "first.jpg"),
(io.BytesIO(photo(exif=Image.Exif())), "second.jpg"),
],
"photo_dates": "2026-01-02T03:04:05+00:00\n2026-01-03T03:04:05+00:00",
"date_taken": "2025",
})
assert response.status_code == 202
manifest = json.loads((data / "submissions" / response.json["id"] / "manifest.json").read_text())
assert all(value.startswith("2025-01-01T") for value in manifest["capture_dates"])
invalid = client.post("/api/photos", headers=AUTH, data={
"photos": [(io.BytesIO(photo()), "photo.jpg")],
"date_taken": "last summer",
})
assert invalid.status_code == 400
assert "year, ISO date, or ISO timestamp" in invalid.json["error"]
def test_dated_retry_replaces_the_same_undated_upload(service): def test_dated_retry_replaces_the_same_undated_upload(service):
_, client, data = service _, client, data = service
raw = photo() raw = photo()
+24 -6
View File
@@ -25,7 +25,7 @@ romania = [
(OLD[4], 2), (OLD[4], 3), (OLD[4], 4), (OLD[0], 0), (OLD[3], 0), (OLD[4], 2), (OLD[4], 3), (OLD[4], 4), (OLD[0], 0), (OLD[3], 0),
] ]
ungrouped = [(OLD[2], 2), (OLD[2], 3)] ungrouped = [(OLD[2], 2), (OLD[2], 3)]
ordered = armenia + romania + ungrouped ordered = romania + armenia + ungrouped
assert len(ordered) == len(set(ordered)) == 18 assert len(ordered) == len(set(ordered)) == 18
collection_groups = [ collection_groups = [
@@ -112,13 +112,31 @@ collection_groups = [
paris = "85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e" paris = "85a558a3fcf582e6f0637d6927bb5a074de422f30a389ab9589630b9c163fc0e"
paris_lille = "e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f" paris_lille = "e013523e4dda324d603c89f93ef3e83abbcd82cb11a16f85e53b670e8613d34f"
geneva = "18a628ae4ea72d0f9a68de6acfbc698449fe10e49ecf990a8ddced0b2e32949d"
iranian_memorial = "bf23ab31ef7b33150e9e99cb31f14b1bc27bc8143293a32bb1d4ac2193a493c6"
foto_64 = "1bb19ce5b6a5570ddda1754bb77583dbfb2b4c534ee88e85a69f511ab47b1a14"
uploaded_highlight_groups = [ uploaded_highlight_groups = [
(
"Iranian Martyrs’ Memorial Cemetery",
datetime(2025, 9, 19, 21, 43, 42, tzinfo=timezone.utc),
[(iranian_memorial, index) for index in range(2)],
),
(
"Geneva",
datetime(2025, 9, 19, 21, 42, 30, tzinfo=timezone.utc),
[(geneva, index) for index in range(3)],
),
( (
"Paris & Lille", "Paris & Lille",
datetime(2026, 9, 19, 21, 41, 30, tzinfo=timezone.utc), datetime(2025, 9, 19, 21, 41, 30, tzinfo=timezone.utc),
[(paris_lille, index) for index in range(13)] + [(paris_lille, index) for index in range(13)] +
[(paris, index) for index in range(10)], [(paris, index) for index in range(10)],
), ),
(
"Foto 64",
datetime(2026, 9, 19, 12, 20, 30, tzinfo=timezone.utc),
[(foto_64, index) for index in range(4)],
),
] ]
anchors = {armenia[0]: "Armenia", romania[0]: "Romania"} anchors = {armenia[0]: "Armenia", romania[0]: "Romania"}
@@ -132,7 +150,7 @@ for job in OLD:
if batch: if batch:
batch.unlink() batch.unlink()
start = datetime(2026, 9, 19, 12, 29, 33, tzinfo=timezone.utc) start = datetime(2026, 9, 19, 12, 40, 33, tzinfo=timezone.utc)
for position, (job, index) in enumerate(ordered): for position, (job, index) in enumerate(ordered):
title = f"photo-{job}-{index:02}" title = f"photo-{job}-{index:02}"
if (job, index) == first_for_job[job]: if (job, index) == first_for_job[job]:
@@ -167,7 +185,7 @@ for folder, group_start, anchor, filenames in collection_groups:
content = re.sub(r"^anchor:.*\n", "", content, flags=re.MULTILINE) content = re.sub(r"^anchor:.*\n", "", content, flags=re.MULTILINE)
if position == 0: if position == 0:
content = content.replace("categories: art\n", "categories: art\nanchor: " + content = content.replace("categories: art\n", "categories: art\nanchor: " +
json.dumps(anchor) + "\n", 1) json.dumps(anchor, ensure_ascii=False) + "\n", 1)
path.write_text(content) path.write_text(content)
for anchor, group_start, photos in uploaded_highlight_groups: for anchor, group_start, photos in uploaded_highlight_groups:
@@ -180,8 +198,8 @@ for anchor, group_start, photos in uploaded_highlight_groups:
content = re.sub(r"^anchor:.*\n", "", content, flags=re.MULTILINE) content = re.sub(r"^anchor:.*\n", "", content, flags=re.MULTILINE)
if position == 0: if position == 0:
content = content.replace("categories: art\n", "categories: art\nanchor: " + content = content.replace("categories: art\n", "categories: art\nanchor: " +
json.dumps(anchor) + "\n", 1) json.dumps(anchor, ensure_ascii=False) + "\n", 1)
elif index == 0: elif index == 0 and f'id="photo-{job}"' not in content:
# Removing this batch's old highlight must not remove its published # Removing this batch's old highlight must not remove its published
# receipt target from the rendered gallery. # receipt target from the rendered gallery.
content = content.replace("---\n\n", f'---\n\n<div id="photo-{job}"></div>\n\n', 1) content = content.replace("---\n\n", f'---\n\n<div id="photo-{job}"></div>\n\n', 1)
+45 -5
View File
@@ -82,10 +82,22 @@ class PhotoHighlightTests(unittest.TestCase):
anchor.group(1) if anchor else None, content)) anchor.group(1) if anchor else None, content))
records.sort(reverse=True) records.sort(reverse=True)
grouped = [record for record in records if record[1] in ARMENIA + ROMANIA] grouped = [record for record in records if record[1] in ARMENIA + ROMANIA]
self.assertEqual([record[1] for record in grouped], ARMENIA + ROMANIA) self.assertEqual([record[1] for record in grouped], ROMANIA + ARMENIA)
self.assertEqual(grouped[0][2], "Armenia") self.assertEqual(grouped[0][2], "Romania")
self.assertEqual(grouped[7][2], "Romania") self.assertEqual(grouped[9][2], "Armenia")
self.assertIn("Transmission of Flesh by Erfan Ashourioun", grouped[2][3]) self.assertIn("Transmission of Flesh by Erfan Ashourioun", grouped[11][3])
def test_2026_highlights_follow_supplied_chronology(self):
anchors = {}
for path in ART.glob("*photo-*.md"):
content = path.read_text()
anchor = re.search(r'^anchor: "([^"]+)"', content, re.MULTILINE)
if anchor and anchor.group(1) in {"Foto 64", "Armenia", "Romania"}:
date = datetime.fromisoformat(re.search(
r'^date: "([^"]+)"', content, re.MULTILINE).group(1))
anchors[anchor.group(1)] = date
self.assertLess(anchors["Foto 64"], anchors["Armenia"])
self.assertLess(anchors["Armenia"], anchors["Romania"])
def test_existing_collection_highlight_starts(self): def test_existing_collection_highlight_starts(self):
for anchor, filenames in COLLECTIONS: for anchor, filenames in COLLECTIONS:
@@ -123,8 +135,36 @@ class PhotoHighlightTests(unittest.TestCase):
[(PARIS_LILLE, index) for index in range(13)] + [(PARIS_LILLE, index) for index in range(13)] +
[(PARIS, index) for index in range(10)]) [(PARIS, index) for index in range(10)])
self.assertEqual(anchors, ["Paris & Lille"]) self.assertEqual(anchors, ["Paris & Lille"])
self.assertTrue(all(date.year == 2025 for date, _, _ in records))
former_paris = (ART / f"2026-09-19-photo-{PARIS}-00.md").read_text() former_paris = (ART / f"2026-09-19-photo-{PARIS}-00.md").read_text()
self.assertIn(f'<div id="photo-{PARIS}"></div>', former_paris) self.assertEqual(former_paris.count(f'<div id="photo-{PARIS}"></div>'), 1)
def test_recent_analog_highlights_use_2025_in_supplied_order(self):
groups = (
("bf23ab31ef7b33150e9e99cb31f14b1bc27bc8143293a32bb1d4ac2193a493c6",
2, "Iranian Martyrs’ Memorial Cemetery"),
("18a628ae4ea72d0f9a68de6acfbc698449fe10e49ecf990a8ddced0b2e32949d",
3, "Geneva"),
)
newest = []
for job, count, anchor in groups:
records = []
anchors = []
for index in range(count):
content = (ART / f"2026-09-19-photo-{job}-{index:02}.md").read_text()
date = datetime.fromisoformat(re.search(
r'^date: "([^"]+)"', content, re.MULTILINE).group(1))
records.append(date)
anchors.extend(re.findall(r'^anchor: "([^"]+)"', content, re.MULTILINE))
self.assertTrue(all(date.year == 2025 for date in records))
self.assertEqual(anchors, [anchor])
newest.append(max(records))
paris_date = datetime.fromisoformat(re.search(
r'^date: "([^"]+)"',
(ART / f"2026-09-19-photo-{PARIS_LILLE}-00.md").read_text(),
re.MULTILINE).group(1))
self.assertGreater(newest[0], newest[1])
self.assertGreater(newest[1], paris_date)
if __name__ == "__main__": if __name__ == "__main__":