Files
serman-rss-wrapper-mixcloud/local_podcast_generator.py

607 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Lokaler Podcast RSS Feed Generator
Scannt das audio/ Verzeichnis nach MP3-Dateien und erstellt einen RSS-Feed für Podcast-Apps.
"""
import os
import xml.etree.ElementTree as ET
from datetime import datetime
import mutagen
from mutagen.mp3 import MP3
from mutagen.id3 import ID3
import argparse
import urllib.parse
from pathlib import Path
class LocalPodcastGenerator:
def __init__(self, audio_dir="../httpdocs/_audio", output_file="podcast_feed.xml", base_url="https://www.serman.club"):
self.audio_dir = audio_dir
self.output_file = output_file
self.base_url = base_url.rstrip('/')
def get_mp3_files(self):
"""Scannt das Audio-Verzeichnis nach MP3-Dateien."""
audio_path = Path(self.audio_dir)
if not audio_path.exists():
print(f"❌ Audio-Verzeichnis '{self.audio_dir}' existiert nicht!")
return []
mp3_files = list(audio_path.glob("*.mp3"))
print(f"🎵 {len(mp3_files)} MP3-Dateien gefunden in '{self.audio_dir}/'")
# Debug: Zeige alle gefundenen Dateien
for i, mp3_file in enumerate(mp3_files):
print(f" Datei {i+1}: '{mp3_file.name}'")
# Benenne MP3-Dateien um (Leerzeichen → Unterstriche)
renamed_files = []
rename_count = 0
for mp3_file in mp3_files:
print(f" 🔍 Prüfe: '{mp3_file.name}' - Leerzeichen vorhanden: {' ' in mp3_file.name}")
if ' ' in mp3_file.name:
# Neuer Dateiname ohne Leerzeichen
new_name = mp3_file.name.replace(' ', '_')
new_path = mp3_file.parent / new_name
# Prüfe ob neue Datei bereits existiert
if not new_path.exists():
try:
mp3_file.rename(new_path)
print(f" 📝 Umbenannt: '{mp3_file.name}''{new_name}'")
renamed_files.append(new_path)
rename_count += 1
except Exception as e:
print(f" ⚠️ Fehler beim Umbenennen von '{mp3_file.name}': {e}")
renamed_files.append(mp3_file)
else:
print(f" ⚠️ Datei '{new_name}' existiert bereits, überspringe Umbenennung")
# Verwende die bereits existierende umbenannte Datei
renamed_files.append(new_path)
else:
print(f" ✅ Keine Umbenennung nötig: '{mp3_file.name}'")
renamed_files.append(mp3_file)
print(f" 📊 {rename_count} Dateien umbenannt, {len(renamed_files)} Dateien total")
# Sortiere nach Änderungsdatum (neueste zuerst)
renamed_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
return renamed_files
def get_mp3_metadata(self, file_path):
"""Extrahiert Metadaten aus einer MP3-Datei."""
try:
audio = MP3(file_path)
# Versuche ID3-Tags zu lesen
title = None
artist = None
album = None
duration = None
cover_data = None
if audio.tags:
title = str(audio.tags.get('TIT2', [''])[0]) if audio.tags.get('TIT2') else None
artist = str(audio.tags.get('TPE1', [''])[0]) if audio.tags.get('TPE1') else None
album = str(audio.tags.get('TALB', [''])[0]) if audio.tags.get('TALB') else None
# Cover-Art extrahieren (APIC = Attached Picture)
# Versuche verschiedene APIC-Tag-Varianten
for key in audio.tags.keys():
if key.startswith('APIC'):
try:
cover_data = audio.tags[key].data
if cover_data:
print(f" 🎨 Cover gefunden in Tag: {key}")
break
except Exception as tag_error:
print(f" ⚠️ Fehler beim Lesen von {key}: {tag_error}")
continue
# Dauer in Sekunden
if hasattr(audio, 'info') and audio.info.length:
duration = int(audio.info.length)
# Fallback: Dateiname als Titel verwenden
if not title:
title = file_path.stem
# Dateigröße
file_size = file_path.stat().st_size
# Änderungsdatum als Veröffentlichungsdatum
pub_date = datetime.fromtimestamp(file_path.stat().st_mtime)
return {
'title': title,
'artist': artist or 'SERMAN',
'album': album,
'duration': duration,
'file_size': file_size,
'pub_date': pub_date,
'filename': file_path.name,
'cover_data': cover_data
}
except Exception as e:
print(f"⚠️ Fehler beim Lesen der Metadaten von {file_path.name}: {e}")
# Fallback-Metadaten
return {
'title': file_path.stem,
'artist': 'SERMAN',
'album': None,
'duration': None,
'file_size': file_path.stat().st_size,
'pub_date': datetime.fromtimestamp(file_path.stat().st_mtime),
'filename': file_path.name,
'cover_data': None
}
def extract_episode_cover(self, metadata, episode_index):
"""Extrahiert und speichert das Episode-Cover (nur wenn noch nicht vorhanden)."""
if not metadata['cover_data']:
return None
try:
# Erstelle Cover-Dateiname: gleicher Name wie MP3, aber mit .jpg Endung und _ statt Leerzeichen
mp3_stem = Path(metadata['filename']).stem
cover_filename = f"{mp3_stem.replace(' ', '_')}.jpg"
# Speichere Cover im _audio-Verzeichnis (neben den MP3-Dateien)
audio_path = Path(self.audio_dir)
cover_path = audio_path / cover_filename
# Prüfe ob Cover bereits existiert
if cover_path.exists():
# Cover existiert bereits - verwende es einfach (URL-encoded)
cover_url = f"{self.base_url}/_audio/{self.safe_url_encode(cover_filename)}"
print(f" 🖼️ Cover bereits vorhanden: {cover_filename}")
return cover_url
# Cover existiert noch nicht - extrahiere und validiere es
try:
from PIL import Image
import io
# Lade das Bild aus den Cover-Daten
image = Image.open(io.BytesIO(metadata['cover_data']))
original_width, original_height = image.size
print(f" 📐 Original Cover-Größe: {original_width}x{original_height}px")
# Apple Podcasts benötigt quadratische Cover zwischen 1400-3000px
# Behalte Originalmaße bei, nur Qualität optimieren für <512KB
if original_width < 1400 or original_height < 1400:
target_size = 1400
print(f" ⚠️ Cover zu klein, skaliere auf {target_size}x{target_size}px")
elif original_width > 3000 or original_height > 3000:
target_size = 3000
print(f" ⚠️ Cover zu groß, skaliere auf {target_size}x{target_size}px")
else:
# Cover ist in akzeptabler Größe, mache es quadratisch
target_size = min(original_width, original_height)
if target_size < 1400:
target_size = 1400
elif target_size > 3000:
target_size = 3000
print(f" ✓ Cover-Größe OK, mache quadratisch: {target_size}x{target_size}px")
# Mache das Bild quadratisch (schneide zu oder fülle auf)
if original_width != original_height:
size = min(original_width, original_height)
left = (original_width - size) // 2
top = (original_height - size) // 2
right = left + size
bottom = top + size
image = image.crop((left, top, right, bottom))
print(f" ✂️ Bild zugeschnitten auf quadratisch: {size}x{size}px")
# Skaliere auf Zielgröße
image = image.resize((target_size, target_size), Image.Resampling.LANCZOS)
# Konvertiere zu RGB falls nötig (für JPEG) - Apple Podcasts erfordert festen Hintergrund
if image.mode in ('RGBA', 'LA', 'P'):
# Verwende weißen Hintergrund für Transparenz (Apple Podcasts Anforderung)
background = Image.new('RGB', image.size, (255, 255, 255))
if image.mode == 'P':
image = image.convert('RGBA')
if image.mode in ('RGBA', 'LA'):
# Berücksichtige Alpha-Kanal für bessere Transparenz-Behandlung
background.paste(image, mask=image.split()[-1] if len(image.split()) > 3 else None)
else:
background.paste(image)
image = background
print(f" 🔄 Transparenz entfernt (Apple Podcasts Anforderung)")
# Speichere das optimierte Cover mit angepasster Qualität für Episode-Cover
# Reduziere nur JPEG-Qualität, behalte Bildgröße zwischen 1400-3000px bei
quality = 85
max_file_size = 512 * 1024 # 512KB in Bytes
# Ersten Versuch mit Standard-Qualität
temp_buffer = io.BytesIO()
image.save(temp_buffer, 'JPEG', quality=quality, optimize=True, progressive=True)
file_size = temp_buffer.tell()
# Reduziere nur Qualität iterativ falls Datei zu groß (Bildgröße bleibt gleich)
while file_size > max_file_size and quality > 30:
quality -= 5
temp_buffer = io.BytesIO()
image.save(temp_buffer, 'JPEG', quality=quality, optimize=True, progressive=True)
file_size = temp_buffer.tell()
print(f" 📉 Reduziere Qualität auf {quality}% (Dateigröße: {file_size//1024}KB)")
# Speichere die finale Version (Bildgröße bleibt bei {target_size}x{target_size}px)
image.save(cover_path, 'JPEG', quality=quality, optimize=True, progressive=True)
final_width, final_height = image.size
final_size_kb = cover_path.stat().st_size // 1024
print(f" 🖼️ Cover optimiert: {cover_filename} ({final_width}x{final_height}px, {final_size_kb}KB, Qualität: {quality}%)")
# Prüfe finale Dateigröße und warne bei Überschreitung
if final_size_kb > 512:
print(f" ⚠️ Warnung: Cover-Datei größer als 512KB ({final_size_kb}KB)")
else:
print(f" ✅ Cover-Datei unter 512KB Limit")
except ImportError:
print(" ❌ PIL/Pillow nicht installiert - kann Cover nicht optimieren!")
print(" 💡 Installiere mit: uv add pillow")
return None
except Exception as img_error:
print(f" ❌ Fehler bei Cover-Optimierung: {img_error}")
return None
# Rückgabe der URL zum Cover (URL-encoded für korrekte Links)
cover_url = f"{self.base_url}/_audio/{self.safe_url_encode(cover_filename)}"
return cover_url
except Exception as e:
print(f" ⚠️ Fehler beim Extrahieren des Covers: {e}")
return None
def safe_url_encode(self, filename):
"""Erstellt URL-sichere Dateinamen durch korrektes Encoding."""
# Ersetze Leerzeichen durch Unterstriche und verwende dann URL-Encoding
safe_filename = filename.replace(' ', '_')
return urllib.parse.quote(safe_filename, safe='')
def format_rfc2822_date(self, dt):
"""Formatiert ein Datum im RFC 2822 Format für RSS."""
# RFC 2822 Format: "Wed, 02 Oct 2002 08:00:00 EST"
# Verwende englische Wochentag/Monat-Namen und GMT
weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
weekday = weekdays[dt.weekday()]
month = months[dt.month - 1]
return f"{weekday}, {dt.day:02d} {month} {dt.year} {dt.hour:02d}:{dt.minute:02d}:{dt.second:02d} GMT"
def format_duration(self, seconds):
"""Formatiert die Dauer in HH:MM:SS Format."""
if not seconds:
return "00:00:00"
hours = seconds // 3600
minutes = (seconds % 3600) // 60
seconds = seconds % 60
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
def create_rss_feed(self, podcast_title="SERMAN - Organic House Podcast",
podcast_description=None, podcast_author="SERMAN"):
"""Erstellt den RSS-Feed aus den lokalen MP3-Dateien."""
# Standard-Beschreibung
if not podcast_description:
podcast_description = """Die Leidenschaft für organischen House
Als Club DJ lebe ich für die Momente, in denen Musik und Emotion verschmelzen. Meine Sets sind geprägt von organischen House-Klängen, die eine Brücke zwischen tiefen Basslines und melodischen Höhenflügen schlagen.
Ich spezialisiere mich auf House Music, die mehr als nur Beats bietet sie erzählt Geschichten durch warme Vocals, emotionale Melodien und eine Atmosphäre, die Menschen verbindet. Jeder Mix ist eine Reise durch verschiedene Stimmungen und Energielevel."""
mp3_files = self.get_mp3_files()
if not mp3_files:
print("❌ Keine MP3-Dateien gefunden!")
return False
# RSS Root Element mit Atom-Namespace für self-link
rss = ET.Element("rss")
rss.set("version", "2.0")
rss.set("xmlns:itunes", "http://www.itunes.com/dtds/podcast-1.0.dtd")
rss.set("xmlns:content", "http://purl.org/rss/1.0/modules/content/")
rss.set("xmlns:atom", "http://www.w3.org/2005/Atom")
# Channel Element
channel = ET.SubElement(rss, "channel")
# Channel Metadaten mit besserer Podcast-Kompatibilität
title = ET.SubElement(channel, "title")
title.text = podcast_title
description = ET.SubElement(channel, "description")
description.text = podcast_description
link = ET.SubElement(channel, "link")
link.text = self.base_url
language = ET.SubElement(channel, "language")
language.text = "de" # ISO 639-1 Code (nicht de-DE)
# Atom self-link (erforderlich für RSS-Validierung)
atom_link = ET.SubElement(channel, "atom:link")
atom_link.set("href", f"{self.base_url}/{self.output_file}")
atom_link.set("rel", "self")
atom_link.set("type", "application/rss+xml")
# Wichtig: lastBuildDate für bessere Erkennung von Updates (RFC 2822 konform)
# Verwende aktuelles Datum, aber falls es in der Zukunft liegt, verwende heutiges Datum
current_date = datetime.now()
if current_date.year > 2024: # Falls Systemdatum falsch ist
current_date = datetime(2024, 12, 5, 12, 0, 0) # Fallback auf realistisches Datum
last_build = ET.SubElement(channel, "lastBuildDate")
last_build.text = self.format_rfc2822_date(current_date)
# pubDate für den Channel (RFC 2822 konform)
channel_pub_date = ET.SubElement(channel, "pubDate")
channel_pub_date.text = self.format_rfc2822_date(current_date)
# Generator Info
generator = ET.SubElement(channel, "generator")
generator.text = "SERMAN Local Podcast Generator"
# iTunes-spezifische Tags für bessere Podcast-App-Kompatibilität
itunes_author = ET.SubElement(channel, "itunes:author")
itunes_author.text = podcast_author
itunes_summary = ET.SubElement(channel, "itunes:summary")
itunes_summary.text = podcast_description
# iTunes Email (empfohlen)
itunes_owner = ET.SubElement(channel, "itunes:owner")
owner_name = ET.SubElement(itunes_owner, "itunes:name")
owner_name.text = podcast_author
owner_email = ET.SubElement(itunes_owner, "itunes:email")
owner_email.text = "music@serman.club"
# iTunes Kategorie (nur gültige iTunes-Kategorien verwenden)
itunes_category = ET.SubElement(channel, "itunes:category")
itunes_category.set("text", "Music")
# Entferne ungültige Sub-Kategorie - Music hat keine gültigen Sub-Kategorien
# Explicit Content (muss "yes", "no" oder "clean" sein)
itunes_explicit = ET.SubElement(channel, "itunes:explicit")
itunes_explicit.text = "no"
# iTunes Type (für episodische Podcasts)
itunes_type = ET.SubElement(channel, "itunes:type")
itunes_type.text = "episodic"
# Standard-Bild (kann später angepasst werden)
image_url = f"{self.base_url}/_img/podcast-cover.jpg"
image = ET.SubElement(channel, "image")
image_url_elem = ET.SubElement(image, "url")
image_url_elem.text = image_url
image_title = ET.SubElement(image, "title")
image_title.text = podcast_title
image_link = ET.SubElement(image, "link")
image_link.text = self.base_url
itunes_image = ET.SubElement(channel, "itunes:image")
itunes_image.set("href", image_url)
print(f"📦 Erstelle RSS-Feed mit {len(mp3_files)} Episoden...")
# Items (Episoden) hinzufügen
for episode_index, mp3_file in enumerate(mp3_files):
metadata = self.get_mp3_metadata(mp3_file)
print(f"\n🎵 Episode {episode_index + 1}: {metadata['title']}")
print(f" 📄 Datei: {metadata['filename']}")
print(f" 🎨 Cover-Daten vorhanden: {'Ja' if metadata['cover_data'] else 'Nein'}")
# Episode-Cover extrahieren
episode_cover_url = self.extract_episode_cover(metadata, episode_index)
if episode_cover_url:
print(f" 🔗 Cover-URL: {episode_cover_url}")
else:
print(f" ❌ Keine Cover-URL generiert")
item = ET.SubElement(channel, "item")
# Titel
item_title = ET.SubElement(item, "title")
item_title.text = metadata['title']
# Beschreibung
item_description = ET.SubElement(item, "description")
description_text = f"Mix von {metadata['artist']}"
if metadata['album']:
description_text += f" aus {metadata['album']}"
item_description.text = description_text
# Link zur Episode-Seite (nicht zur Audio-Datei)
item_link = ET.SubElement(item, "link")
item_link.text = self.base_url
# GUID (eindeutige ID - verwende Audio-URL)
item_guid = ET.SubElement(item, "guid")
# Die Audio-URL als eindeutige GUID (URL-safe encoded)
audio_url = f"{self.base_url}/_audio/{self.safe_url_encode(metadata['filename'])}"
item_guid.text = audio_url
item_guid.set("isPermaLink", "false")
# Veröffentlichungsdatum (RFC 2822 konform)
item_pubdate = ET.SubElement(item, "pubDate")
# RFC 2822 Format mit korrekter Formatierung
pub_date_formatted = self.format_rfc2822_date(metadata['pub_date'])
item_pubdate.text = pub_date_formatted
# Audio-Enclosure (das ist die eigentliche Audio-URL)
enclosure = ET.SubElement(item, "enclosure")
enclosure.set("url", audio_url)
enclosure.set("type", "audio/mpeg")
enclosure.set("length", str(metadata['file_size']))
# Dauer (Apple empfiehlt Sekunden als Integer)
if metadata['duration']:
item_duration = ET.SubElement(item, "itunes:duration")
item_duration.text = str(int(metadata['duration'])) # Integer Sekunden für Apple
# iTunes-spezifische Tags für bessere Episode-Erkennung
itunes_title = ET.SubElement(item, "itunes:title")
itunes_title.text = metadata['title']
itunes_summary = ET.SubElement(item, "itunes:summary")
itunes_summary.text = description_text
itunes_explicit_item = ET.SubElement(item, "itunes:explicit")
itunes_explicit_item.text = "no" # Muss "yes", "no" oder "clean" sein
# iTunes Episode Typ
itunes_episode_type = ET.SubElement(item, "itunes:episodeType")
itunes_episode_type.text = "full"
# Episode-Cover hinzufügen - nur iTunes Format für Episode-Level
if episode_cover_url:
# Nur iTunes image tag für Episode-Cover (Standard RSS <image> ist nur für Channel-Level gültig)
itunes_image_item = ET.SubElement(item, "itunes:image")
itunes_image_item.set("href", episode_cover_url)
print(f" ✅ iTunes Cover-Tag hinzugefügt: {episode_cover_url}")
else:
print(f" ⚠️ Kein Episode-Cover verfügbar - verwende Standard-Podcast-Cover")
# Keywords/Tags basierend auf Dateiname (mit Kommas getrennt)
if any(keyword in metadata['title'].lower() for keyword in ['organic', 'house']):
itunes_keywords = ET.SubElement(item, "itunes:keywords")
itunes_keywords.text = "organic, house, electronic, mix"
print(f"{metadata['title']} ({self.format_duration(metadata['duration']) if metadata['duration'] else 'Unbekannte Dauer'})")
if episode_cover_url:
print(f" 🖼️ Mit Episode-Cover: {episode_cover_url}")
else:
print(f" 📷 Ohne Episode-Cover")
# XML in Datei schreiben
tree = ET.ElementTree(rss)
ET.indent(tree, space=" ", level=0)
try:
# Explizite XML-Deklaration mit UTF-8 encoding
tree.write(self.output_file, encoding='UTF-8', xml_declaration=True)
print(f"✅ RSS-Feed erfolgreich erstellt: {self.output_file}")
print(f"📊 Anzahl der Episoden: {len(mp3_files)}")
print(f"🌐 Audio-URLs verwenden Basis-URL: {self.base_url}")
return True
except Exception as e:
print(f"❌ Fehler beim Schreiben der XML-Datei: {e}")
return False
def serve_feed(self, port=8000):
"""Startet einen einfachen HTTP-Server für den RSS-Feed und Audio-Dateien."""
import http.server
import socketserver
import os
from pathlib import Path
# Kopiere den RSS-Feed ins httpdocs-Verzeichnis
if "../httpdocs" in self.audio_dir:
httpdocs_path = Path("../httpdocs").resolve()
if httpdocs_path.exists():
# Kopiere RSS-Feed ins httpdocs-Verzeichnis
import shutil
source_feed = Path(self.output_file)
target_feed = httpdocs_path / self.output_file
if source_feed.exists():
shutil.copy2(source_feed, target_feed)
print(f"📄 RSS-Feed kopiert nach: {target_feed}")
# Wechsle ins httpdocs-Verzeichnis für den Server
os.chdir(httpdocs_path)
print(f"📁 Server-Root: {httpdocs_path}")
else:
print(f"⚠️ httpdocs-Verzeichnis nicht gefunden: {httpdocs_path}")
class CustomHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
"""Überschreibt GET-Requests um Root-Zugriff zu blockieren."""
if self.path == '/' or self.path == '/index.html' or self.path == '/index.htm':
self.send_error(403, "Access denied")
return
else:
# Für alle anderen Pfade normale Verarbeitung
super().do_GET()
def do_HEAD(self):
"""HTTP HEAD Requests für Apple Podcasts Kompatibilität."""
# HEAD-Requests sind für Apple Podcasts erforderlich
super().do_HEAD()
def list_directory(self, path):
"""Deaktiviert Directory-Listing - zeigt 403 Forbidden."""
self.send_error(403, "Directory listing disabled")
return None
def end_headers(self):
# CORS-Header hinzufügen für bessere Kompatibilität
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, HEAD, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
# Cache-Control für bessere Performance
if self.path.endswith(('.mp3', '.jpg', '.png')):
self.send_header('Cache-Control', 'public, max-age=3600')
super().end_headers()
handler = CustomHandler
try:
with socketserver.TCPServer(("", port), handler) as httpd:
print(f"🌐 Server läuft auf http://localhost:{port}")
print(f"📡 RSS-Feed: http://localhost:{port}/{self.output_file}")
print(f"🎵 Audio-Dateien: http://localhost:{port}/_audio/")
print("⏹️ Drücke Ctrl+C zum Beenden")
httpd.serve_forever()
except KeyboardInterrupt:
print("\n👋 Server beendet.")
except Exception as e:
print(f"❌ Fehler beim Starten des Servers: {e}")
def main():
parser = argparse.ArgumentParser(description="Erstellt einen RSS-Feed aus lokalen MP3-Dateien")
parser.add_argument("-a", "--audio-dir", default="../httpdocs/_audio",
help="Verzeichnis mit MP3-Dateien (Standard: ../httpdocs/_audio)")
parser.add_argument("-o", "--output", default="podcast_feed.xml",
help="Ausgabedatei für den RSS-Feed (Standard: podcast_feed.xml)")
parser.add_argument("-u", "--base-url", default="https://www.serman.club",
help="Basis-URL für Audio-Dateien (Standard: https://www.serman.club)")
parser.add_argument("-t", "--title", default="SERMAN - Organic House Podcast",
help="Titel des Podcasts")
parser.add_argument("--author", default="SERMAN",
help="Autor des Podcasts")
parser.add_argument("--serve", action="store_true",
help="Startet einen HTTP-Server für den RSS-Feed")
parser.add_argument("--port", type=int, default=8087,
help="Port für den HTTP-Server (Standard: 80870)")
args = parser.parse_args()
generator = LocalPodcastGenerator(args.audio_dir, args.output, args.base_url)
print(f"🎵 Erstelle lokalen Podcast-Feed aus '{args.audio_dir}/'")
print(f"📡 Basis-URL: {args.base_url}")
print("-" * 60)
success = generator.create_rss_feed(args.title, None, args.author)
if success and args.serve:
generator.serve_feed(args.port)
if __name__ == "__main__":
main()