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.
This commit is contained in:
2025-07-05 19:39:19 +02:00
parent 22c18a2f77
commit 36b62f7136
13 changed files with 627 additions and 928 deletions

150
README.md
View File

@ -1,117 +1,113 @@
# Mixcloud RSS Feed Generator
# SERMAN RSS Feed Generator - Lokale Version
Dieses Python-Script erstellt einen RSS-Feed aus deinen Mixcloud-Tracks, damit du sie über Podcast-Apps abonnieren und anhören kannst.
Ein Python-Tool zum Erstellen von RSS-Feeds aus lokalen MP3-Dateien für Podcast-Apps.
## Features
- 🎵 Scannt automatisch das `_audio/` Verzeichnis nach MP3-Dateien
- 📱 Erstellt Podcast-kompatible RSS-Feeds
- 🏷️ Liest ID3-Tags automatisch aus MP3-Dateien
- 🌐 Integrierter HTTP-Server zum Hosten der Dateien
- ⚡ Einfache Bedienung über Kommandozeile
## Installation
1. Abhängigkeiten installieren:
```bash
uv pip install -r requirements.txt
uv sync
```
2. MP3-Dateien in das `_audio/` Verzeichnis legen
## Verwendung
### RSS-Feed mit echten Audio-URLs erstellen (empfohlen)
### Einfache Nutzung
```bash
uv run python mixcloud_rss_pro.py serman_dj
uv run python main.py
```
Das erstellt eine `mixcloud_feed.xml` Datei mit **echten Audio-Streams**, die in Podcast-Apps abspielbar sind.
### Original-Version (nur Mixcloud-Links)
### Mit HTTP-Server
```bash
python mixcloud_rss.py serman_dj
uv run python main.py --serve
```
### Erweiterte Optionen
```bash
# Feed mit 100 Tracks erstellen (mit Audio-URLs)
uv run python mixcloud_rss_pro.py serman_dj --limit 100
# Feed in spezifische Datei speichern
uv run python mixcloud_rss_pro.py serman_dj --output mein_feed.xml
# Schnellmodus ohne Audio-Extraktion
uv run python mixcloud_rss_pro.py serman_dj --no-audio
# HTTP-Server starten für den Feed
uv run python mixcloud_rss_pro.py serman_dj --serve
# Server-Wrapper verwenden (automatische Updates)
python start_server.py
uv run python main.py --audio-dir _audio --output my_podcast.xml --base-url https://meinserver.de --serve --port 8080
```
### RSS-Feed in Podcast-App hinzufügen
## Parameter
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`
- `--audio-dir`: Verzeichnis mit MP3-Dateien (Standard: `_audio`)
- `--output`: Name der RSS-Feed-Datei (Standard: `serman_podcast.xml`)
- `--base-url`: Basis-URL für Audio-Links (Standard: `http://localhost:8000`)
- `--serve`: Startet automatisch einen HTTP-Server
- `--port`: Port für den HTTP-Server (Standard: 8000)
2. **Feed-Datei hosten:**
- Lade die generierte XML-Datei auf einen Webserver hoch
- Verwende die öffentliche URL in deiner Podcast-App
## RSS-Feed URLs
## Funktionen
Nach dem Start des Servers ist der Feed verfügbar unter:
- **Lokal**: http://localhost:8000/serman_podcast.xml
- **Mit eigener Basis-URL**: [DEINE_URL]/serman_podcast.xml
- ✅ **Echte Audio-URLs**: Extrahiert direkte Audio-Streams für Podcast-Apps
- ✅ 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
- ✅ Parallele Audio-URL-Extraktion für bessere Performance
## Podcast-App Integration
## Audio-Streaming
1. RSS-Feed generieren und Server starten
2. RSS-URL in Podcast-App hinzufügen
3. Neue MP3-Dateien im `_audio/` Verzeichnis ablegen
4. Feed neu generieren für Updates
🎉 **Problem gelöst!** Das neue `mixcloud_rss_pro.py` Script extrahiert echte Audio-URLs, die in Podcast-Apps abspielbar sind:
## Dateiformat
- ✅ Direkte `.m4a` Audio-Streams
- ✅ Korrekte Content-Types für Podcast-Apps
- ✅ Funktioniert mit Apple Podcasts, Spotify, etc.
Das Tool liest automatisch folgende ID3-Tags:
- **TIT2**: Titel (Fallback: Dateiname)
- **TPE1**: Künstler (Fallback: "SERMAN")
- **TALB**: Album (optional)
- **Dauer**: Automatisch erkannt
## Bekannte Einschränkungen
~~**Audio-Streaming:** Mixcloud erlaubt kein direktes Audio-Streaming ohne Autorisierung.~~
**✅ Gelöst:** Mit `yt-dlp` werden jetzt echte Audio-URLs extrahiert!
## 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`
- Verwende die Pro-Version: `uv run python mixcloud_rss_pro.py serman_dj`
### Audio wird nicht abgespielt
- ✅ **Gelöst:** Verwende `mixcloud_rss_pro.py` für echte Audio-URLs
- Die Pro-Version extrahiert direkte Audio-Streams
- Dauert länger, aber funktioniert in allen Podcast-Apps
## Beispiel-Ausgabe
## Verzeichnisstruktur
```text
Erstelle RSS-Feed für Mixcloud-User: serman_dj
RSS-Feed erfolgreich erstellt: mixcloud_feed.xml
Anzahl der Episoden: 50
rss-feeder/
├── _audio/ # MP3-Dateien hier ablegen
│ ├── mix1.mp3
│ ├── mix2.mp3
│ └── README.md
├── main.py # Hauptprogramm
├── local_podcast_generator.py # RSS-Generator
├── serman_podcast.xml # Generierter RSS-Feed
└── requirements.txt # Python-Abhängigkeiten
```
## Deployment
Für produktive Nutzung:
1. **Server bereitstellen** (z.B. VPS, Cloud-Instance)
2. **Dateien hochladen** und Dependencies installieren
3. **Permanenten Webserver** konfigurieren (nginx, Apache)
4. **RSS-Feed URL** an Hörer verteilen
## Automatisierung
Du kannst das Script regelmäßig ausführen lassen, um den Feed aktuell zu halten:
Für automatische Updates bei neuen MP3-Dateien:
```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
# Beispiel-Script für cron job
#!/bin/bash
cd /pfad/zu/rss-feeder
uv run python main.py --base-url https://meinserver.de/podcast
```
---
## Legacy: Mixcloud-Version
Die ursprünglichen Mixcloud-Skripte sind noch vorhanden:
- `mixcloud_rss_pro.py` - Mixcloud RSS mit Audio-Extraktion
- `mixcloud_rss.py` - Einfache Mixcloud RSS-Generierung
Siehe Git-Historie für die ursprüngliche Mixcloud-basierte README.

