Files
serman-rss-wrapper-mixcloud/local_podcast_generator.py
Manuel Weiser 36b62f7136 Update dependencies and restructure project files
- Added mutagen, requests, and yt-dlp as dependencies in pyproject.toml and requirements.txt.
- Removed run_generator.py, start_server.py, and test_generator.py as they are no longer needed.
- Introduced setup.sh for initial setup instructions and directory creation.
- Updated uv.lock with new package versions and dependencies.
2025-07-05 19:39:19 +02:00

308 lines
12 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="_audio", output_file="podcast_feed.xml", base_url="http://localhost:8087"):
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}/'")
# Sortiere nach Änderungsdatum (neueste zuerst)
mp3_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
return mp3_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
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
# 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
}
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
}
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
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/")
# Channel Element
channel = ET.SubElement(rss, "channel")
# Channel Metadaten
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-DE"
# iTunes-spezifische Tags
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_category = ET.SubElement(channel, "itunes:category")
itunes_category.set("text", "Music")
# Explicit Content
itunes_explicit = ET.SubElement(channel, "itunes:explicit")
itunes_explicit.text = "false"
# Standard-Bild (kann später angepasst werden)
image_url = f"{self.base_url}/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 mp3_file in mp3_files:
metadata = self.get_mp3_metadata(mp3_file)
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 Audio-Datei
item_link = ET.SubElement(item, "link")
audio_url = f"{self.base_url}/{self.audio_dir}/{urllib.parse.quote(metadata['filename'])}"
item_link.text = audio_url
# GUID
item_guid = ET.SubElement(item, "guid")
item_guid.text = audio_url
item_guid.set("isPermaLink", "true")
# Veröffentlichungsdatum
item_pubdate = ET.SubElement(item, "pubDate")
item_pubdate.text = metadata['pub_date'].strftime('%a, %d %b %Y %H:%M:%S %z')
# Audio-Enclosure
enclosure = ET.SubElement(item, "enclosure")
enclosure.set("url", audio_url)
enclosure.set("type", "audio/mpeg")
enclosure.set("length", str(metadata['file_size']))
# Dauer
if metadata['duration']:
item_duration = ET.SubElement(item, "itunes:duration")
item_duration.text = self.format_duration(metadata['duration'])
# iTunes-spezifische Tags
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 = "false"
# Keywords/Tags basierend auf Dateiname
if any(keyword in metadata['title'].lower() for keyword in ['organic', 'house']):
itunes_keywords = ET.SubElement(item, "itunes:keywords")
itunes_keywords.text = "Organic house"
print(f"{metadata['title']} ({self.format_duration(metadata['duration']) if metadata['duration'] else 'Unbekannte Dauer'})")
# XML in Datei schreiben
tree = ET.ElementTree(rss)
ET.indent(tree, space=" ", level=0)
try:
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
# Wechsle in das Arbeitsverzeichnis
original_dir = os.getcwd()
class CustomHandler(http.server.SimpleHTTPRequestHandler):
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, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
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}/{self.audio_dir}/")
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="_audio",
help="Verzeichnis mit MP3-Dateien (Standard: _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="http://localhost:8087",
help="Basis-URL für Audio-Dateien (Standard: http://localhost:8087)")
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()