Complete RSS/OPML output metadata #35

Manually merged
marvin8 merged 5 commits from feat/issue-28-feed-opml-metadata into main 2026-08-20 03:08:54 +00:00
Collaborator

Closes #28

Two output-metadata additions (the width/height dimensions were decided out — see the review thread):

  • OPML <head><dateModified> — current UTC timestamp in RFC 822 format (locale-independent via email.utils.format_datetime), so aggregators can tell when the index last changed.
  • Missing-feed warningwrite_opml() takes a verbose flag and warns when a channel directory has no feed.xml at OPML generation time (threaded through from the CLI's --verbose).

Covered by Tryke tests (red → green): dateModified format, and the verbose warning (warn + silent paths).

Closes #28 Two output-metadata additions (the width/height dimensions were decided out — see the review thread): - **OPML `<head><dateModified>`** — current UTC timestamp in RFC 822 format (locale-independent via `email.utils.format_datetime`), so aggregators can tell when the index last changed. - **Missing-feed warning** — `write_opml()` takes a `verbose` flag and warns when a channel directory has no `feed.xml` at OPML generation time (threaded through from the CLI's `--verbose`). Covered by Tryke tests (red → green): `dateModified` format, and the verbose warning (warn + silent paths).
add itunes:image dimensions, OPML dateModified, and missing-feed warning
All checks were successful
/ gitleaks (pull_request) Successful in 18s
/ checks (pull_request) Successful in 2m6s
/ publish (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 3m25s
1eb3ba97c7
forgejo-actions left a comment

WuMing

Found 3 issue(s). See inline comments below.

## WuMing Found **3** issue(s). See inline comments below.
@ -98,3 +98,3 @@
print(str(opml_path))
else:
write_opml(parent_dir, channels, base_url)
write_opml(parent_dir, channels, base_url, verbose=verbose)

tests [LOW]

The CLI now threads verbose into write_opml, but no test in the diff exercises this wiring. The new unit tests cover write_opml directly, so a regression that drops this argument would go undetected. Add a CLI-level test (e.g., in the existing CLI opml.xml integration block) that runs with --verbose and a channel missing feed.xml, asserting the warning is printed.

**tests** [LOW] The CLI now threads `verbose` into `write_opml`, but no test in the diff exercises this wiring. The new unit tests cover `write_opml` directly, so a regression that drops this argument would go undetected. Add a CLI-level test (e.g., in the existing CLI opml.xml integration block) that runs with `--verbose` and a channel missing `feed.xml`, asserting the warning is printed. <!-- wuming:sha256:654cb91a80b381a8e258ebbb605538884565cfcdc6b2d41f90a82dc8679a079a -->
marvin8 marked this conversation as resolved
@ -1,6 +1,8 @@
"""OPML builder — generates an ``opml.xml`` index listing all channel feeds."""
# SPDX-License-Identifier: AGPL-3.0-or-later
from datetime import UTC

code [MEDIUM]

datetime.UTC was added in Python 3.11. If this package still supports Python 3.9 or 3.10 (the type hints use list[...], not the older typing.List), this import will raise ImportError at runtime. Use from datetime import timezone and datetime.now(timezone.utc) instead for broader compatibility.

**code** [MEDIUM] datetime.UTC was added in Python 3.11. If this package still supports Python 3.9 or 3.10 (the type hints use list[...], not the older typing.List), this import will raise ImportError at runtime. Use from datetime import timezone and datetime.now(timezone.utc) instead for broader compatibility. <!-- wuming:sha256:ea31398419667aceb28219dbc9fc5ea5de434fb2609eecc20c2db4337798fd3d -->
marvin8 marked this conversation as resolved
@ -27,6 +29,7 @@ def build_opml_xml(
root = etree.Element("opml", attrib={"version": "2.0"})
head = etree.SubElement(root, "head")
etree.SubElement(head, "title").text = "Podcasts"
etree.SubElement(head, "dateModified").text = datetime.now(UTC).strftime("%a, %d %b %Y %H:%M:%S +0000")

code [HIGH]

strftime('%a, %d %b ...') is locale-dependent. Under a non-English locale the day and month abbreviations can be localized, producing an invalid RFC 822/OPML dateModified value. Use locale-independent formatting such as email.utils.formatdate(datetime.now(timezone.utc).timestamp(), usegmt=True) or email.utils.format_datetime, which always emits English abbreviations.

**code** [HIGH] strftime('%a, %d %b ...') is locale-dependent. Under a non-English locale the day and month abbreviations can be localized, producing an invalid RFC 822/OPML dateModified value. Use locale-independent formatting such as email.utils.formatdate(datetime.now(timezone.utc).timestamp(), usegmt=True) or email.utils.format_datetime, which always emits English abbreviations. <!-- wuming:sha256:de1ef35634d8f1f4389c7b2897b19cc27cc33bbbd091a9df65ae45df9681fd39 -->
marvin8 marked this conversation as resolved
🐛 use locale-independent format_datetime for OPML dateModified
All checks were successful
/ gitleaks (pull_request) Successful in 23s
/ checks (pull_request) Successful in 2m34s
/ publish (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 4m50s
9db1f9db55
Author
Collaborator

WuMing review — 1 fixed, 2 declined

src/sub2pod/opml.py line 32

strftime('%a, %d %b ...') is locale-dependent…

Fixed in commit 9db1f9d — now uses email.utils.format_datetime(datetime.now(UTC)), which always emits English abbreviations regardless of locale.

src/sub2pod/opml.py line 4

datetime.UTC was added in Python 3.11…

🔴 Declined — pyproject.toml sets requires-python = ">=3.12", so datetime.UTC (added in 3.11) is always available. feed.py already imports it (from datetime import UTC).

src/sub2pod/cli.py line 100

The CLI now threads verbose into write_opml, but no test exercises this wiring…

🔴 Declined — the warning is unreachable at the CLI level: in a normal run, write_feed() writes every channel's feed.xml before write_opml() runs, so no channel is ever missing it. A CLI-level test can't construct the missing-feed scenario. The verbose behaviour is covered by direct unit tests on write_opml() (warn + silent paths).

### WuMing review — 1 fixed, 2 declined [`src/sub2pod/opml.py` line 32](https://forge.marvin8.zone/marvin8/sub2pod/pulls/35#issuecomment-1741) > strftime('%a, %d %b ...') is locale-dependent… ✅ Fixed in commit `9db1f9d` — now uses `email.utils.format_datetime(datetime.now(UTC))`, which always emits English abbreviations regardless of locale. [`src/sub2pod/opml.py` line 4](https://forge.marvin8.zone/marvin8/sub2pod/pulls/35#issuecomment-1742) > datetime.UTC was added in Python 3.11… 🔴 Declined — `pyproject.toml` sets `requires-python = ">=3.12"`, so `datetime.UTC` (added in 3.11) is always available. `feed.py` already imports it (`from datetime import UTC`). [`src/sub2pod/cli.py` line 100](https://forge.marvin8.zone/marvin8/sub2pod/pulls/35#issuecomment-1743) > The CLI now threads verbose into write_opml, but no test exercises this wiring… 🔴 Declined — the warning is unreachable at the CLI level: in a normal run, `write_feed()` writes every channel's `feed.xml` before `write_opml()` runs, so no channel is ever missing it. A CLI-level test can't construct the missing-feed scenario. The verbose behaviour is covered by direct unit tests on `write_opml()` (warn + silent paths).
forgejo-actions left a comment

WuMing

Found 4 issue(s). See inline comments below.

## WuMing Found **4** issue(s). See inline comments below.
@ -199,6 +199,8 @@ def _append_episode_item(
image_url = build_enclosure_url(base_url, channel_dir, episode.thumbnail_path)
itunes_image = etree.SubElement(item_element, f"{_ITUNES}image")
itunes_image.set("href", image_url)
itunes_image.set("width", "3000")

code [MEDIUM]

Hardcoded width/height of 3000 may not match the actual episode thumbnail. If episode.thumbnail_path is not guaranteed to be exactly 3000x3000, this produces incorrect RSS metadata. Derive the real dimensions from the image or enforce/omit these attributes.

**code** [MEDIUM] Hardcoded width/height of 3000 may not match the actual episode thumbnail. If episode.thumbnail_path is not guaranteed to be exactly 3000x3000, this produces incorrect RSS metadata. Derive the real dimensions from the image or enforce/omit these attributes. <!-- wuming:sha256:9153279b788ed92bb199b1895834515e09b14e0cdb064f948637482923c2cf8b -->
marvin8 marked this conversation as resolved
@ -1,6 +1,9 @@
"""OPML builder — generates an ``opml.xml`` index listing all channel feeds."""
# SPDX-License-Identifier: AGPL-3.0-or-later
from datetime import UTC

code [LOW]

datetime.UTC was added in Python 3.11. If this project still supports Python 3.10 or earlier, importing opml will raise ImportError. Prefer from datetime import timezone and datetime.now(timezone.utc) for broader compatibility.

**code** [LOW] datetime.UTC was added in Python 3.11. If this project still supports Python 3.10 or earlier, importing opml will raise ImportError. Prefer from datetime import timezone and datetime.now(timezone.utc) for broader compatibility. <!-- wuming:sha256:edab9529e8cb8fd21ea559e5e04fcee7b3c21db66f32d34ef1f7801d249d4598 -->
marvin8 marked this conversation as resolved
@ -27,6 +30,7 @@ def build_opml_xml(
root = etree.Element("opml", attrib={"version": "2.0"})
head = etree.SubElement(root, "head")
etree.SubElement(head, "title").text = "Podcasts"
etree.SubElement(head, "dateModified").text = format_datetime(datetime.now(UTC))

code [MEDIUM]

format_datetime delegates to strftime, whose weekday/month abbreviations are locale-dependent. In a non-English locale this can emit non-RFC-822 names and make the test regex fail. Use a fixed-English formatter or force the C locale for this call.

**code** [MEDIUM] format_datetime delegates to strftime, whose weekday/month abbreviations are locale-dependent. In a non-English locale this can emit non-RFC-822 names and make the test regex fail. Use a fixed-English formatter or force the C locale for this call. <!-- wuming:sha256:ba4c3a8f0d658d1d24e7bae8848cb9870105e005ee16a33806a198e59542a873 -->
marvin8 marked this conversation as resolved
@ -57,0 +64,4 @@
if verbose:
for channel_info, channel_dir in channels:
if not (channel_dir / "feed.xml").exists():
print(f"warning: {channel_info.title} has no feed.xml")

code [LOW]

Warnings should normally go to stderr, not stdout, so normal command output remains unpolluted for scripts and pipes. Use print(..., file=sys.stderr) and update the test to capture stderr.

**code** [LOW] Warnings should normally go to stderr, not stdout, so normal command output remains unpolluted for scripts and pipes. Use print(..., file=sys.stderr) and update the test to capture stderr. <!-- wuming:sha256:013fc5d716ffc2476fa68a51516b03969ddc7fafe5f2f0eb453a75dba08885a4 -->
marvin8 marked this conversation as resolved
Author
Collaborator

WuMing review — 1 escalating, 3 declined

src/sub2pod/feed.py line 202

Hardcoded width/height of 3000 may not match the actual episode thumbnail…

Escalating to owner — this is a design decision (declare 3000×3000 per the issue's spec, read real dimensions via a new Pillow dependency, or omit). Will resolve separately.

src/sub2pod/opml.py line 33

format_datetime delegates to strftime, whose weekday/month abbreviations are locale-dependent…

🔴 Declined — incorrect. email.utils.format_datetime formats day/month via _format_timetuple_and_zone using hardcoded English name lists, deliberately avoiding strftime (CPython source comment: "we cannot use strftime() because that honors the locale"). The only strftime call is %z for the numeric offset, which is locale-independent.

src/sub2pod/opml.py line 4

datetime.UTC was added in Python 3.11…

🔴 Declined (re-raise of #1742) — requires-python = ">=3.12", so datetime.UTC (3.11+) is always available.

src/sub2pod/opml.py line 66

Warnings should normally go to stderr…

🔴 Declined — this warning is --verbose diagnostic output, which this codebase prints to stdout (see the existing verbose channel/episode logging in cli.py). Splitting just this line to stderr would fragment the verbose stream.

### WuMing review — 1 escalating, 3 declined [`src/sub2pod/feed.py` line 202](https://forge.marvin8.zone/marvin8/sub2pod/pulls/35#issuecomment-1747) > Hardcoded width/height of 3000 may not match the actual episode thumbnail… Escalating to owner — this is a design decision (declare 3000×3000 per the issue's spec, read real dimensions via a new Pillow dependency, or omit). Will resolve separately. [`src/sub2pod/opml.py` line 33](https://forge.marvin8.zone/marvin8/sub2pod/pulls/35#issuecomment-1748) > format_datetime delegates to strftime, whose weekday/month abbreviations are locale-dependent… 🔴 Declined — incorrect. `email.utils.format_datetime` formats day/month via `_format_timetuple_and_zone` using hardcoded English name lists, deliberately avoiding `strftime` (CPython source comment: "we cannot use strftime() because that honors the locale"). The only `strftime` call is `%z` for the numeric offset, which is locale-independent. [`src/sub2pod/opml.py` line 4](https://forge.marvin8.zone/marvin8/sub2pod/pulls/35#issuecomment-1749) > datetime.UTC was added in Python 3.11… 🔴 Declined (re-raise of #1742) — `requires-python = ">=3.12"`, so `datetime.UTC` (3.11+) is always available. [`src/sub2pod/opml.py` line 66](https://forge.marvin8.zone/marvin8/sub2pod/pulls/35#issuecomment-1750) > Warnings should normally go to stderr… 🔴 Declined — this warning is `--verbose` diagnostic output, which this codebase prints to stdout (see the existing verbose channel/episode logging in `cli.py`). Splitting just this line to stderr would fragment the verbose stream.
📝 mark completed improvements; add OPML metadata release notes
All checks were successful
/ gitleaks (pull_request) Successful in 18s
/ checks (pull_request) Successful in 2m11s
/ publish (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 4m32s
ca70d2e321
Author
Collaborator

src/sub2pod/feed.py line 202

Hardcoded width/height of 3000 may not match the actual episode thumbnail…

Addressed in commit 47a3cbc — the width/height attributes are now omitted entirely. The actual ytdl-sub thumbnails are 16:9 YouTube images, so declaring 3000×3000 would be inaccurate; we chose to omit rather than add an image-reading dependency.

[`src/sub2pod/feed.py` line 202](https://forge.marvin8.zone/marvin8/sub2pod/pulls/35#issuecomment-1747) > Hardcoded width/height of 3000 may not match the actual episode thumbnail… ✅ Addressed in commit `47a3cbc` — the `width`/`height` attributes are now omitted entirely. The actual ytdl-sub thumbnails are 16:9 YouTube images, so declaring 3000×3000 would be inaccurate; we chose to omit rather than add an image-reading dependency.
forgejo-actions left a comment

WuMing

Found 1 issue(s). See inline comments below.

## WuMing Found **1** issue(s). See inline comments below.
improvements.md Outdated
@ -35,3 +35,3 @@
- [ ] Update `ytdl-sub-integration.md` to reflect that per-episode thumbnails are supported
- [ ] Add `width`/`height` attributes to per-episode `<itunes:image>` elements (3000×3000 recommended)
- [x] Add `width`/`height` attributes to per-episode `<itunes:image>` elements (3000×3000 recommended) — decided to omit: ytdl-sub thumbnails are 16:9 YouTube images, not 3000×3000 square, so declared dimensions would be inaccurate

docs [HIGH]

This line contradicts itself and the PR description: the checkbox marks “Add width/height attributes” as completed, but the note says the change was “decided to omit.” If the attributes were not added, reword the item to reflect a decision (e.g. “[x] Decide whether to add width/height… — decided to omit…”) and update the PR description. If they were added, remove the “decided to omit” note.

**docs** [HIGH] This line contradicts itself and the PR description: the checkbox marks “Add width/height attributes” as completed, but the note says the change was “decided to omit.” If the attributes were not added, reword the item to reflect a decision (e.g. “[x] Decide whether to add width/height… — decided to omit…”) and update the PR description. If they were added, remove the “decided to omit” note. <!-- wuming:sha256:65d5537b01bc1d86498dc6e85d5ad987e45f6d040113c6538b00e964506f9e92 -->
marvin8 marked this conversation as resolved
✏️ reword width/height item as a decision, not an addition
All checks were successful
/ gitleaks (pull_request) Successful in 18s
/ checks (pull_request) Successful in 1m58s
/ publish (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 3m52s
411e5f5c59
Author
Collaborator

improvements.md line 37

This line contradicts itself and the PR description…

Fixed in commit 411e5f5 — reworded to "Decide on per-episode <itunes:image> width/height attributes — decided to omit: …". The PR description already reflects the omission.

[`improvements.md` line 37](https://forge.marvin8.zone/marvin8/sub2pod/pulls/35#issuecomment-1756) > This line contradicts itself and the PR description… ✅ Fixed in commit `411e5f5` — reworded to "Decide on per-episode `<itunes:image>` width/height attributes — decided to omit: …". The PR description already reflects the omission.
marvin8 approved these changes 2026-08-20 03:08:28 +00:00
marvin8 manually merged commit b4ba927733 into main 2026-08-20 03:08:54 +00:00
marvin8 deleted branch feat/issue-28-feed-opml-metadata 2026-08-20 03:09:01 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
marvin8/sub2pod!35
No description provided.