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:
152
README.md
152
README.md
@ -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
|
## Installation
|
||||||
|
|
||||||
1. Abhängigkeiten installieren:
|
1. Abhängigkeiten installieren:
|
||||||
|
```bash
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
2. MP3-Dateien in das `_audio/` Verzeichnis legen
|
||||||
uv pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verwendung
|
## Verwendung
|
||||||
|
|
||||||
### RSS-Feed mit echten Audio-URLs erstellen (empfohlen)
|
### Einfache Nutzung
|
||||||
|
|
||||||
```bash
|
```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.
|
### Mit HTTP-Server
|
||||||
|
|
||||||
### Original-Version (nur Mixcloud-Links)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python mixcloud_rss.py serman_dj
|
uv run python main.py --serve
|
||||||
```
|
```
|
||||||
|
|
||||||
### Erweiterte Optionen
|
### Erweiterte Optionen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Feed mit 100 Tracks erstellen (mit Audio-URLs)
|
uv run python main.py --audio-dir _audio --output my_podcast.xml --base-url https://meinserver.de --serve --port 8080
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### RSS-Feed in Podcast-App hinzufügen
|
## Parameter
|
||||||
|
|
||||||
1. **Mit HTTP-Server (empfohlen):**
|
- `--audio-dir`: Verzeichnis mit MP3-Dateien (Standard: `_audio`)
|
||||||
- Starte den Server: `python mixcloud_rss.py serman_dj --serve`
|
- `--output`: Name der RSS-Feed-Datei (Standard: `serman_podcast.xml`)
|
||||||
- Füge diese URL in deiner Podcast-App hinzu: `http://localhost:8000/mixcloud_feed.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:**
|
## RSS-Feed URLs
|
||||||
- Lade die generierte XML-Datei auf einen Webserver hoch
|
|
||||||
- Verwende die öffentliche URL in deiner Podcast-App
|
|
||||||
|
|
||||||
## 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
|
## Podcast-App Integration
|
||||||
- ✅ 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
|
|
||||||
|
|
||||||
## 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
|
Das Tool liest automatisch folgende ID3-Tags:
|
||||||
- ✅ Korrekte Content-Types für Podcast-Apps
|
- **TIT2**: Titel (Fallback: Dateiname)
|
||||||
- ✅ Funktioniert mit Apple Podcasts, Spotify, etc.
|
- **TPE1**: Künstler (Fallback: "SERMAN")
|
||||||
|
- **TALB**: Album (optional)
|
||||||
|
- **Dauer**: Automatisch erkannt
|
||||||
|
|
||||||
## Bekannte Einschränkungen
|
## Verzeichnisstruktur
|
||||||
|
|
||||||
~~**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
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Erstelle RSS-Feed für Mixcloud-User: serman_dj
|
rss-feeder/
|
||||||
RSS-Feed erfolgreich erstellt: mixcloud_feed.xml
|
├── _audio/ # MP3-Dateien hier ablegen
|
||||||
Anzahl der Episoden: 50
|
│ ├── 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
|
## 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
|
```bash
|
||||||
# Crontab-Eintrag für tägliche Updates um 6:00 Uhr
|
# Beispiel-Script für cron job
|
||||||
0 6 * * * cd /pfad/zu/rss-feeder && python mixcloud_rss.py serman_dj
|
#!/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
29
_audio/README.md
Normal 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
307
local_podcast_generator.py
Normal 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
68
main.py
@ -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():
|
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
sys.exit(main())
|
||||||
|
240
mixcloud_rss.py
240
mixcloud_rss.py
@ -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()
|
|
@ -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()
|
|
@ -4,4 +4,8 @@ version = "0.1.0"
|
|||||||
description = "Add your description here"
|
description = "Add your description here"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = []
|
dependencies = [
|
||||||
|
"mutagen>=1.47.0",
|
||||||
|
"requests>=2.31.0",
|
||||||
|
"yt-dlp>=2024.1.0",
|
||||||
|
]
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
requests>=2.31.0
|
requests>=2.31.0
|
||||||
yt-dlp>=2024.1.0
|
yt-dlp>=2024.1.0
|
||||||
|
mutagen>=1.47.0
|
||||||
|
@ -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
25
setup.sh
Executable 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 ""
|
@ -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()
|
|
@ -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
120
uv.lock
generated
@ -2,7 +2,127 @@ version = 1
|
|||||||
revision = 2
|
revision = 2
|
||||||
requires-python = ">=3.11"
|
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]]
|
[[package]]
|
||||||
name = "rss-feeder"
|
name = "rss-feeder"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
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" },
|
||||||
|
]
|
||||||
|
Reference in New Issue
Block a user