commit a74162f4eb5ac83edf175c85672bcf50149d1396 Author: Manuel Weiser Date: Sat Jul 5 17:02:16 2025 +0200 Füge Mixcloud RSS Feed Generator hinzu, einschließlich Hauptskript, Serverstarter und Testskript. Aktualisiere .gitignore, .python-version, pyproject.toml und requirements.txt. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ecf51e --- /dev/null +++ b/.gitignore @@ -0,0 +1,200 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be added to the global gitignore or merged into this project gitignore. For a PyCharm +# project, it is recommended to comment out the following line: +#.idea/ + +# Project-specific files +# Generated RSS feeds +*.xml +!example_feed.xml + +# Temporary test files +test_*.xml +*_test.xml + +# Log files +*.log + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# VS Code +.vscode/ +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# IDE files +.idea/ +*.swp +*.swo +*~ + +# Backup files +*.backup +*.bak + +# UV package manager +.uv_cache/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/README.md b/README.md new file mode 100644 index 0000000..c300da5 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Mixcloud RSS Feed Generator + +Dieses Python-Script erstellt einen RSS-Feed aus deinen Mixcloud-Tracks, damit du sie über Podcast-Apps abonnieren und anhören kannst. + +## Installation + +1. Abhängigkeiten installieren: + + ```bash + uv pip install -r requirements.txt + ``` + +## Verwendung + +### RSS-Feed erstellen + +```bash +python mixcloud_rss.py serman_dj +``` + +Das erstellt eine `mixcloud_feed.xml` Datei mit deinen neuesten Mixcloud-Tracks. + +### Erweiterte Optionen + +```bash +# Feed mit 100 Tracks erstellen +python mixcloud_rss.py serman_dj --limit 100 + +# Feed in spezifische Datei speichern +python mixcloud_rss.py serman_dj --output mein_feed.xml + +# HTTP-Server starten für den Feed +python mixcloud_rss.py serman_dj --serve + +# Server auf anderem Port starten +python mixcloud_rss.py serman_dj --serve --port 8080 +``` + +### RSS-Feed in Podcast-App hinzufügen + +1. **Mit HTTP-Server (empfohlen):** + - Starte den Server: `python mixcloud_rss.py serman_dj --serve` + - Füge diese URL in deiner Podcast-App hinzu: `http://localhost:8000/mixcloud_feed.xml` + +2. **Feed-Datei hosten:** + - Lade die generierte XML-Datei auf einen Webserver hoch + - Verwende die öffentliche URL in deiner Podcast-App + +## Funktionen + +- ✅ Holt automatisch deine neuesten Mixcloud-Tracks +- ✅ Erstellt RSS-Feed im Podcast-Format +- ✅ Unterstützt iTunes-Tags für bessere Kompatibilität +- ✅ Inkludiert Track-Metadaten (Titel, Beschreibung, Dauer, Tags) +- ✅ Eingebauter HTTP-Server zum Testen +- ✅ Konfigurierbare Anzahl von Tracks + +## Bekannte Einschränkungen + +**Audio-Streaming:** Mixcloud erlaubt kein direktes Audio-Streaming ohne Autorisierung. Die generierten Links verweisen auf die Mixcloud-Webseite. Für echtes Audio-Streaming müsste man: + +1. Die offizielle Mixcloud API für Streaming verwenden +2. Oder eine Alternative wie yt-dlp für das Extrahieren der Audio-URLs nutzen + +## Troubleshooting + +### "Keine Cloudcasts gefunden" + +- Überprüfe den Benutzernamen +- Stelle sicher, dass das Profil öffentlich ist + +### RSS-Feed wird nicht in Podcast-App erkannt + +- Überprüfe, ob der HTTP-Server läuft +- Teste die URL im Browser: `http://localhost:8000/mixcloud_feed.xml` + +## Beispiel-Ausgabe + +```text +Erstelle RSS-Feed für Mixcloud-User: serman_dj +RSS-Feed erfolgreich erstellt: mixcloud_feed.xml +Anzahl der Episoden: 50 +``` + +## Automatisierung + +Du kannst das Script regelmäßig ausführen lassen, um den Feed aktuell zu halten: + +```bash +# Crontab-Eintrag für tägliche Updates um 6:00 Uhr +0 6 * * * cd /pfad/zu/rss-feeder && python mixcloud_rss.py serman_dj +``` diff --git a/main.py b/main.py new file mode 100644 index 0000000..a283ec9 --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from rss-feeder!") + + +if __name__ == "__main__": + main() diff --git a/mixcloud_rss.py b/mixcloud_rss.py new file mode 100755 index 0000000..6fa8a8a --- /dev/null +++ b/mixcloud_rss.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +""" +Mixcloud RSS Feed Generator +Erstellt einen RSS-Feed aus Mixcloud-Tracks für Podcast-Apps. +""" + +import requests +import xml.etree.ElementTree as ET +from datetime import datetime +import json +import time +from urllib.parse import quote +import argparse +import os + + +class MixcloudRSSGenerator: + def __init__(self, username, output_file="mixcloud_feed.xml"): + self.username = username + self.output_file = output_file + self.base_url = "https://api.mixcloud.com" + self.user_url = f"{self.base_url}/{username}/" + + def get_user_info(self): + """Holt Benutzerinformationen von Mixcloud.""" + try: + response = requests.get(self.user_url) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + print(f"Fehler beim Abrufen der Benutzerinformationen: {e}") + return None + + def get_cloudcasts(self, limit=50): + """Holt die neuesten Cloudcasts (Tracks) des Benutzers.""" + cloudcasts_url = f"{self.user_url}cloudcasts/" + params = {"limit": limit} + + try: + response = requests.get(cloudcasts_url, params=params) + response.raise_for_status() + data = response.json() + return data.get("data", []) + except requests.RequestException as e: + print(f"Fehler beim Abrufen der Cloudcasts: {e}") + return [] + + def format_duration(self, seconds): + """Formatiert die Dauer in HH:MM:SS Format.""" + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + seconds = seconds % 60 + return f"{hours:02d}:{minutes:02d}:{seconds:02d}" + + def get_stream_url(self, cloudcast_key): + """ + Generiert eine Stream-URL für den Cloudcast. + Hinweis: Mixcloud erlaubt direktes Streaming nur über ihre eigene API. + """ + # Dies ist eine vereinfachte URL - in der Praxis müsste man + # die offizielle Mixcloud-Streaming-API verwenden + return f"https://www.mixcloud.com{cloudcast_key}stream/" + + def create_rss_feed(self): + """Erstellt den RSS-Feed aus den Mixcloud-Daten.""" + user_info = self.get_user_info() + if not user_info: + return False + + cloudcasts = self.get_cloudcasts() + if not cloudcasts: + print("Keine Cloudcasts 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 = f"{user_info.get('name', self.username)} - Mixcloud Feed" + + description = ET.SubElement(channel, "description") + description.text = user_info.get('biog', f"Mixcloud-Feed von {self.username}") + + link = ET.SubElement(channel, "link") + link.text = f"https://www.mixcloud.com/{self.username}/" + + language = ET.SubElement(channel, "language") + language.text = "de-DE" + + # iTunes-spezifische Tags + itunes_author = ET.SubElement(channel, "itunes:author") + itunes_author.text = user_info.get('name', self.username) + + itunes_summary = ET.SubElement(channel, "itunes:summary") + itunes_summary.text = user_info.get('biog', f"Mixcloud-Feed von {self.username}") + + itunes_category = ET.SubElement(channel, "itunes:category") + itunes_category.set("text", "Music") + + # Bild falls vorhanden + if user_info.get('pictures', {}).get('large'): + image = ET.SubElement(channel, "image") + image_url = ET.SubElement(image, "url") + image_url.text = user_info['pictures']['large'] + image_title = ET.SubElement(image, "title") + image_title.text = title.text + image_link = ET.SubElement(image, "link") + image_link.text = link.text + + itunes_image = ET.SubElement(channel, "itunes:image") + itunes_image.set("href", user_info['pictures']['large']) + + # Items (Episoden) hinzufügen + for cloudcast in cloudcasts: + item = ET.SubElement(channel, "item") + + # Titel + item_title = ET.SubElement(item, "title") + item_title.text = cloudcast.get('name', 'Unbekannter Titel') + + # Beschreibung + item_description = ET.SubElement(item, "description") + description_text = cloudcast.get('description', '') + if not description_text: + description_text = f"Mix von {self.username}" + item_description.text = description_text + + # Link zur Mixcloud-Seite + item_link = ET.SubElement(item, "link") + item_link.text = cloudcast.get('url', '') + + # GUID + item_guid = ET.SubElement(item, "guid") + item_guid.text = cloudcast.get('key', '') + item_guid.set("isPermaLink", "false") + + # Veröffentlichungsdatum + item_pubdate = ET.SubElement(item, "pubDate") + created_time = cloudcast.get('created_time') + if created_time: + # Konvertiere ISO-Format zu RFC 2822 + dt = datetime.fromisoformat(created_time.replace('Z', '+00:00')) + item_pubdate.text = dt.strftime('%a, %d %b %Y %H:%M:%S %z') + + # Audio-Enclosure + # Hinweis: Mixcloud erlaubt kein direktes Audio-Streaming ohne Autorisierung + # Dies ist ein Platzhalter - für echtes Streaming müsste man die Mixcloud API verwenden + enclosure = ET.SubElement(item, "enclosure") + stream_url = f"https://www.mixcloud.com{cloudcast.get('key', '')}" + enclosure.set("url", stream_url) + enclosure.set("type", "audio/mpeg") + + # Dauer + duration = cloudcast.get('audio_length', 0) + if duration: + item_duration = ET.SubElement(item, "itunes:duration") + item_duration.text = self.format_duration(duration) + + # iTunes-spezifische Tags + itunes_title = ET.SubElement(item, "itunes:title") + itunes_title.text = item_title.text + + itunes_summary = ET.SubElement(item, "itunes:summary") + itunes_summary.text = description_text + + # Tags hinzufügen + tags = cloudcast.get('tags', []) + if tags: + keywords = ", ".join([tag['name'] for tag in tags[:5]]) # Nur erste 5 Tags + itunes_keywords = ET.SubElement(item, "itunes:keywords") + itunes_keywords.text = keywords + + # 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(cloudcasts)}") + 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.""" + import http.server + import socketserver + import os + + # Wechsle in das Verzeichnis mit der XML-Datei + os.chdir(os.path.dirname(os.path.abspath(self.output_file))) + + handler = http.server.SimpleHTTPRequestHandler + + try: + with socketserver.TCPServer(("", port), handler) as httpd: + print(f"Server läuft auf http://localhost:{port}") + print(f"RSS-Feed verfügbar unter: http://localhost:{port}/{os.path.basename(self.output_file)}") + print("Drücke Ctrl+C zum Beenden") + httpd.serve_forever() + except KeyboardInterrupt: + print("\nServer beendet.") + except Exception as e: + print(f"Fehler beim Starten des Servers: {e}") + + +def main(): + parser = argparse.ArgumentParser(description="Erstellt einen RSS-Feed aus Mixcloud-Tracks") + parser.add_argument("username", help="Mixcloud-Benutzername (z.B. serman_dj)") + parser.add_argument("-o", "--output", default="mixcloud_feed.xml", + help="Ausgabedatei für den RSS-Feed (Standard: mixcloud_feed.xml)") + parser.add_argument("-l", "--limit", type=int, default=50, + help="Anzahl der zu holenden Tracks (Standard: 50)") + parser.add_argument("--serve", action="store_true", + help="Startet einen HTTP-Server für den RSS-Feed") + parser.add_argument("--port", type=int, default=8000, + help="Port für den HTTP-Server (Standard: 8000)") + + args = parser.parse_args() + + generator = MixcloudRSSGenerator(args.username, args.output) + + print(f"Erstelle RSS-Feed für Mixcloud-User: {args.username}") + success = generator.create_rss_feed() + + if success and args.serve: + generator.serve_feed(args.port) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..67f0cfe --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "rss-feeder" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0eb8cae --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests>=2.31.0 diff --git a/start_server.py b/start_server.py new file mode 100755 index 0000000..c5ff147 --- /dev/null +++ b/start_server.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +Einfacher Server-Starter für den Mixcloud RSS-Feed +""" + +import os +import sys +import subprocess +import time +import threading +import http.server +import socketserver +from pathlib import Path + +def update_feed(): + """Aktualisiert den RSS-Feed.""" + print("🔄 Aktualisiere RSS-Feed...") + result = subprocess.run([ + sys.executable, "mixcloud_rss.py", "serman_dj", + "--output", "mixcloud_feed.xml" + ], capture_output=True, text=True) + + if result.returncode == 0: + print("✅ RSS-Feed erfolgreich aktualisiert!") + return True + else: + print(f"❌ Fehler beim Aktualisieren: {result.stderr}") + return False + +def start_server(port=8000): + """Startet den HTTP-Server für den RSS-Feed.""" + handler = http.server.SimpleHTTPRequestHandler + + try: + with socketserver.TCPServer(("", port), handler) as httpd: + print(f"🌐 Server läuft auf http://localhost:{port}") + print(f"📡 RSS-Feed: http://localhost:{port}/mixcloud_feed.xml") + print("=" * 60) + print("📱 Podcast-App Anleitung:") + print(" 1. Kopiere diese URL: http://localhost:{port}/mixcloud_feed.xml") + print(" 2. Öffne deine Podcast-App") + print(" 3. Wähle 'Podcast hinzufügen' oder 'Feed hinzufügen'") + print(" 4. Füge die URL ein") + print("=" * 60) + print("⏹️ Drücke Ctrl+C zum Beenden") + print() + + httpd.serve_forever() + except KeyboardInterrupt: + print("\n👋 Server beendet.") + except Exception as e: + print(f"❌ Fehler beim Starten des Servers: {e}") + +def auto_update_feed(interval_minutes=60): + """Aktualisiert den Feed automatisch in regelmäßigen Abständen.""" + while True: + time.sleep(interval_minutes * 60) # Warte x Minuten + print(f"\n⏰ Automatische Aktualisierung nach {interval_minutes} Minuten...") + update_feed() + +def main(): + """Hauptfunktion.""" + print("🎵 SERMAN DJ - Mixcloud RSS Server") + print("=" * 40) + + # Überprüfe ob wir im richtigen Verzeichnis sind + if not Path("mixcloud_rss.py").exists(): + print("❌ mixcloud_rss.py nicht gefunden!") + print(" Stelle sicher, dass du im richtigen Verzeichnis bist.") + sys.exit(1) + + # Erstelle initialen Feed + print("🏁 Erstelle initialen RSS-Feed...") + if not update_feed(): + sys.exit(1) + + # Überprüfe ob Feed existiert + if not Path("mixcloud_feed.xml").exists(): + print("❌ RSS-Feed konnte nicht erstellt werden!") + sys.exit(1) + + # Starte automatische Updates in separatem Thread + update_thread = threading.Thread( + target=auto_update_feed, + args=(60,), # Aktualisiere jede Stunde + daemon=True + ) + update_thread.start() + print("⏰ Automatische Updates aktiviert (alle 60 Minuten)") + + # Starte Server + start_server() + +if __name__ == "__main__": + main() diff --git a/test_generator.py b/test_generator.py new file mode 100755 index 0000000..5e8bf95 --- /dev/null +++ b/test_generator.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +""" +Beispiel-Script zum Testen des Mixcloud RSS Generators +""" + +from mixcloud_rss import MixcloudRSSGenerator +import sys + +def test_generator(): + """Testet den RSS-Generator mit deinem Mixcloud-Profil.""" + username = "serman_dj" + + print(f"🎵 Teste RSS-Generator für: {username}") + print("-" * 50) + + # Generator erstellen + generator = MixcloudRSSGenerator(username, "test_feed.xml") + + # Benutzerinfo testen + print("📋 Hole Benutzerinformationen...") + user_info = generator.get_user_info() + + if user_info: + print(f"✅ Benutzer gefunden: {user_info.get('name', 'Unbekannt')}") + print(f" Follower: {user_info.get('follower_count', 0)}") + print(f" Following: {user_info.get('following_count', 0)}") + print(f" Cloudcasts: {user_info.get('cloudcast_count', 0)}") + else: + print("❌ Benutzer nicht gefunden!") + return False + + # Cloudcasts testen + print("\n🎧 Hole die ersten 10 Cloudcasts...") + cloudcasts = generator.get_cloudcasts(limit=10) + + if cloudcasts: + print(f"✅ {len(cloudcasts)} Cloudcasts gefunden:") + for i, cloudcast in enumerate(cloudcasts[:5], 1): + name = cloudcast.get('name', 'Unbekannter Titel') + duration = cloudcast.get('audio_length', 0) + duration_str = generator.format_duration(duration) if duration else "Unbekannt" + print(f" {i}. {name} ({duration_str})") + + if len(cloudcasts) > 5: + print(f" ... und {len(cloudcasts) - 5} weitere") + else: + print("❌ Keine Cloudcasts gefunden!") + return False + + # RSS-Feed erstellen + print("\n📡 Erstelle RSS-Feed...") + success = generator.create_rss_feed() + + if success: + print("✅ RSS-Feed erfolgreich erstellt!") + print(f" Datei: {generator.output_file}") + + # Feed-Info anzeigen + import os + if os.path.exists(generator.output_file): + size = os.path.getsize(generator.output_file) + print(f" Größe: {size} Bytes") + + # Erste Zeilen der XML-Datei anzeigen + print("\n📄 Feed-Preview:") + with open(generator.output_file, 'r', encoding='utf-8') as f: + lines = f.readlines()[:10] + for line in lines: + print(f" {line.rstrip()}") + if len(lines) >= 10: + print(" ...") + + return True + else: + print("❌ Fehler beim Erstellen des RSS-Feeds!") + return False + +def main(): + """Hauptfunktion.""" + print("🎵 Mixcloud RSS Generator - Test") + print("=" * 50) + + success = test_generator() + + if success: + print("\n🎉 Test erfolgreich abgeschlossen!") + print("\nNächste Schritte:") + print("1. Starte den Server: python mixcloud_rss.py serman_dj --serve") + print("2. Öffne http://localhost:8000/mixcloud_feed.xml im Browser") + print("3. Füge die URL in deiner Podcast-App hinzu") + else: + print("\n❌ Test fehlgeschlagen!") + sys.exit(1) + +if __name__ == "__main__": + main()