Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb13fd8d2b | ||
|
|
eb222dd2ca | ||
|
|
790cdc9911 | ||
|
|
46429663d9 | ||
|
|
6f8074ff43 | ||
|
|
240779052f | ||
|
|
c2d050ed71 |
@@ -12,28 +12,41 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
|
||||||
def build():
|
def build():
|
||||||
system = platform.system()
|
system = platform.system()
|
||||||
project_dir = Path(__file__).parent
|
project_dir = Path(__file__).parent
|
||||||
|
|
||||||
|
dist_dir = project_dir / "dist"
|
||||||
|
|
||||||
|
# Alte App löschen
|
||||||
|
app_name = "fcco-Converter"
|
||||||
|
if system == "Darwin":
|
||||||
|
old_app = dist_dir / f"{app_name}.app"
|
||||||
|
else:
|
||||||
|
old_app = dist_dir / f"{app_name}.exe"
|
||||||
|
|
||||||
|
if old_app.exists():
|
||||||
|
if old_app.is_dir():
|
||||||
|
shutil.rmtree(old_app)
|
||||||
|
else:
|
||||||
|
old_app.unlink()
|
||||||
|
|
||||||
# Basis-Argumente für PyInstaller
|
# Basis-Argumente für PyInstaller
|
||||||
args = [
|
args = [
|
||||||
sys.executable,
|
sys.executable,
|
||||||
"-m", "PyInstaller",
|
"-m", "PyInstaller",
|
||||||
"--onefile",
|
"--onefile",
|
||||||
"--windowed",
|
"--windowed",
|
||||||
"--name", "fcco-Converter",
|
"--name", app_name,
|
||||||
"--clean",
|
"--clean",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Icon hinzufügen (wenn vorhanden)
|
# Icon hinzufügen (wenn vorhanden)
|
||||||
icon_path = project_dir / "icon.ico"
|
icon_path = project_dir / "icon.ico"
|
||||||
if icon_path.exists():
|
if icon_path.exists():
|
||||||
if system == "Windows":
|
args.extend(["--icon", str(icon_path)])
|
||||||
args.extend(["--icon", str(icon_path)])
|
|
||||||
elif system == "Darwin":
|
|
||||||
# macOS braucht .icns – .ico funktioniert trotzdem als Fallback
|
|
||||||
args.extend(["--icon", str(icon_path)])
|
|
||||||
|
|
||||||
# Versteckte Imports (die PyInstaller evtl. nicht automatisch findet)
|
# Versteckte Imports (die PyInstaller evtl. nicht automatisch findet)
|
||||||
hidden_imports = [
|
hidden_imports = [
|
||||||
@@ -66,8 +79,34 @@ def build():
|
|||||||
|
|
||||||
result = subprocess.run(args, cwd=str(project_dir))
|
result = subprocess.run(args, cwd=str(project_dir))
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
|
# Kopiere config.json in den dist Ordner, damit die App sie direkt findet
|
||||||
|
config_src = project_dir / "config.json"
|
||||||
|
if config_src.exists():
|
||||||
|
shutil.copy(config_src, dist_dir / "config.json")
|
||||||
|
|
||||||
|
# ZIP-Archiv für das Release erstellen
|
||||||
|
import tempfile
|
||||||
|
zip_name = f"fcco-Converter-{system.lower()}"
|
||||||
|
zip_path = dist_dir / zip_name
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
tmp = Path(tmpdir)
|
||||||
|
|
||||||
|
# App/Exe ins Temp-Verzeichnis kopieren
|
||||||
|
if system == "Darwin":
|
||||||
|
shutil.copytree(dist_dir / f"{app_name}.app", tmp / f"{app_name}.app")
|
||||||
|
else:
|
||||||
|
shutil.copy(dist_dir / f"{app_name}.exe", tmp / f"{app_name}.exe")
|
||||||
|
|
||||||
|
# config.json mit in die ZIP packen
|
||||||
|
if config_src.exists():
|
||||||
|
shutil.copy(config_src, tmp / "config.json")
|
||||||
|
|
||||||
|
shutil.make_archive(str(zip_path), 'zip', str(tmp))
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print(f"Build erfolgreich! Ausgabe in: {project_dir / 'dist'}")
|
print(f"Build erfolgreich! Ausgabe in: {dist_dir}")
|
||||||
|
print(f"ZIP für Release: {zip_path}.zip")
|
||||||
else:
|
else:
|
||||||
print()
|
print()
|
||||||
print(f"Build fehlgeschlagen (Code {result.returncode}).")
|
print(f"Build fehlgeschlagen (Code {result.returncode}).")
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"app_name": "fcco Converter",
|
"app_name": "fcco Converter",
|
||||||
"app_version": "2.0.0",
|
"app_version": "2.0.1",
|
||||||
"repo_owner": "public",
|
"repo_owner": "public",
|
||||||
"repo_name": "fcco-Converter",
|
"repo_name": "fcco-Converter",
|
||||||
"gitea_api_base": "https://git.joelunger.de/api/v1"
|
"gitea_api_base": "https://git.joelunger.de/api/v1"
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,12 @@ def _load_app_info():
|
|||||||
try:
|
try:
|
||||||
# Finde config.json relativ zum Skript oder im PyInstaller Bundle
|
# Finde config.json relativ zum Skript oder im PyInstaller Bundle
|
||||||
if getattr(sys, "frozen", False):
|
if getattr(sys, "frozen", False):
|
||||||
|
# Suche ausgehend von der Executable aufwärts nach config.json
|
||||||
base_dir = Path(sys.executable).parent
|
base_dir = Path(sys.executable).parent
|
||||||
|
while base_dir.name and str(base_dir) != base_dir.root:
|
||||||
|
if (base_dir / "config.json").exists():
|
||||||
|
break
|
||||||
|
base_dir = base_dir.parent
|
||||||
else:
|
else:
|
||||||
base_dir = Path(__file__).parent.parent
|
base_dir = Path(__file__).parent.parent
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,9 @@ def _build_ffmpeg_cmd(
|
|||||||
else:
|
else:
|
||||||
cmd.append("-n")
|
cmd.append("-n")
|
||||||
|
|
||||||
|
# Keine Video/Bilder-Streams übernehmen (verhindert Konflikte mit eigenen Covern)
|
||||||
|
cmd.append("-vn")
|
||||||
|
|
||||||
cmd.append(str(output_path))
|
cmd.append(str(output_path))
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
@@ -190,6 +193,8 @@ def convert_file_entry(
|
|||||||
audio_settings: dict | None = None,
|
audio_settings: dict | None = None,
|
||||||
threads: int = 0,
|
threads: int = 0,
|
||||||
progress_callback: Callable[[int, int, str], None] | None = None,
|
progress_callback: Callable[[int, int, str], None] | None = None,
|
||||||
|
cover_data: bytes | None = None,
|
||||||
|
cover_mime: str = "image/jpeg",
|
||||||
) -> tuple[int, str, str | None]:
|
) -> tuple[int, str, str | None]:
|
||||||
"""Konvertiert eine Datei und schreibt die Metadaten.
|
"""Konvertiert eine Datei und schreibt die Metadaten.
|
||||||
|
|
||||||
@@ -227,7 +232,6 @@ def convert_file_entry(
|
|||||||
|
|
||||||
# ID3-Metadaten nur für MP3 schreiben
|
# ID3-Metadaten nur für MP3 schreiben
|
||||||
if output_format == "mp3":
|
if output_format == "mp3":
|
||||||
cover_path = Path(meta["cover"]) if meta.get("cover") else None
|
|
||||||
apply_id3_tags(
|
apply_id3_tags(
|
||||||
mp3_path=output_file,
|
mp3_path=output_file,
|
||||||
title=title,
|
title=title,
|
||||||
@@ -235,7 +239,8 @@ def convert_file_entry(
|
|||||||
album=meta.get("album", ""),
|
album=meta.get("album", ""),
|
||||||
track_number=index + 1,
|
track_number=index + 1,
|
||||||
year=meta.get("year", ""),
|
year=meta.get("year", ""),
|
||||||
cover_path=cover_path,
|
cover_data=cover_data,
|
||||||
|
cover_mime=cover_mime,
|
||||||
)
|
)
|
||||||
|
|
||||||
return (index, f"{title}.{output_format}", None)
|
return (index, f"{title}.{output_format}", None)
|
||||||
@@ -266,6 +271,19 @@ def run_batch_conversion(
|
|||||||
successful_files = []
|
successful_files = []
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
|
# Cover-Bild einmalig in den Arbeitsspeicher laden, um Dateisperren (Locking)
|
||||||
|
# bei parallelem Zugriff durch Windows Defender/Antivirus zu vermeiden
|
||||||
|
cover_data = None
|
||||||
|
cover_mime = "image/jpeg"
|
||||||
|
cover_path_str = meta.get("cover")
|
||||||
|
if cover_path_str:
|
||||||
|
cp = Path(cover_path_str)
|
||||||
|
if cp.exists():
|
||||||
|
if cp.suffix.lower() == ".png":
|
||||||
|
cover_mime = "image/png"
|
||||||
|
with open(cp, "rb") as f:
|
||||||
|
cover_data = f.read()
|
||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
futures = {
|
futures = {
|
||||||
executor.submit(
|
executor.submit(
|
||||||
@@ -280,6 +298,8 @@ def run_batch_conversion(
|
|||||||
audio_settings=audio_settings,
|
audio_settings=audio_settings,
|
||||||
threads=0,
|
threads=0,
|
||||||
progress_callback=progress_callback,
|
progress_callback=progress_callback,
|
||||||
|
cover_data=cover_data,
|
||||||
|
cover_mime=cover_mime,
|
||||||
): idx
|
): idx
|
||||||
for idx, entry in enumerate(entries)
|
for idx, entry in enumerate(entries)
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-24
@@ -51,6 +51,8 @@ def extract_event_from_folder(file_path: Path) -> tuple[str, str]:
|
|||||||
return "", ""
|
return "", ""
|
||||||
|
|
||||||
|
|
||||||
|
from mutagen.id3 import ID3, APIC, TIT2, TPE1, TALB, TRCK, TDRC
|
||||||
|
|
||||||
def apply_id3_tags(
|
def apply_id3_tags(
|
||||||
mp3_path: Path,
|
mp3_path: Path,
|
||||||
title: str,
|
title: str,
|
||||||
@@ -58,31 +60,31 @@ def apply_id3_tags(
|
|||||||
album: str,
|
album: str,
|
||||||
track_number: int,
|
track_number: int,
|
||||||
year: str,
|
year: str,
|
||||||
cover_path: Path | None = None,
|
cover_data: bytes | None = None,
|
||||||
|
cover_mime: str = "image/jpeg",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Schreibt ID3-Metadaten und optional ein Cover in die MP3-Datei."""
|
"""Schreibt ID3-Metadaten und optional ein Cover in die MP3-Datei."""
|
||||||
audiofile = EasyID3(str(mp3_path))
|
try:
|
||||||
audiofile["title"] = title
|
tags = ID3(str(mp3_path))
|
||||||
audiofile["artist"] = artist or "Unbekannt"
|
except Exception:
|
||||||
audiofile["album"] = album or "Unbekanntes Album"
|
tags = ID3()
|
||||||
audiofile["tracknumber"] = str(track_number)
|
|
||||||
audiofile["date"] = year
|
|
||||||
audiofile.save()
|
|
||||||
|
|
||||||
if cover_path and cover_path.exists():
|
tags.add(TIT2(encoding=3, text=title))
|
||||||
audio_id3 = ID3(str(mp3_path))
|
tags.add(TPE1(encoding=3, text=artist or "Unbekannt"))
|
||||||
mime = "image/jpeg"
|
tags.add(TALB(encoding=3, text=album or "Unbekanntes Album"))
|
||||||
suffix = cover_path.suffix.lower()
|
tags.add(TRCK(encoding=3, text=str(track_number)))
|
||||||
if suffix == ".png":
|
if year:
|
||||||
mime = "image/png"
|
tags.add(TDRC(encoding=3, text=year))
|
||||||
with open(cover_path, "rb") as img:
|
|
||||||
audio_id3.add(
|
if cover_data:
|
||||||
APIC(
|
tags.add(
|
||||||
encoding=3,
|
APIC(
|
||||||
mime=mime,
|
encoding=3,
|
||||||
type=3,
|
mime=cover_mime,
|
||||||
desc="Cover",
|
type=3,
|
||||||
data=img.read(),
|
desc="Cover",
|
||||||
)
|
data=cover_data,
|
||||||
)
|
)
|
||||||
audio_id3.save()
|
)
|
||||||
|
|
||||||
|
tags.save(str(mp3_path), v2_version=3)
|
||||||
|
|||||||
+34
-24
@@ -39,36 +39,46 @@ def _fetch_json(url: str) -> list | dict:
|
|||||||
return json.loads(resp.read().decode("utf-8"))
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
def check_for_updates() -> dict | None:
|
def _parse_version(v: str) -> list[int]:
|
||||||
"""Prüft, ob ein neues Release verfügbar ist.
|
parts = []
|
||||||
|
for p in v.lstrip("vV").split("."):
|
||||||
|
try:
|
||||||
|
parts.append(int(p))
|
||||||
|
except ValueError:
|
||||||
|
parts.append(0)
|
||||||
|
while len(parts) < 3:
|
||||||
|
parts.append(0)
|
||||||
|
return parts
|
||||||
|
|
||||||
|
|
||||||
|
def check_for_updates() -> dict | None:
|
||||||
|
"""Prüft, ob auf Gitea ein neues Release (nach SemVer) vorliegt.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict mit Release-Infos (tag_name, name, body, assets) oder None.
|
Das Release-Dict (von Gitea) wenn ein Update verfügbar ist, sonst None.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
releases = _fetch_json(RELEASES_URL)
|
req = Request(RELEASES_URL, headers={'User-Agent': 'fcco-Updater/1.0'})
|
||||||
if not releases:
|
with urlopen(req, timeout=5) as resp:
|
||||||
|
data = json.loads(resp.read().decode('utf-8'))
|
||||||
|
|
||||||
|
if not data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Neuestes (nicht als Draft/Prerelease markiertes) Release
|
# Gitea gibt Releases sortiert zurück, 0 = neuestes
|
||||||
for release in releases:
|
release_info = data[0]
|
||||||
if release.get("draft") or release.get("prerelease"):
|
tag_name = release_info.get("tag_name", "")
|
||||||
continue
|
|
||||||
|
# Entferne eventuelles "v" Präfix
|
||||||
tag = release.get("tag_name", "").lstrip("v")
|
latest_version = tag_name.lstrip("vV")
|
||||||
if tag and tag != CURRENT_VERSION:
|
|
||||||
# Einfacher Versionsvergleich
|
# Intelligenter Versions-Vergleich
|
||||||
try:
|
if _parse_version(latest_version) > _parse_version(CURRENT_VERSION):
|
||||||
remote_parts = [int(x) for x in tag.split(".")]
|
return release_info
|
||||||
local_parts = [int(x) for x in CURRENT_VERSION.split(".")]
|
|
||||||
if remote_parts > local_parts:
|
|
||||||
return release
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
break # Nur das neueste Release prüfen
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
except (URLError, json.JSONDecodeError, OSError):
|
except Exception as e:
|
||||||
|
print(f"Update Check Error: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user