import os, time, email.utils, urllib.parse, html, re, sys, datetime
TITLE = os.environ.get("FEED_TITLE", "Cum Town Archive (local)")
DESC = os.environ.get("FEED_DESC", "Self-hosted archive feed served from my NAS")
BASE = os.environ.get("BASE_URL")
LIMIT = int(os.environ.get("MAX_ITEMS","0")) # 0 = onbeperkt
ORDER_ASC = False # output-volgorde (maakt in PA meestal niet uit)
ONLY_SUBDIR = os.environ.get("ONLY_SUBDIR","").strip() # bv. '1) Cum Town Archive'
NUMBERED_PUBDATE = os.environ.get("NUMBERED_PUBDATE","0") in ("1","true","True")
if not BASE:
raise SystemExit("BASE_URL env var ontbreekt")
AUDIO_EXTS = {".mp3",".m4a",".aac",".ogg",".opus",".flac",".wav"}
def is_audio(fn):
return os.path.splitext(fn)[1].lower() in AUDIO_EXTS
def esc(s:str) -> str:
return html.escape(s, quote=True)
def parse_episode_number(name: str):
base = os.path.splitext(os.path.basename(name))[0]
m = re.match(r'^\s*(\d+)', base)
if m: return int(m.group(1))
m = re.search(r'\b[Ee]p[.\s-]*(\d+)', base)
if m: return int(m.group(1))
m = re.search(r'\b[Bb]onus[.\s-]*(\d+)', base)
if m: return int(m.group(1))
return None
walk_root = "."
if ONLY_SUBDIR:
walk_root = os.path.join(".", ONLY_SUBDIR)
if not os.path.isdir(walk_root):
raise SystemExit(f"ONLY_SUBDIR bestaat niet: {ONLY_SUBDIR}")
items=[]
for root,_,files in os.walk(walk_root):
for f in files:
if not is_audio(f):
continue
p=os.path.join(root,f)
st=os.stat(p)
rel=os.path.relpath(p,".").replace("\\","/")
url=BASE.rstrip("/")+"/"+urllib.parse.quote(rel)
size=str(st.st_size)
original_title=os.path.splitext(os.path.basename(f))[0]
epnum=parse_episode_number(f)
items.append({
"mtime": st.st_mtime,
"original_title": original_title,
"url": url,
"size": size,
"epnum": epnum,
"rel": rel,
})
# Paddingbreedte bepalen
epnums=[it["epnum"] for it in items if it["epnum"] is not None]
pad = len(str(max(epnums))) if epnums else 3
# Definitieve titels + pubDate bepalen
base_dt = datetime.datetime(2010,1,1, tzinfo=datetime.timezone.utc) # vast begin; puur voor volgorde
for it in items:
if it["epnum"] is not None:
it["title"] = f"{it['epnum']:0{pad}d} — {it['original_title']}"
else:
it["title"] = it["original_title"]
if NUMBERED_PUBDATE and it["epnum"] is not None:
# volgorde volgens nummer: lagere nummers = oudere datum
dt = base_dt + datetime.timedelta(seconds=it["epnum"])
it["pub"] = email.utils.format_datetime(dt)
else:
it["pub"] = email.utils.formatdate(it["mtime"], usegmt=True)
# sorteren voor output (niet per se nodig, maar netjes)
def sort_key(it):
# sorteer primair op epnum, secundair op mtime
key_primary = (it["epnum"] is None, it["epnum"] if it["epnum"] is not None else it["mtime"])
return key_primary
items.sort(key=sort_key, reverse=not ORDER_ASC)
if LIMIT>0:
items = items[:LIMIT]
now=email.utils.formatdate(time.time(), usegmt=True)
parts=[f'''
{esc(TITLE)}
{esc(BASE)}
{esc(DESC)}nl-NL{esc(now)}Self-hostedfalse
''']
for it in items:
ext=it["url"].split("?")[0].split(".")[-1].lower()
mime=("audio/mpeg" if ext=="mp3" else
"audio/mp4" if ext in ("m4a","aac") else
"audio/ogg" if ext in ("ogg","opus") else
"application/octet-stream")
guid=urllib.parse.quote(it["url"], safe="")
parts.append(" \n")
parts.append(f" {esc(it['title'])}\n")
if it["epnum"] is not None:
parts.append(f" {it['epnum']}\n")
parts.append(" full\n")
parts.append(f" {esc(it['pub'])}\n")
parts.append(f" {esc(guid)}\n")
parts.append(f" \n")
parts.append(" \n")
parts.append(" \n\n")
with open("feed.xml","w",encoding="utf-8") as f:
f.write("".join(parts))
print(f"Written feed.xml with {len(items)} items (pad width {pad}, numbered_pubdate={NUMBERED_PUBDATE})")