From 6172a898c3c89e3fa9faece0821789cd0f78de6d Mon Sep 17 00:00:00 2001 From: Manuel Weiser Date: Sat, 5 Jul 2025 20:47:35 +0200 Subject: [PATCH] =?UTF-8?q?Verbessere=20die=20Cover-Optimierung=20durch=20?= =?UTF-8?q?Anpassung=20der=20Gr=C3=B6=C3=9Fenpr=C3=BCfung=20und=20-anpassu?= =?UTF-8?q?ng=20f=C3=BCr=20bessere=20Kompatibilit=C3=A4t=20mit=20Apple=20P?= =?UTF-8?q?odcasts.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fix_covers.py | 109 +++++++++++++++++++++++++++++++++++++ local_podcast_generator.py | 67 +++++++++++++++-------- 2 files changed, 154 insertions(+), 22 deletions(-) create mode 100644 fix_covers.py diff --git a/fix_covers.py b/fix_covers.py new file mode 100644 index 0000000..f70bfae --- /dev/null +++ b/fix_covers.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +""" +Cover-Optimierungs-Tool +Repariert alle Cover-Bilder im _audio-Verzeichnis für Apple Podcasts Kompatibilität. +""" + +from pathlib import Path +import urllib.parse + +def fix_all_covers(audio_dir="../httpdocs/_audio"): + """Optimiert alle Cover-Dateien im Audio-Verzeichnis.""" + try: + from PIL import Image + except ImportError: + print("❌ PIL/Pillow nicht installiert!") + print("💡 Installiere mit: uv add pillow") + return False + + audio_path = Path(audio_dir) + if not audio_path.exists(): + print(f"❌ Audio-Verzeichnis '{audio_dir}' existiert nicht!") + return False + + # Finde alle Cover-Dateien + cover_files = list(audio_path.glob("cover_*.jpg")) + print(f"🖼️ {len(cover_files)} Cover-Dateien gefunden") + + if not cover_files: + print("ℹ️ Keine Cover-Dateien zum Optimieren gefunden") + return True + + fixed_count = 0 + + for cover_file in cover_files: + try: + print(f"\n📋 Prüfe: {cover_file.name}") + + # Lade das Bild + image = Image.open(cover_file) + original_width, original_height = image.size + print(f" 📐 Aktuelle Größe: {original_width}x{original_height}px") + + # Prüfe ob Optimierung nötig ist + needs_fix = False + + if original_width < 1400 or original_height < 1400: + print(f" ⚠️ Zu klein (< 1400px)") + needs_fix = True + elif original_width > 3000 or original_height > 3000: + print(f" ⚠️ Zu groß (> 3000px)") + needs_fix = True + elif original_width != original_height: + print(f" ⚠️ Nicht quadratisch") + needs_fix = True + + if not needs_fix: + print(f" ✅ Cover ist bereits optimal") + continue + + # Bestimme Zielgröße + if original_width < 1400 or original_height < 1400: + target_size = 1400 + elif original_width > 3000 or original_height > 3000: + target_size = 3000 + else: + target_size = min(original_width, original_height) + if target_size < 1400: + target_size = 1400 + elif target_size > 3000: + target_size = 3000 + + print(f" 🎯 Zielgröße: {target_size}x{target_size}px") + + # Mache quadratisch falls nötig + if original_width != original_height: + size = min(original_width, original_height) + left = (original_width - size) // 2 + top = (original_height - size) // 2 + right = left + size + bottom = top + size + image = image.crop((left, top, right, bottom)) + print(f" ✂️ Zugeschnitten auf quadratisch") + + # Skaliere auf Zielgröße + image = image.resize((target_size, target_size), Image.Resampling.LANCZOS) + + # Konvertiere zu RGB falls nötig + if image.mode in ('RGBA', 'LA', 'P'): + background = Image.new('RGB', image.size, (255, 255, 255)) + if image.mode == 'P': + image = image.convert('RGBA') + background.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None) + image = background + + # Speichere optimiertes Cover + image.save(cover_file, 'JPEG', quality=95, optimize=True) + final_width, final_height = image.size + print(f" ✅ Optimiert: {final_width}x{final_height}px") + + fixed_count += 1 + + except Exception as e: + print(f" ❌ Fehler bei {cover_file.name}: {e}") + + print(f"\n🎉 {fixed_count} von {len(cover_files)} Cover-Dateien optimiert!") + return True + +if __name__ == "__main__": + fix_all_covers() diff --git a/local_podcast_generator.py b/local_podcast_generator.py index ca9a097..a7450b7 100644 --- a/local_podcast_generator.py +++ b/local_podcast_generator.py @@ -132,36 +132,59 @@ class LocalPodcastGenerator: # Lade das Bild aus den Cover-Daten image = Image.open(io.BytesIO(metadata['cover_data'])) - width, height = image.size + original_width, original_height = image.size + print(f" 📐 Original Cover-Größe: {original_width}x{original_height}px") # Apple Podcasts benötigt quadratische Cover zwischen 1400-3000px - if width < 1400 or height < 1400: - print(f" ⚠️ Cover zu klein ({width}x{height}px), skaliere auf 1400x1400px") - image = image.resize((1400, 1400), Image.Resampling.LANCZOS) - elif width > 3000 or height > 3000: - print(f" ⚠️ Cover zu groß ({width}x{height}px), skaliere auf 3000x3000px") - image = image.resize((3000, 3000), Image.Resampling.LANCZOS) - elif width != height: - print(f" ⚠️ Cover nicht quadratisch ({width}x{height}px), schneide zu") - size = min(width, height) - image = image.crop(((width-size)//2, (height-size)//2, (width+size)//2, (height+size)//2)) + # Wähle Zielgröße basierend auf Originalgröße + if original_width < 1400 or original_height < 1400: + target_size = 1400 + print(f" ⚠️ Cover zu klein, skaliere auf {target_size}x{target_size}px") + elif original_width > 3000 or original_height > 3000: + target_size = 3000 + print(f" ⚠️ Cover zu groß, skaliere auf {target_size}x{target_size}px") + else: + # Cover ist in akzeptabler Größe, mache es quadratisch + target_size = min(original_width, original_height) + if target_size < 1400: + target_size = 1400 + elif target_size > 3000: + target_size = 3000 + print(f" ✓ Cover-Größe OK, mache quadratisch: {target_size}x{target_size}px") + + # Mache das Bild quadratisch (schneide zu oder fülle auf) + if original_width != original_height: + size = min(original_width, original_height) + left = (original_width - size) // 2 + top = (original_height - size) // 2 + right = left + size + bottom = top + size + image = image.crop((left, top, right, bottom)) + print(f" ✂️ Bild zugeschnitten auf quadratisch: {size}x{size}px") + + # Skaliere auf Zielgröße + image = image.resize((target_size, target_size), Image.Resampling.LANCZOS) + + # Konvertiere zu RGB falls nötig (für JPEG) + if image.mode in ('RGBA', 'LA', 'P'): + background = Image.new('RGB', image.size, (255, 255, 255)) + if image.mode == 'P': + image = image.convert('RGBA') + background.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None) + image = background # Speichere das optimierte Cover image.save(cover_path, 'JPEG', quality=95, optimize=True) - print(f" 🖼️ Cover optimiert und gespeichert: {cover_filename} ({image.size[0]}x{image.size[1]}px)") + final_width, final_height = image.size + print(f" 🖼️ Cover optimiert und gespeichert: {cover_filename} ({final_width}x{final_height}px)") except ImportError: - print(" ⚠️ PIL/Pillow nicht installiert - Cover wird ohne Optimierung gespeichert") - # Fallback: Speichere Cover ohne Optimierung - with open(cover_path, 'wb') as f: - f.write(metadata['cover_data']) - print(f" 🖼️ Cover gespeichert: {cover_filename} (unoptimiert)") + print(" ❌ PIL/Pillow nicht installiert - kann Cover nicht optimieren!") + print(" 💡 Installiere mit: uv add pillow") + return None except Exception as img_error: - print(f" ⚠️ Fehler bei Cover-Optimierung: {img_error}") - # Fallback: Speichere Cover ohne Optimierung - with open(cover_path, 'wb') as f: - f.write(metadata['cover_data']) - print(f" 🖼️ Cover gespeichert: {cover_filename} (unoptimiert)") + print(f" ❌ Fehler bei Cover-Optimierung: {img_error}") + return None # Rückgabe der URL zum Cover (URL-encoded für korrekte Links) cover_url = f"{self.base_url}/_audio/{urllib.parse.quote(cover_filename)}"