3 Commits
Author SHA1 Message Date
joel e31794462c Fix: .DS_Store Konflikt gelöst und Datei entfernt 2026-04-09 10:20:04 +02:00
joel 1ae4cff45b added requirements 2026-04-09 10:14:53 +02:00
joel 179d4bf127 added start.bat and code zo main.py 2026-04-09 10:13:35 +02:00
3 changed files with 460 additions and 0 deletions
+450
View File
@@ -0,0 +1,450 @@
import os
import json
import ctypes
import threading
import subprocess
from datetime import datetime
from tkinter import filedialog, messagebox, simpledialog, Toplevel, Button
from tkinter import *
from tkinter.ttk import Progressbar
from concurrent.futures import ThreadPoolExecutor, as_completed
try:
from mutagen.easyid3 import EasyID3
from mutagen.id3 import ID3, APIC
except ImportError:
messagebox.showerror("Fehler", "Die Bibliothek 'mutagen' ist nicht installiert. Bitte installieren Sie sie mit 'pip install mutagen'.")
exit()
SETTINGS_FILE = "settings.json"
HISTORY_FILE = "history.json"
WEEKDAY_SHORT_DE = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
DEFAULT_EXPORT_PATHS = {
"Gottesdienst": "gottesdienste",
"Hochzeit": "hochzeiten",
"Weissagung": "weissagungen",
"Jugend": "jugend"
}
def load_settings():
if os.path.exists(SETTINGS_FILE):
with open(SETTINGS_FILE, "r") as f:
return json.load(f)
return {
"export_paths": DEFAULT_EXPORT_PATHS.copy(),
"threads": 0 # 0 = auto, use all available cores
}
def save_settings(settings):
with open(SETTINGS_FILE, "w") as f:
json.dump(settings, f, indent=2)
def save_export_history(entry):
history = []
if os.path.exists(HISTORY_FILE):
with open(HISTORY_FILE, "r") as f:
history = json.load(f)
history.append(entry)
with open(HISTORY_FILE, "w") as f:
json.dump(history, f, indent=2)
# Fix pixelation on high-DPI displays
try:
ctypes.windll.shcore.SetProcessDpiAwareness(2) # Per-monitor DPI awareness
except:
try:
ctypes.windll.user32.SetProcessDPIAware() # Fallback for older Windows
except:
pass
settings = load_settings()
root = Tk()
root.iconbitmap("icon.ico")
root.title("fcco Converter")
root.geometry("800x1200")
Label(root, text="Wähle WAV-Dateien oder Ordner aus:").pack(pady=5)
file_listbox = Listbox(root, width=60)
file_listbox.pack(pady=5)
file_status_listbox = Listbox(root, width=60)
file_status_listbox.pack(pady=5)
progress_bars = [] # List to store per-file progress bars
file_entries = [] # List of dicts: {'full_path': str, 'rel_dir': str}
base_folder = None # Set when folder is selected
is_converting = False # Track conversion status
conversion_running = False # Prevent multiple conversions
def choose_files():
global file_entries, base_folder
paths = filedialog.askopenfilenames(filetypes=[("WAV-Dateien", "*.wav")])
if paths:
base_folder = None
file_entries = [{'full_path': p, 'rel_dir': ''} for p in paths]
update_file_list()
def choose_folder():
global file_entries, base_folder
folder = filedialog.askdirectory()
if folder:
base_folder = folder
file_entries = []
for root_dir, _, files in os.walk(folder):
for f in files:
if f.lower().endswith('.wav'):
full_path = os.path.join(root_dir, f)
rel_path = os.path.relpath(root_dir, folder)
file_entries.append({'full_path': full_path, 'rel_dir': rel_path})
update_file_list()
def update_file_list():
file_listbox.delete(0, END)
file_status_listbox.delete(0, END)
for bar in progress_bars:
bar.destroy()
progress_bars.clear()
for entry in file_entries:
basename = os.path.basename(entry['full_path'])
display_name = os.path.join(entry['rel_dir'], basename) if entry['rel_dir'] else basename
file_listbox.insert(END, display_name)
file_status_listbox.insert(END, f"0% {basename} Wartet...")
progress = Progressbar(root, orient=HORIZONTAL, length=400, mode='determinate')
progress.pack(pady=2)
progress_bars.append(progress)
Button(root, text="Dateien auswählen", command=choose_files).pack(pady=5)
Button(root, text="Ordner auswählen", command=choose_folder).pack(pady=5)
def generate_album_name(veranstaltung=""):
return datetime.now().strftime("%d.%m.%Y") + (f" {veranstaltung}" if veranstaltung else "")
def generate_folder_name(date_part, veranstaltung):
return f"{date_part} {veranstaltung}" if veranstaltung else date_part
def extract_event_from_folder(file_path):
folder_name = os.path.basename(os.path.dirname(file_path))
if len(folder_name) >= 8 and folder_name[:6].isdigit() and folder_name[6:8] in WEEKDAY_SHORT_DE:
return folder_name[:8], folder_name[8:].strip() if len(folder_name) > 8 else ""
return "", ""
def open_settings_window():
win = Toplevel(root)
win.iconbitmap("icon.ico")
win.title("Einstellungen")
win.geometry("500x600")
Label(win, text="Künstlername:").pack()
artist_entry = Entry(win, width=40)
artist_entry.pack()
artist_entry.insert(0, settings.get("metadata", {}).get("artist", ""))
Label(win, text="Cover Pfad:").pack()
cover_entry = Entry(win, width=40)
cover_entry.pack()
cover_entry.insert(0, settings.get("metadata", {}).get("cover", ""))
Label(win, text="Anzahl Threads (0 = Auto):").pack(pady=10)
threads_entry = Entry(win, width=40)
threads_entry.pack()
threads_entry.insert(0, str(settings.get("threads", 0)))
Label(win, text="Standardverzeichnisse für Exporte:").pack(pady=10)
entries = {}
for key in DEFAULT_EXPORT_PATHS:
Label(win, text=key).pack()
entry = Entry(win, width=40)
entry.pack()
entry.insert(0, settings.get("export_paths", {}).get(key, DEFAULT_EXPORT_PATHS[key]))
entries[key] = entry
def save_all():
try:
threads = int(threads_entry.get())
if threads < 0:
messagebox.showerror("Fehler", "Threads müssen 0 oder größer sein.")
return
except ValueError:
messagebox.showerror("Fehler", "Bitte geben Sie eine gültige Zahl für Threads ein.")
return
settings["metadata"] = {
"artist": artist_entry.get(),
"cover": cover_entry.get()
}
settings["export_paths"] = {k: v.get() for k, v in entries.items()}
settings["threads"] = threads
save_settings(settings)
messagebox.showinfo("Gespeichert", "Einstellungen wurden gespeichert.")
win.destroy()
Button(win, text="Speichern", command=save_all).pack(pady=10)
Button(root, text="Einstellungen öffnen", command=open_settings_window).pack(pady=5)
def confirm_metadata_and_type():
meta = settings.get("metadata", {}).copy()
if file_entries:
date_part, veranstaltung = extract_event_from_folder(file_entries[0]['full_path'])
if not date_part:
date_part = datetime.now().strftime("%y%m%d") + WEEKDAY_SHORT_DE[datetime.now().weekday()]
else:
date_part = datetime.now().strftime("%y%m%d") + WEEKDAY_SHORT_DE[datetime.now().weekday()]
veranstaltung = ""
meta["album"] = generate_album_name(veranstaltung)
meta["year"] = str(datetime.now().year)
win = Toplevel(root)
win.iconbitmap("icon.ico")
win.title("Metadaten und Exportoptionen")
win.geometry("650x650")
Label(win, text="Künstlername:").pack()
artist_entry = Entry(win, width=40)
artist_entry.pack()
artist_entry.insert(0, meta.get("artist", ""))
Label(win, text="Albumname:").pack()
album_entry = Entry(win, width=40)
album_entry.pack()
album_entry.insert(0, meta.get("album", ""))
Label(win, text="Jahr:").pack()
year_entry = Entry(win, width=40)
year_entry.pack()
year_entry.insert(0, meta.get("year", ""))
Label(win, text="Cover-Pfad:").pack()
cover_entry = Entry(win, width=40)
cover_entry.pack()
cover_entry.insert(0, meta.get("cover", ""))
Label(win, text="Art des Inhalts:").pack(pady=10)
content_type = StringVar(value="Gottesdienst")
for option in ["Gottesdienst", "Hochzeit", "Weissagung", "Jugend", "Sonstiges"]:
Radiobutton(win, text=option, variable=content_type, value=option).pack(anchor=W)
date_event_frame = Frame(win)
date_event_frame.pack(pady=10)
Label(date_event_frame, text="Datum:").grid(row=0, column=0, sticky="ew")
date_entry = Entry(date_event_frame, width=20)
date_entry.grid(row=1, column=0, padx=5, pady=5)
date_entry.insert(0, date_part)
Label(date_event_frame, text="Veranstaltung (optional):").grid(row=0, column=1, sticky="ew")
event_entry = Entry(date_event_frame, width=20)
event_entry.grid(row=1, column=1, padx=5, pady=5)
event_entry.insert(0, veranstaltung)
def update_album_name(*args):
event = event_entry.get().strip()
album_entry.delete(0, END)
album_entry.insert(0, generate_album_name(event))
event_entry.bind("<KeyRelease>", update_album_name)
confirmed_data = {}
def confirm():
confirmed_data["artist"] = artist_entry.get()
confirmed_data["album"] = album_entry.get()
confirmed_data["year"] = year_entry.get()
confirmed_data["cover"] = cover_entry.get()
confirmed_data["type"] = content_type.get()
confirmed_data["event"] = event_entry.get().strip()
confirmed_data["date_part"] = date_entry.get().strip()
win.destroy()
Button(win, text="Bestätigen", command=confirm).pack(pady=10)
win.grab_set()
root.wait_window(win)
return confirmed_data if confirmed_data else None
def get_duration(file):
cmd = [
"ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", file
]
try:
return float(subprocess.check_output(cmd, universal_newlines=True).strip())
except:
return 0
def convert_file(entry, index, export_dir, meta, use_subdirs):
file = entry['full_path']
rel_dir = entry['rel_dir']
try:
title = os.path.splitext(os.path.basename(file))[0]
if use_subdirs and rel_dir:
export_subdir = os.path.join(export_dir, rel_dir)
os.makedirs(export_subdir, exist_ok=True)
mp3_path = os.path.join(export_subdir, f"{title}.mp3")
else:
mp3_path = os.path.join(export_dir, f"{title}.mp3")
duration = get_duration(file)
if duration == 0:
raise Exception("Could not determine file duration.")
threads = settings.get("threads", 0)
cmd = [
"ffmpeg",
"-i", file,
"-threads", str(threads) if threads > 0 else "0",
"-c:a", "libmp3lame",
"-b:a", "192k",
"-y",
mp3_path
]
process = subprocess.Popen(cmd, stderr=subprocess.PIPE, universal_newlines=True)
while True:
line = process.stderr.readline()
if not line:
break
if "time=" in line:
time_str = line.split("time=")[1].split(" ")[0]
try:
h, m, s = map(float, time_str.split(':'))
current_time = h * 3600 + m * 60 + s
percentage = min(int((current_time / duration) * 100), 100)
root.after(0, lambda idx=index, pct=percentage, ttl=title: update_progress(idx, pct, ttl, "Konvertiert..."))
except:
pass
process.wait()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, cmd)
audiofile = EasyID3(mp3_path)
audiofile["title"] = title
audiofile["artist"] = meta["artist"] or "Unbekannt"
audiofile["album"] = meta["album"] or "Unbekanntes Album"
audiofile["tracknumber"] = str(index + 1)
audiofile["date"] = meta.get("year", "")
audiofile.save()
audiofile = ID3(mp3_path)
with open(meta["cover"], "rb") as img:
audiofile.add(APIC(
encoding=3,
mime='image/jpeg',
type=3,
desc=u'Cover',
data=img.read()
))
audiofile.save()
return (index, f"{title}.mp3", None)
except subprocess.CalledProcessError as e:
return (index, os.path.basename(file), f"FFmpeg Error: {str(e)}")
except Exception as e:
return (index, os.path.basename(file), str(e))
def update_progress(index, percentage, filename, status):
file_status_listbox.delete(index)
file_status_listbox.insert(index, f"{percentage}% {filename} {status}")
progress_bars[index]["value"] = percentage
root.update_idletasks()
def convert():
global is_converting, conversion_running
if not file_entries or conversion_running:
if not file_entries:
messagebox.showerror("Fehler", "Keine Dateien ausgewählt.")
return
conversion_running = True
result = confirm_metadata_and_type()
if not result:
conversion_running = False
return
meta = result
content_type = meta["type"]
veranstaltung = meta.get("event", "").strip()
date_part = meta.get("date_part", datetime.now().strftime("%y%m%d") + WEEKDAY_SHORT_DE[datetime.now().weekday()])
# Use subdirectories if a folder was selected
use_subdirs = (base_folder is not None)
if content_type in settings.get("export_paths", {}) and content_type != "Sonstiges":
base_dir = settings["export_paths"][content_type]
os.makedirs(base_dir, exist_ok=True)
folder_name = generate_folder_name(date_part, veranstaltung)
export_dir = os.path.join(base_dir, folder_name)
os.makedirs(export_dir, exist_ok=True)
else:
export_dir = filedialog.askdirectory(title="Export-Ordner wählen")
if not export_dir:
conversion_running = False
return
settings["last_export_dir"] = export_dir
save_settings(settings)
sorted_entries = sorted(file_entries, key=lambda x: os.path.basename(x['full_path']).lower())
total = len(sorted_entries)
file_status_listbox.delete(0, END)
for e in sorted_entries:
file_status_listbox.insert(END, f"0% {os.path.basename(e['full_path'])} Wartet...")
def run_conversion():
global is_converting, conversion_running
is_converting = True
successful_files = []
max_workers = settings.get("threads", 0) or os.cpu_count() or 1
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_file = {executor.submit(convert_file, entry, idx, export_dir, meta, use_subdirs): idx for idx, entry in enumerate(sorted_entries)}
for future in as_completed(future_to_file):
index = future_to_file[future]
result = future.result()
index, filename, error = result
if error:
update_progress(index, 100, filename, f"Fehler: {error}")
else:
update_progress(index, 100, filename, "Fertig!")
successful_files.append(filename)
save_export_history({
"datum": datetime.now().strftime("%d.%m.%Y %H:%M"),
"ordner": export_dir,
"dateien": successful_files
})
# Custom completion popup with "OK" and "Programm schließen"
win = Toplevel(root)
win.iconbitmap("icon.ico")
win.title("Konvertierung abgeschlossen")
win.geometry("300x150")
Label(win, text="Konvertierung abgeschlossen!", font=("Arial", 12)).pack(pady=20)
Button(win, text="OK", command=win.destroy, width=10).pack(side=LEFT, padx=10, pady=10)
Button(win, text="Programm schließen", command=root.destroy, width=15).pack(side=RIGHT, padx=10, pady=10)
win.grab_set()
is_converting = False
conversion_running = False
threading.Thread(target=run_conversion).start()
def on_closing():
global is_converting
if is_converting:
if messagebox.askyesno("Bestätigung", "Konvertierung läuft. Möchten Sie das Programm wirklich beenden?"):
root.destroy()
else:
root.destroy()
Button(root, text="Konvertieren", command=convert).pack(pady=10)
root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()
+4
View File
@@ -0,0 +1,4 @@
pydub
mutagen
tkinter
datetime
+6
View File
@@ -0,0 +1,6 @@
@echo off
REM Stelle sicher, dass Python 3.11 in deinem PATH ist oder gib den vollständigen Pfad an.
python3.11 main.py
REM Optional: Das Fenster bleibt offen, damit du eventuelle Fehlermeldungen sehen kannst.
pause