element found")
+ except Exception as exc:
+ print(
+ f"Warning: could not parse existing appcast.xml ({exc}), creating fresh one"
+ )
+ et = None
+else:
+ print("No existing appcast.xml found, creating fresh one")
+ et = None
+
+if et is None:
+ root = ET.fromstring(
+ ''
+ ''
+ ""
+ "BetterCapture Updates"
+ "https://github.com/jsattler/BetterCapture/releases/latest/download/appcast.xml"
+ "Updates for BetterCapture"
+ "en"
+ ""
+ ""
+ )
+ et = ET.ElementTree(root)
+ channel = root.find("channel")
+
+# Remove any existing items with the same version
+for item in channel.findall("item"):
+ sv = item.find("sparkle:shortVersionString", namespaces)
+ if sv is not None and sv.text == version:
+ channel.remove(item)
+ # Also remove items without pubDate (malformed)
+ if item.find("pubDate") is None:
+ channel.remove(item)
+
+# Prune old items, keep the most recent 15
+pubdate_format = "%a, %d %b %Y %H:%M:%S %z"
+items = channel.findall("item")
+items_with_date = [item for item in items if item.find("pubDate") is not None]
+items_with_date.sort(
+ key=lambda item: datetime.strptime(item.find("pubDate").text, pubdate_format)
+)
+prune_limit = 15
+if len(items_with_date) > prune_limit:
+ for item in items_with_date[:-prune_limit]:
+ channel.remove(item)
+
+
+def markdown_to_simple_html(md: str) -> str:
+ """Very basic markdown to HTML conversion for release notes."""
+ lines = md.strip().split("\n")
+ html_lines = []
+ in_list = False
+
+ for line in lines:
+ stripped = line.strip()
+
+ # Skip empty lines
+ if not stripped:
+ if in_list:
+ html_lines.append("")
+ in_list = False
+ html_lines.append("")
+ continue
+
+ # Headers
+ if stripped.startswith("### "):
+ if in_list:
+ html_lines.append("")
+ in_list = False
+ html_lines.append(f"{stripped[4:]}
")
+ elif stripped.startswith("## "):
+ if in_list:
+ html_lines.append("")
+ in_list = False
+ html_lines.append(f"{stripped[3:]}
")
+ elif stripped.startswith("# "):
+ if in_list:
+ html_lines.append("")
+ in_list = False
+ html_lines.append(f"{stripped[2:]}
")
+ # List items
+ elif stripped.startswith("- ") or stripped.startswith("* "):
+ if not in_list:
+ html_lines.append("")
+ in_list = True
+ content = stripped[2:]
+ # Convert markdown links to HTML
+ content = re.sub(
+ r"\[([^\]]+)\]\(([^)]+)\)", r'\1', content
+ )
+ # Convert bold
+ content = re.sub(r"\*\*([^*]+)\*\*", r"\1", content)
+ # Convert inline code
+ content = re.sub(r"`([^`]+)`", r"\1", content)
+ html_lines.append(f" - {content}
")
+ # Regular paragraph text
+ else:
+ if in_list:
+ html_lines.append("
")
+ in_list = False
+ # Convert inline formatting
+ text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'\1', stripped)
+ text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text)
+ text = re.sub(r"`([^`]+)`", r"\1", text)
+ html_lines.append(f"{text}
")
+
+ if in_list:
+ html_lines.append("")
+
+ return "\n".join(html_lines)
+
+
+# Build release notes HTML
+if release_notes.strip():
+ notes_html = markdown_to_simple_html(release_notes)
+ description_html = f"""
+BetterCapture v{version}
+{notes_html}
+"""
+else:
+ description_html = f"""
+BetterCapture v{version}
+This release was published on {now.strftime("%Y-%m-%d")}.
+
+View the full release notes on
+GitHub.
+
+"""
+
+# Create new appcast item
+item = ET.SubElement(channel, "item")
+
+elem = ET.SubElement(item, "title")
+elem.text = f"Version {version}"
+
+elem = ET.SubElement(item, "pubDate")
+elem.text = now.strftime(pubdate_format)
+
+elem = ET.SubElement(item, "sparkle:version")
+# Use a build number derived from version for CFBundleVersion comparison
+# Sparkle compares sparkle:version against CFBundleVersion
+elem.text = "1" # Will be overridden in CI with the actual build number
+
+elem = ET.SubElement(item, "sparkle:shortVersionString")
+elem.text = version
+
+elem = ET.SubElement(item, "sparkle:minimumSystemVersion")
+elem.text = "26.0"
+
+if release_url:
+ elem = ET.SubElement(item, "sparkle:fullReleaseNotesLink")
+ elem.text = release_url
+
+elem = ET.SubElement(item, "description")
+elem.text = description_html
+
+elem = ET.SubElement(item, "enclosure")
+elem.set("url", dmg_url)
+elem.set("type", "application/octet-stream")
+for key, value in attrs.items():
+ elem.set(key, value)
+
+# Write output
+et.write("appcast_new.xml", xml_declaration=True, encoding="utf-8")
+print(f"Generated appcast_new.xml for version {version}")