Verbessere die Cover-Optimierung durch Anpassung der Größenprüfung und -anpassung für bessere Kompatibilität mit Apple Podcasts.
This commit is contained in:
109
fix_covers.py
Normal file
109
fix_covers.py
Normal file
@ -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()
|
@ -132,36 +132,59 @@ class LocalPodcastGenerator:
|
|||||||
|
|
||||||
# Lade das Bild aus den Cover-Daten
|
# Lade das Bild aus den Cover-Daten
|
||||||
image = Image.open(io.BytesIO(metadata['cover_data']))
|
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
|
# Apple Podcasts benötigt quadratische Cover zwischen 1400-3000px
|
||||||
if width < 1400 or height < 1400:
|
# Wähle Zielgröße basierend auf Originalgröße
|
||||||
print(f" ⚠️ Cover zu klein ({width}x{height}px), skaliere auf 1400x1400px")
|
if original_width < 1400 or original_height < 1400:
|
||||||
image = image.resize((1400, 1400), Image.Resampling.LANCZOS)
|
target_size = 1400
|
||||||
elif width > 3000 or height > 3000:
|
print(f" ⚠️ Cover zu klein, skaliere auf {target_size}x{target_size}px")
|
||||||
print(f" ⚠️ Cover zu groß ({width}x{height}px), skaliere auf 3000x3000px")
|
elif original_width > 3000 or original_height > 3000:
|
||||||
image = image.resize((3000, 3000), Image.Resampling.LANCZOS)
|
target_size = 3000
|
||||||
elif width != height:
|
print(f" ⚠️ Cover zu groß, skaliere auf {target_size}x{target_size}px")
|
||||||
print(f" ⚠️ Cover nicht quadratisch ({width}x{height}px), schneide zu")
|
else:
|
||||||
size = min(width, height)
|
# Cover ist in akzeptabler Größe, mache es quadratisch
|
||||||
image = image.crop(((width-size)//2, (height-size)//2, (width+size)//2, (height+size)//2))
|
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
|
# Speichere das optimierte Cover
|
||||||
image.save(cover_path, 'JPEG', quality=95, optimize=True)
|
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:
|
except ImportError:
|
||||||
print(" ⚠️ PIL/Pillow nicht installiert - Cover wird ohne Optimierung gespeichert")
|
print(" ❌ PIL/Pillow nicht installiert - kann Cover nicht optimieren!")
|
||||||
# Fallback: Speichere Cover ohne Optimierung
|
print(" 💡 Installiere mit: uv add pillow")
|
||||||
with open(cover_path, 'wb') as f:
|
return None
|
||||||
f.write(metadata['cover_data'])
|
|
||||||
print(f" 🖼️ Cover gespeichert: {cover_filename} (unoptimiert)")
|
|
||||||
except Exception as img_error:
|
except Exception as img_error:
|
||||||
print(f" ⚠️ Fehler bei Cover-Optimierung: {img_error}")
|
print(f" ❌ Fehler bei Cover-Optimierung: {img_error}")
|
||||||
# Fallback: Speichere Cover ohne Optimierung
|
return None
|
||||||
with open(cover_path, 'wb') as f:
|
|
||||||
f.write(metadata['cover_data'])
|
|
||||||
print(f" 🖼️ Cover gespeichert: {cover_filename} (unoptimiert)")
|
|
||||||
|
|
||||||
# Rückgabe der URL zum Cover (URL-encoded für korrekte Links)
|
# Rückgabe der URL zum Cover (URL-encoded für korrekte Links)
|
||||||
cover_url = f"{self.base_url}/_audio/{urllib.parse.quote(cover_filename)}"
|
cover_url = f"{self.base_url}/_audio/{urllib.parse.quote(cover_filename)}"
|
||||||
|
Reference in New Issue
Block a user