29
_audio/README.md Normal file
View File

@ -0,0 +1,29 @@
# Audio-Dateien
Lege deine MP3-Dateien in diesem Verzeichnis ab, um sie im RSS-Feed verfügbar zu machen.
## Unterstützte Formate
- MP3-Dateien (.mp3)
## Metadaten
Das System versucht automatisch ID3-Tags aus den MP3-Dateien zu lesen:
- **Titel**: Aus dem TIT2-Tag, sonst Dateiname
- **Künstler**: Aus dem TPE1-Tag, sonst "SERMAN"
- **Album**: Aus dem TALB-Tag (optional)
- **Dauer**: Automatisch erkannt
## Reihenfolge
Die Dateien werden nach Änderungsdatum sortiert (neueste zuerst).
## Beispiel-Struktur
```text
_audio/
├── SERMAN - Organic House 01.07.2025.mp3
├── SERMAN - Organic House PT2 28.06.25.mp3
└── README.md
```

307
local_podcast_generator.py Normal file
View File

@ -0,0 +1,307 @@
#!/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()

68
main.py
View File

@ -1,6 +1,70 @@
#!/usr/bin/env python3
"""
SERMAN RSS Feed Generator - Lokale Version
Generiert RSS-Feeds aus lokalen MP3-Dateien für Podcast-Apps.
"""
from local_podcast_generator import LocalPodcastGenerator
import argparse
import sys
import os
def main():
print("Hello from rss-feeder!")
parser = argparse.ArgumentParser(description="SERMAN RSS Feed Generator - Lokale Version")
parser.add_argument("-a", "--audio-dir", default="_audio",
help="Verzeichnis mit MP3-Dateien (Standard: _audio)")
parser.add_argument("-o", "--output", default="serman_podcast.xml",
help="Ausgabedatei für den RSS-Feed (Standard: serman_podcast.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("--serve", action="store_true",
help="Startet automatisch einen HTTP-Server nach der Generierung")
parser.add_argument("--port", type=int, default=8087,
help="Port für den HTTP-Server (Standard: 8087)")
args = parser.parse_args()
print("🎵 SERMAN RSS Feed Generator - Lokale Version")
print("=" * 50)
# Prüfe ob Audio-Verzeichnis existiert
if not os.path.exists(args.audio_dir):
print(f"❌ Audio-Verzeichnis '{args.audio_dir}' existiert nicht!")
print(f"💡 Erstelle das Verzeichnis und lege deine MP3-Dateien dort ab.")
return 1
# Erstelle den Generator
generator = LocalPodcastGenerator(
audio_dir=args.audio_dir,
output_file=args.output,
base_url=args.base_url
)
# Generiere den RSS-Feed
print(f"📂 Scanne '{args.audio_dir}/' nach MP3-Dateien...")
success = generator.create_rss_feed(
podcast_title="SERMAN - Organic House Podcast",
podcast_author="SERMAN"
)
if not success:
print("❌ Fehler beim Erstellen des RSS-Feeds!")
return 1
print("\n🎉 RSS-Feed erfolgreich erstellt!")
print(f"📄 Datei: {args.output}")
print(f"🌐 URL: {args.base_url}/{args.output}")
if args.serve:
print("\n🚀 Starte HTTP-Server...")
generator.serve_feed(args.port)
else:
print(f"\n💡 Zum Starten des Servers: python main.py --serve")
print(f"💡 Oder direkt: python local_podcast_generator.py --serve")
return 0
if __name__ == "__main__":
main()
sys.exit(main())

