Custom Python script for updating this list

2026-04-07

I've made this little script for updating this very same list of project updates. It updates the HTML and the RSS feed in one go, and I can easily use it from my phone using Shortcuts and a-Shell
        
          #!/usr/bin/env python3
import urllib.request
import urllib.error
import getpass
import re
import io
import sys
from datetime import datetime, timezone
import xml.etree.ElementTree as ET
from ftplib import FTP_TLS

def slugify(text):
    """Convert title to a URL-friendly filename."""
    text = text.lower()
    text = re.sub(r'[^\w\s-]', '', text)
    return re.sub(r'[-\s]+', '-', text).strip('-')

def fetch_file(url):
    try:
        req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
        with urllib.request.urlopen(req, timeout=30) as response:
            return response.read().decode('utf-8')
    except Exception as e:
        print(f"Error fetching {url}: {e}")
        return None

def generate_project_page(title, date, project_html):
    """Creates the standalone HTML. Note the 4 levels of ../ to reach root."""
    return f'''<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>{title} - Adriano Farina</title>
    <link rel="stylesheet" type="text/css" href="../../../../shome.css" />
  </head>
  <body>
    <article>
      <nav><a href="../../../../index.html">← Back to Home</a></nav>
      <header>
        <h1>{title}</h1>
        <p><em>{date}</em></p>
      </header>
      <section>
        {project_html}
      </section>
    </article>
    <footer>
      <hr>
      <p><a href="../../../../index.html">Adriano Farina</a></p>
    </footer>
  </body>
</html>'''

def add_project_to_html(html, title, date, project_html, year, subjects, project_url):
    subject_classes = ' '.join(sorted(subjects))
    new_entry = f'''          
  • {date}: {title} {project_html}
  • ''' pattern = r'(
      [\s\S]*?)(
    \s*

    Current Projects

    )' match = re.search(pattern, html, re.IGNORECASE) if match: insert_pos = match.start(2) return html[:insert_pos] + new_entry + "\n" + html[insert_pos:] return html def parse_rss_robustly(xml_str): if not xml_str: return None try: return ET.fromstring(xml_str.strip().encode('utf-8')) except ET.ParseError: for fix in ["", ""]: try: return ET.fromstring((xml_str.strip() + fix).encode('utf-8')) except: continue return ET.fromstring('Adriano Farina'.encode('utf-8')) def add_entry_to_rss(feed_xml, title, link, pub_date, description): root = parse_rss_robustly(feed_xml) if root is None: return None channel = root.find('channel') target = channel if channel is not None else root item = ET.SubElement(target, 'item') ET.SubElement(item, 'title').text = title ET.SubElement(item, 'link').text = link ET.SubElement(item, 'pubDate').text = pub_date ET.SubElement(item, 'description').text = description return '\n' + ET.tostring(root, encoding='unicode') def ensure_ftp_dir(ftp, remote_directory): """Recursively creates directories on the FTP server.""" parts = remote_directory.split('/') for part in parts: if not part: continue try: ftp.mkd(part) except: pass # Directory probably exists ftp.cwd(part) # Go back to root after building ftp.cwd('/') def upload_via_ftp(host, username, password, index_html, feed_xml, project_page_html, project_dir, project_filename): try: ftp = FTP_TLS() ftp.connect(host, 21, timeout=30) ftp.auth() ftp.prot_p() ftp.login(username, password) # 1. Ensure the nested directory structure exists ensure_ftp_dir(ftp, project_dir) # 2. Upload Main Files ftp.storbinary('STOR index.html', io.BytesIO(index_html.encode('utf-8'))) ftp.storbinary('STOR feed.xml', io.BytesIO(feed_xml.encode('utf-8'))) # 3. Upload Project Page to its specific folder ftp.storbinary(f'STOR {project_dir}/{project_filename}', io.BytesIO(project_page_html.encode('utf-8'))) ftp.quit() return True except Exception as e: print(f"FTP Error: {e}") return False def main(): if len(sys.argv) >= 7: title, date, project_html, subjects_raw, ftp_user, ftp_pass = sys.argv[1:7] subjects = [s.strip() for s in subjects_raw.split(',')] else: title = input("Title: ") date = input("Date (YYYY-MM-DD): ") project_html = input("Description: ") subjects = [s.strip() for s in input("Subjects: ").split(',')] ftp_user = input("User: ") ftp_pass = getpass.getpass("Pass: ") # Date parsing for directories try: dt = datetime.strptime(date, "%Y-%m-%d") year, month, day = date.split('-') except: print("Invalid date format.") return pub_date = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S GMT") # Path logic slug = slugify(title) project_filename = f"{slug}.html" project_dir = f"projects/{year}/{month}/{day}" project_path = f"{project_dir}/{project_filename}" full_url = f"https://www.adrianofarina.it/{project_path}" print("Fetching site...") index_html = fetch_file("https://www.adrianofarina.it/index.html") feed_xml = fetch_file("https://www.adrianofarina.it/feed.xml") if not index_html: return print("Generating files...") project_page_content = generate_project_page(title, date, project_html) updated_html = add_project_to_html(index_html, title, date, project_html, year, subjects, project_path) updated_feed = add_entry_to_rss(feed_xml, title, full_url, pub_date, project_html) print("Uploading to FTP...") if upload_via_ftp("example.com", ftp_user, ftp_pass, updated_html, updated_feed, project_page_content, project_dir, project_filename): print(f"SUCCESS! Page live at: {full_url}") else: print("Upload failed.") if __name__ == "__main__": main()