Spaces:
Sleeping
Sleeping
Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import subprocess
|
3 |
+
import os
|
4 |
+
from datetime import datetime, timedelta
|
5 |
+
from apscheduler.schedulers.background import BackgroundScheduler
|
6 |
+
import pytz # Für Zeitzonen-Management
|
7 |
+
|
8 |
+
# --- Konfiguration ---
|
9 |
+
OUTPUT_DIR = "recordings"
|
10 |
+
# Erstelle das Verzeichnis, falls es nicht existiert
|
11 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
12 |
+
|
13 |
+
# Initialisiere den Scheduler
|
14 |
+
scheduler = BackgroundScheduler()
|
15 |
+
scheduler.start()
|
16 |
+
|
17 |
+
# --- Globale Variable für laufende Prozesse (optional, aber nützlich für Stop-Funktionalität) ---
|
18 |
+
# Für dieses einfache Beispiel lassen wir das "Stoppen" über die Dauer von ffmpeg
|
19 |
+
# Wenn du eine "Sofort Stoppen"-Funktion bräuchtest, müsstest du ffmpeg-Prozesse speichern und killen.
|
20 |
+
|
21 |
+
# --- Funktionen für die Aufnahme und Planung ---
|
22 |
+
|
23 |
+
def record_radio_stream(stream_url: str, output_filename: str, duration_seconds: int):
|
24 |
+
"""
|
25 |
+
Startet die Aufnahme eines Webradio-Streams mit ffmpeg.
|
26 |
+
"""
|
27 |
+
full_output_path = os.path.join(OUTPUT_DIR, output_filename)
|
28 |
+
|
29 |
+
print(f"🎬 Starte Aufnahme von {stream_url} für {duration_seconds} Sekunden nach {full_output_path}...")
|
30 |
+
|
31 |
+
# ffmpeg Kommando:
|
32 |
+
# -i: Input URL
|
33 |
+
# -c:a copy: Audio-Codec kopieren (kein Re-Encoding), das spart CPU und Zeit
|
34 |
+
# -map 0:a: Stelle sicher, dass nur der Audio-Stream gemappt wird
|
35 |
+
# -t: Dauer der Aufnahme in Sekunden
|
36 |
+
command = [
|
37 |
+
"ffmpeg",
|
38 |
+
"-i", stream_url,
|
39 |
+
"-c:a", "copy",
|
40 |
+
"-map", "0:a",
|
41 |
+
"-t", str(duration_seconds),
|
42 |
+
full_output_path
|
43 |
+
]
|
44 |
+
|
45 |
+
try:
|
46 |
+
# Führe den ffmpeg-Befehl aus
|
47 |
+
# check=True wird eine CalledProcessError erzeugen, wenn ffmpeg fehlschlägt
|
48 |
+
subprocess.run(command, check=True, capture_output=True)
|
49 |
+
print(f"✅ Aufnahme abgeschlossen: {full_output_path}")
|
50 |
+
return full_output_path
|
51 |
+
except subprocess.CalledProcessError as e:
|
52 |
+
print(f"❌ Fehler bei der Aufnahme von {stream_url}:")
|
53 |
+
print(f"Stdout: {e.stdout.decode()}")
|
54 |
+
print(f"Stderr: {e.stderr.decode()}")
|
55 |
+
# Lösche unvollständige Datei, falls vorhanden
|
56 |
+
if os.path.exists(full_output_path):
|
57 |
+
os.remove(full_output_path)
|
58 |
+
return None # Signalisiere Fehler
|
59 |
+
|
60 |
+
def schedule_recording(stream_url: str, start_datetime_str: str, end_datetime_str: str):
|
61 |
+
"""
|
62 |
+
Plant eine Webradio-Aufnahme basierend auf Start- und Endzeit.
|
63 |
+
Die Zeiten kommen als String von Gradio und müssen geparst werden.
|
64 |
+
"""
|
65 |
+
try:
|
66 |
+
# Gradio gibt Datetime-Strings im Format 'YYYY-MM-DD HH:MM:SS' zurück
|
67 |
+
# Konvertiere in datetime-Objekte
|
68 |
+
start_datetime = datetime.fromisoformat(start_datetime_str)
|
69 |
+
end_datetime = datetime.fromisoformat(end_datetime_str)
|
70 |
+
|
71 |
+
# Optional: Zeitzone explizit setzen (z.B. UTC), wenn du sicherstellen willst, dass es unabhängig vom Server läuft
|
72 |
+
# Du kannst hier auch die Zeitzone des Nutzers abfragen, wenn es komplizierter werden soll
|
73 |
+
# Für Hugging Face Spaces ist UTC oft eine gute Wahl für den Server.
|
74 |
+
# Wenn der Nutzer aber eine lokale Zeit eingibt, muss dies berücksichtigt werden.
|
75 |
+
# Hier gehen wir davon aus, dass die eingegebene Zeit auf dem Server interpretiert wird.
|
76 |
+
|
77 |
+
# Berechne die Dauer der Aufnahme in Sekunden
|
78 |
+
duration = (end_datetime - start_datetime).total_seconds()
|
79 |
+
|
80 |
+
if duration <= 0:
|
81 |
+
return "❌ Fehler: Die Endzeit muss nach der Startzeit liegen."
|
82 |
+
|
83 |
+
# Generiere einen eindeutigen Dateinamen
|
84 |
+
timestamp = start_datetime.strftime("%Y%m%d_%H%M%S")
|
85 |
+
output_filename = f"radio_recording_{timestamp}.mp3"
|
86 |
+
|
87 |
+
# Füge den Job zum Scheduler hinzu
|
88 |
+
# 'date' trigger bedeutet, dass der Job zu einem spezifischen Datum und Uhrzeit ausgeführt wird
|
89 |
+
scheduler.add_job(
|
90 |
+
record_radio_stream,
|
91 |
+
'date',
|
92 |
+
run_date=start_datetime,
|
93 |
+
args=[stream_url, output_filename, int(duration)] # Dauer als Integer übergeben
|
94 |
+
)
|
95 |
+
|
96 |
+
# Zeige geplante Jobs an (optional, zur Fehlersuche)
|
97 |
+
# for job in scheduler.get_jobs():
|
98 |
+
# print(f"Geplanter Job: {job.id} - Nächste Ausführung: {job.next_run_time}")
|
99 |
+
|
100 |
+
return f"✅ Aufnahme von **{stream_url}** erfolgreich geplant.\nStart: **{start_datetime}** | Ende: **{end_datetime}**.\nDatei: **{output_filename}**\nBitte aktualisiere die Dateiliste, nachdem die Aufnahme abgeschlossen ist."
|
101 |
+
|
102 |
+
except ValueError as e:
|
103 |
+
return f"❌ Fehler beim Parsen der Daten/Uhrzeiten: {e}. Bitte stelle sicher, dass das Format korrekt ist."
|
104 |
+
except Exception as e:
|
105 |
+
return f"❌ Ein unerwarteter Fehler ist aufgetreten: {e}"
|
106 |
+
|
107 |
+
def get_recorded_files():
|
108 |
+
"""
|
109 |
+
Gibt eine Liste der Pfade zu allen aufgenommenen MP3-Dateien zurück.
|
110 |
+
"""
|
111 |
+
files = [os.path.join(OUTPUT_DIR, f) for f in os.listdir(OUTPUT_DIR) if f.endswith(".mp3")]
|
112 |
+
# Gradio gr.Files erwartet eine Liste von Pfaden.
|
113 |
+
# Wenn keine Dateien da sind, gibt eine leere Liste zurück.
|
114 |
+
return files
|
115 |
+
|
116 |
+
# --- Gradio UI Definition ---
|
117 |
+
|
118 |
+
with gr.Blocks() as demo:
|
119 |
+
gr.Markdown("# 📻 Webradio Recorder")
|
120 |
+
gr.Markdown(
|
121 |
+
"Plane deine Webradio-Aufnahmen! Gib die Stream-URL, Start- und Endzeit an, "
|
122 |
+
"und die App nimmt den Stream für dich auf. Die fertige Datei kannst du dann herunterladen."
|
123 |
+
)
|
124 |
+
|
125 |
+
with gr.Tab("Aufnahme planen"):
|
126 |
+
with gr.Row():
|
127 |
+
stream_url_input = gr.Textbox(
|
128 |
+
label="Stream URL",
|
129 |
+
placeholder="z.B. http://stream.laut.fm/meinstream (MP3- oder AAC-Stream)"
|
130 |
+
)
|
131 |
+
with gr.Row():
|
132 |
+
start_datetime_input = gr.Datetime(
|
133 |
+
label="Start Datum & Uhrzeit",
|
134 |
+
value=datetime.now() + timedelta(minutes=1), # Voreinstellung: 1 Minute in der Zukunft
|
135 |
+
info="Wähle das Datum und die Uhrzeit, wann die Aufnahme beginnen soll."
|
136 |
+
)
|
137 |
+
end_datetime_input = gr.Datetime(
|
138 |
+
label="End Datum & Uhrzeit",
|
139 |
+
value=datetime.now() + timedelta(minutes=10), # Voreinstellung: 10 Minuten in der Zukunft
|
140 |
+
info="Wähle das Datum und die Uhrzeit, wann die Aufnahme enden soll."
|
141 |
+
)
|
142 |
+
|
143 |
+
schedule_button = gr.Button("▶️ Aufnahme planen", variant="primary")
|
144 |
+
schedule_output_message = gr.Textbox(label="Status der Planung", interactive=False)
|
145 |
+
|
146 |
+
schedule_button.click(
|
147 |
+
fn=schedule_recording,
|
148 |
+
inputs=[stream_url_input, start_datetime_input, end_datetime_input],
|
149 |
+
outputs=schedule_output_message
|
150 |
+
)
|
151 |
+
|
152 |
+
with gr.Tab("Verfügbare Aufnahmen"):
|
153 |
+
gr.Markdown("Hier siehst du alle bisher aufgenommenen Dateien.")
|
154 |
+
|
155 |
+
refresh_files_button = gr.Button("🔄 Aufnahmen aktualisieren", variant="secondary")
|
156 |
+
# gr.Files ermöglicht das Herunterladen der Dateien
|
157 |
+
downloadable_files = gr.Files(label="Deine Aufnahmen zum Herunterladen", type="file")
|
158 |
+
|
159 |
+
# Wenn der "Aktualisieren"-Button geklickt wird, rufe die Funktion auf, um die Dateien zu listen
|
160 |
+
refresh_files_button.click(
|
161 |
+
fn=get_recorded_files,
|
162 |
+
outputs=downloadable_files
|
163 |
+
)
|
164 |
+
|
165 |
+
# Beim Laden der App auch die Dateien einmal anzeigen
|
166 |
+
demo.load(
|
167 |
+
fn=get_recorded_files,
|
168 |
+
outputs=downloadable_files
|
169 |
+
)
|
170 |
+
|
171 |
+
# --- App starten ---
|
172 |
+
if __name__ == "__main__":
|
173 |
+
demo.launch()
|