View File

@ -1,240 +0,0 @@
#!/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()

View File

@ -1,359 +0,0 @@
#!/usr/bin/env python3
"""
Erweiterte Mixcloud RSS Feed Generator mit Audio-Streaming
Extrahiert echte Audio-URLs für die Wiedergabe in 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
import yt_dlp
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading
class MixcloudRSSGeneratorPro:
def __init__(self, username, output_file="mixcloud_feed.xml", extract_audio=True):
self.username = username
self.output_file = output_file
self.extract_audio = extract_audio
self.base_url = "https://api.mixcloud.com"
self.user_url = f"{self.base_url}/{username}/"
self.audio_cache = {}
self.cache_lock = threading.Lock()
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 extract_audio_url(self, mixcloud_url):
"""Extrahiert die echte Audio-URL mit yt-dlp."""
if not self.extract_audio:
return mixcloud_url
# Cache prüfen
with self.cache_lock:
if mixcloud_url in self.audio_cache:
return self.audio_cache[mixcloud_url]
try:
ydl_opts = {
'quiet': True,
'no_warnings': True,
'format': 'best[ext=m4a]/best', # Bevorzuge m4a für bessere Podcast-Kompatibilität
'extractaudio': False,
'noplaylist': True,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(mixcloud_url, download=False)
if info and 'url' in info:
audio_url = info['url']
# Cache speichern
with self.cache_lock:
self.audio_cache[mixcloud_url] = audio_url
return audio_url
else:
print(f"⚠️ Keine Audio-URL gefunden für: {mixcloud_url}")
return mixcloud_url
except Exception as e:
print(f"⚠️ Fehler beim Extrahieren der Audio-URL für {mixcloud_url}: {e}")
return mixcloud_url
def extract_audio_urls_parallel(self, cloudcasts, max_workers=3):
"""Extrahiert Audio-URLs parallel für bessere Performance."""
if not self.extract_audio:
return cloudcasts
print(f"🎵 Extrahiere Audio-URLs für {len(cloudcasts)} Tracks...")
def extract_for_cloudcast(cloudcast):
mixcloud_url = f"https://www.mixcloud.com{cloudcast.get('key', '')}"
audio_url = self.extract_audio_url(mixcloud_url)
cloudcast['audio_url'] = audio_url
return cloudcast
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_cloudcast = {
executor.submit(extract_for_cloudcast, cloudcast): cloudcast
for cloudcast in cloudcasts
}
completed_cloudcasts = []
for i, future in enumerate(as_completed(future_to_cloudcast), 1):
try:
cloudcast = future.result()
completed_cloudcasts.append(cloudcast)
print(f"{i}/{len(cloudcasts)} - {cloudcast.get('name', 'Unbekannt')}")
except Exception as e:
cloudcast = future_to_cloudcast[future]
cloudcast['audio_url'] = f"https://www.mixcloud.com{cloudcast.get('key', '')}"
completed_cloudcasts.append(cloudcast)
print(f" ⚠️ {i}/{len(cloudcasts)} - Fehler: {e}")
return completed_cloudcasts
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_content_type_and_size(self, url):
"""Ermittelt Content-Type und Dateigröße einer URL."""
try:
response = requests.head(url, timeout=10)
content_type = response.headers.get('content-type', 'audio/mpeg')
content_length = response.headers.get('content-length')
# Fallback für Content-Type basierend auf URL
if 'audio' not in content_type:
if '.m4a' in url or '.aac' in url:
content_type = 'audio/mp4'
elif '.mp3' in url:
content_type = 'audio/mpeg'
else:
content_type = 'audio/mpeg'
return content_type, content_length
except:
return 'audio/mpeg', None
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
# Audio-URLs extrahieren wenn aktiviert
if self.extract_audio:
cloudcasts = self.extract_audio_urls_parallel(cloudcasts)
# 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")
# Explicit Content (für Musik meist nicht nötig)
itunes_explicit = ET.SubElement(channel, "itunes:explicit")
itunes_explicit.text = "false"
# 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'])
print(f"📦 Erstelle RSS-Feed mit {len(cloudcasts)} Episoden...")
# 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 mit echter Audio-URL
enclosure = ET.SubElement(item, "enclosure")
audio_url = cloudcast.get('audio_url', f"https://www.mixcloud.com{cloudcast.get('key', '')}")
enclosure.set("url", audio_url)
# Content-Type und Größe ermitteln
if self.extract_audio and audio_url != f"https://www.mixcloud.com{cloudcast.get('key', '')}":
content_type, content_length = self.get_content_type_and_size(audio_url)
enclosure.set("type", content_type)
if content_length:
enclosure.set("length", content_length)
else:
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
itunes_explicit_item = ET.SubElement(item, "itunes:explicit")
itunes_explicit_item.text = "false"
# 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)}")
if self.extract_audio:
audio_count = sum(1 for c in cloudcasts if c.get('audio_url', '').startswith('http') and 'mixcloud.com' not in c.get('audio_url', ''))
print(f"🎵 Direkte Audio-URLs extrahiert: {audio_count}/{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: http://localhost:{port}/{os.path.basename(self.output_file)}")
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 Mixcloud-Tracks mit echten Audio-URLs")
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("--no-audio", action="store_true",
help="Deaktiviert die Audio-URL-Extraktion (nur Mixcloud-Links)")
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()
extract_audio = not args.no_audio
generator = MixcloudRSSGeneratorPro(args.username, args.output, extract_audio)
print(f"🎵 Erstelle RSS-Feed für Mixcloud-User: {args.username}")
if extract_audio:
print("🔧 Audio-URL-Extraktion aktiviert (kann einige Minuten dauern)")
else:
print("⚡ Schnellmodus: Nur Mixcloud-Links (keine Audio-Extraktion)")
print("-" * 60)
success = generator.create_rss_feed()
if success and args.serve:
generator.serve_feed(args.port)
if __name__ == "__main__":
main()

View File

@ -4,4 +4,8 @@ version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = []
dependencies = [
"mutagen>=1.47.0",
"requests>=2.31.0",
"yt-dlp>=2024.1.0",
]

View File

@ -1,2 +1,3 @@
requests>=2.31.0
yt-dlp>=2024.1.0
mutagen>=1.47.0

View File

@ -1,56 +0,0 @@
#!/usr/bin/env python3
"""
Optimierter Mixcloud RSS Feed Generator
Erstellt Podcast-kompatible RSS-Feeds mit direkten Audio-Links.
"""
import subprocess
import sys
import os
from pathlib import Path
def run_pro_script(*args):
"""Führt das Pro-Script mit uv run aus."""
cmd = ["uv", "run", "python", "mixcloud_rss_pro.py"] + list(args)
try:
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
print(result.stdout)
return True
except subprocess.CalledProcessError as e:
print(f"❌ Fehler: {e}")
if e.stdout:
print("STDOUT:", e.stdout)
if e.stderr:
print("STDERR:", e.stderr)
return False
except FileNotFoundError:
print("❌ uv nicht gefunden. Verwende fallback...")
return False
def main():
"""Wrapper für das Pro-Script."""
if not Path("mixcloud_rss_pro.py").exists():
print("❌ mixcloud_rss_pro.py nicht gefunden!")
sys.exit(1)
# Argumente weiterleiten
args = sys.argv[1:]
print("🚀 Starte optimierten Mixcloud RSS Generator...")
print("=" * 50)
success = run_pro_script(*args)
if not success:
print("\n⚠️ Fallback: Versuche direkten Python-Aufruf...")
try:
import mixcloud_rss_pro
# Hier könntest du das Script direkt aufrufen
print("❌ Bitte verwende: uv run python mixcloud_rss_pro.py")
except ImportError as e:
print(f"❌ Import-Fehler: {e}")
print("💡 Installiere die Abhängigkeiten: uv pip install -r requirements.txt")
if __name__ == "__main__":
main()

25
setup.sh Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
# SERMAN RSS Feed Generator - Setup Script
echo "🎵 SERMAN RSS Feed Generator - Setup"
echo "==================================="
echo ""
# Prüfe ob audio-Verzeichnis existiert
if [ ! -d "_audio" ]; then
echo "📁 Erstelle _audio/ Verzeichnis..."
mkdir _audio
fi
echo "📋 Setup abgeschlossen!"
echo ""
echo "Nächste Schritte:"
echo "1. Lege deine MP3-Dateien in das _audio/ Verzeichnis"
echo "2. Führe 'uv run python main.py' aus, um den RSS-Feed zu erstellen"
echo "3. Verwende 'uv run python main.py --serve' für lokales Testen"
echo ""
echo "💡 Beispiel-Befehle:"
echo " uv run python main.py # Feed erstellen"
echo " uv run python main.py --serve # Mit HTTP-Server"
echo " uv run python main.py --base-url https://... # Produktions-URL"
echo ""

View File

@ -1,96 +0,0 @@
#!/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([
"uv", "run", "python", "mixcloud_rss_pro.py", "serman_dj",
"--output", "mixcloud_feed.xml"
], capture_output=True, text=True)
if result.returncode == 0:
print("✅ RSS-Feed erfolgreich aktualisiert!")
print(result.stdout)
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_pro.py").exists():
print("❌ mixcloud_rss_pro.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()

View File

@ -1,96 +0,0 @@
#!/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()

120
uv.lock generated
View File

@ -2,7 +2,127 @@ version = 1
revision = 2
requires-python = ">=3.11"
[[package]]
name = "certifi"
version = "2025.6.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" },
{ url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" },
{ url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" },
{ url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" },
{ url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" },
{ url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" },
{ url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" },
{ url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" },
{ url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" },
{ url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" },
{ url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" },
{ url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" },
{ url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
{ url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
{ url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
{ url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
{ url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
{ url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
{ url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
{ url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
{ url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
{ url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
{ url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
{ url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "mutagen"
version = "1.47.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/81/e6/64bc71b74eef4b68e61eb921dcf72dabd9e4ec4af1e11891bbd312ccbb77/mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99", size = 1274186, upload-time = "2023-09-03T16:33:33.411Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/7a/620f945b96be1f6ee357d211d5bf74ab1b7fe72a9f1525aafbfe3aee6875/mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719", size = 194391, upload-time = "2023-09-03T16:33:29.955Z" },
]
[[package]]
name = "requests"
version = "2.32.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
]
[[package]]
name = "rss-feeder"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "mutagen" },
{ name = "requests" },
{ name = "yt-dlp" },
]
[package.metadata]
requires-dist = [
{ name = "mutagen", specifier = ">=1.47.0" },
{ name = "requests", specifier = ">=2.31.0" },
{ name = "yt-dlp", specifier = ">=2024.1.0" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]
[[package]]
name = "yt-dlp"
version = "2025.6.30"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/23/9c/ff64c2fed7909f43a9a0aedb7395c65404e71c2439198764685a6e3b3059/yt_dlp-2025.6.30.tar.gz", hash = "sha256:6d0ae855c0a55bfcc28dffba804ec8525b9b955d34a41191a1561a4cec03d8bd", size = 3034364, upload-time = "2025-06-30T23:58:36.605Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/41/2f048ae3f6d0fa2e59223f08ba5049dbcdac628b0a9f9deac722dd9260a5/yt_dlp-2025.6.30-py3-none-any.whl", hash = "sha256:541becc29ed7b7b3a08751c0a66da4b7f8ee95cb81066221c78e83598bc3d1f3", size = 3279333, upload-time = "2025-06-30T23:58:34.911Z" },
]