Dateien nach "/" hochladen

This commit is contained in:
2026-02-05 15:26:50 +01:00
parent db2fb3a5fd
commit 1d888f10e7
4 changed files with 327 additions and 0 deletions

169
Multi-Musikanteil.py Normal file
View File

@@ -0,0 +1,169 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MehrmonatsAuswertung ACRCloud → PDFReport + Druck
———————————————————————————————————————————————
• TimestampSpalte : "Timestamp(UTC+02:00)"
• DauerSpalte : "Played Duration" (Sekunden)
"""
import calendar
import os
import subprocess
import sys
import tempfile
from datetime import datetime
from pathlib import Path
from typing import List, Tuple
import pandas as pd
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
import tkinter as tk
from tkinter import filedialog, messagebox
possible_cols = [
"Timestamp(UTC+01:00)",
"Timestamp(UTC+02:00)",
]
TS_COL = next(col for col in possible_cols if col in df.columns)
DUR_COL = "Played Duration"
def musikanteil(path: Path) -> Tuple[int, int, float]:
"""liefert (Jahr, Monat, Prozent) für genau eine Datei"""
df = pd.read_excel(path)
if TS_COL not in df.columns or DUR_COL not in df.columns:
raise ValueError(f"{path.name}: Erforderliche Spalten nicht gefunden.")
df[TS_COL] = pd.to_datetime(df[TS_COL], errors="raise")
erstes = df[TS_COL].min()
jahr, monat = erstes.year, erstes.month
gesamt = df[DUR_COL].sum()
sek_monat = calendar.monthrange(jahr, monat)[1] * 86_400
proz = gesamt / sek_monat * 100
return jahr, monat, proz
def erstelle_pdf(daten: List[Tuple[int, int, float]], ausgabe: Path) -> None:
"""Erzeugt einen QuerformatPDFReport mit Tabelle."""
c = canvas.Canvas(str(ausgabe), pagesize=landscape(A4))
w, h = landscape(A4)
# Überschrift
title = "Musikanteil pro Monat (ACRCloudAuswertung)"
c.setFont("Helvetica-Bold", 16)
c.drawCentredString(w / 2, h - 25 * mm, title)
# Datum
c.setFont("Helvetica", 9)
c.drawString(15 * mm, h - 32 * mm, f"Erstellt am: {datetime.now():%d.%m.%Y %H:%M}")
# Tabellenkopf
ypos = h - 50 * mm
col_widths = [50 * mm, 50 * mm, 50 * mm]
headers = ["Monat/Jahr", "Musikanteil[%]", "Datei"]
c.setFont("Helvetica-Bold", 11)
for i, head in enumerate(headers):
c.drawString(15 * mm + sum(col_widths[:i]), ypos, head)
# Tabellenzeilen
c.setFont("Helvetica", 11)
ypos -= 8 * mm
for jahr, monat, proz, dateiname in daten:
c.drawString(15 * mm, ypos, f"{monat:02d}/{jahr}")
c.drawRightString(15 * mm + col_widths[0] + col_widths[1] - 5 * mm,
ypos, f"{proz:.2f}")
c.drawString(15 * mm + col_widths[0] + col_widths[1], ypos, dateiname)
ypos -= 7 * mm
if ypos < 20 * mm: # neue Seite
c.showPage()
ypos = h - 25 * mm
c.save()
def drucke_pdf(pfad: Path) -> None:
"""Sendet das PDF an den WindowsStandarddrucker (Acrobat bzw. Edge)."""
try:
# os.startfile mit "print" funktioniert auf Windows
os.startfile(pfad, "print")
except Exception as e:
messagebox.showerror("Druckfehler", f"Drucken fehlgeschlagen:\n{e}")
def main() -> None:
root = tk.Tk()
root.withdraw()
dateien = filedialog.askopenfilenames(
title="Mehrere ACRCloudExcelDateien wählen",
filetypes=[("ExcelDateien", "*.xlsx;*.xls")],
)
if not dateien:
return
ergebnisse = []
fehler = []
for pfad in dateien:
try:
jahr, monat, proz = musikanteil(Path(pfad))
ergebnisse.append((jahr, monat, proz, Path(pfad).name))
except Exception as exc:
fehler.append(f"{Path(pfad).name}: {exc}")
if not ergebnisse:
messagebox.showerror("Fehler", "\n".join(fehler) or "Keine gültigen Dateien.")
return
# chronologisch sortieren
ergebnisse.sort(key=lambda x: (x[0], x[1]))
# PDFDatei speichern
save_path = filedialog.asksaveasfilename(
title="PDFReport speichern unter …",
defaultextension=".pdf",
filetypes=[("PDFDatei", "*.pdf")],
initialfile="Musikanteil_Report.pdf",
)
if not save_path:
return
erstelle_pdf(ergebnisse, Path(save_path))
# Zusammenfassung anzeigen + Druckoption
text_lines = [f"{m:02d}/{j}: {p:.2f}%" for j, m, p, _ in ergebnisse]
summary = "Erfolgreich erstellt:\n" + "\n".join(text_lines)
def dialog():
win = tk.Toplevel()
win.title("Auswertung fertig")
win.geometry("320x240")
tk.Label(win, text=summary, justify="left", pady=10).pack()
frame = tk.Frame(win)
frame.pack(pady=10)
tk.Button(frame, text="PDF öffnen",
command=lambda: [os.startfile(save_path), win.destroy()]).pack(side="left", padx=5)
tk.Button(frame, text="Drucken",
command=lambda: [drucke_pdf(Path(save_path)), win.destroy()]).pack(side="left", padx=5)
tk.Button(frame, text="Schließen", command=win.destroy).pack(side="right", padx=5)
win.mainloop()
dialog()
if fehler:
messagebox.showwarning("Einige Dateien übersprungen",
"Folgende Dateien konnten nicht verarbeitet werden:\n" + "\n".join(fehler))
if __name__ == "__main__":
if sys.platform != "win32":
print("Dieses Tool ist für Windows gedacht.")
main()

BIN
sampleACRCloudDataCEST.xlsx Normal file

Binary file not shown.

BIN
sampleACRCloudDataCET.xlsx Normal file

Binary file not shown.

158
suisa-convert-acr-v4.py Normal file
View File

@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""
Format ACRCloud CSV/XLSX reports (Radio Stadtfilter) in das SUISALayout
mit korrekter Spaltenreihenfolge, automatischer Spaltenbreite,
Fonts/Ausrichtung und sauberem Zeitformat.
ÄNDERUNGEN (v0.4):
• Header jetzt fett
• Sendedauer wird als **HH:MM:SS** ausgegeben (Excel interpretiert korrekt)
für Zeiten < 1h wird 00:MM:SS geschrieben, ExcelFormat bleibt "hh:mm:ss".
Usage:
python format_radio_report.py <input.xlsx> [-o <output.xlsx>]
Abhängigkeiten:
pip install pandas openpyxl
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from typing import Optional
import pandas as pd
from openpyxl import load_workbook
from openpyxl.utils import get_column_letter
from openpyxl.styles import Alignment, Font, Border, Side
# ---------------------------------------------------------------------------
# Konfiguration
# ---------------------------------------------------------------------------
HEADER_ORDER = [
"Titel", "Komponist", "Interpret", "Sender", "Sendedatum", "Sendedauer",
"Sendezeit", "ISWC", "ISRC", "Label", "Albumtitel", "Release Date",
"Lyricists", "Creators", "UPC", "ACRID",
]
SOURCE_MAP = {
"Titel": "Title", "Komponist": "Composers", "Interpret": "Artist",
"Sender": "Stream Name", "ISWC": "ISWC", "ISRC": "ISRC", "Label": "Label",
"Albumtitel": "Album", "UPC": "UPC", "ACRID": "ACRID",
}
RIGHT_ALIGN_COLS = {"Sendedatum", "Sendedauer", "Sendezeit"}
# ---------------------------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------------------------
def _fmt_duration(seconds: Optional[float | int]) -> str:
"""Wandelt Sekunden (float|int|NaN) in **HH:MM:SS** um.
Excel interpretiert dann korrekt. Für Kurzzeiten → 00:MM:SS"""
if seconds is None or pd.isna(seconds):
seconds = 0
total = int(round(float(seconds)))
hours, rem = divmod(total, 3600)
mins, secs = divmod(rem, 60)
return f"{hours:02d}:{mins:02d}:{secs:02d}"
def _build_dataframe(src: Path) -> pd.DataFrame:
try:
df_orig = pd.read_excel(src)
except FileNotFoundError:
raise FileNotFoundError(f"Eingabedatei nicht gefunden: {src}")
df_new = pd.DataFrame()
# Einfaches Mapping
for tgt, src_col in SOURCE_MAP.items():
df_new[tgt] = df_orig.get(src_col, "")
# Datum/Zeit
ts = pd.to_datetime(df_orig.get("Timestamp(UTC+01:00)"), errors="coerce")
df_new["Sendedatum"] = ts.dt.strftime("%Y%m%d")
df_new["Sendezeit"] = ts.dt.strftime("%H:%M:%S")
# Dauer in HH:MM:SS
df_new["Sendedauer"] = df_orig.get("Played Duration", 0).apply(_fmt_duration)
# Release Date
rel = pd.to_datetime(df_orig.get("Release Date"), errors="coerce")
df_new["Release Date"] = rel.dt.strftime("%Y%m%d")
# Leerspalten
df_new["Lyricists"] = ""
df_new["Creators"] = ""
# Richtige Reihenfolge
df_new = df_new[HEADER_ORDER]
return df_new
# ---------------------------------------------------------------------------
# ExcelNachformatierung
# ---------------------------------------------------------------------------
def _autofit_and_align(xlsx: Path) -> None:
wb = load_workbook(xlsx)
ws = wb.active
# Spaltenbreiten
for col_idx, header_cell in enumerate(ws[1], 1):
letter = get_column_letter(col_idx)
max_len = len(str(header_cell.value)) if header_cell.value else 0
for cell in ws[letter][1:]: # skip header
if cell.value is not None:
max_len = 40
ws.column_dimensions[letter].width = max_len + 2
# Ausrichtung
right_cols_idx = {idx for idx, cell in enumerate(ws[1], 1) if cell.value in RIGHT_ALIGN_COLS}
for row in ws.iter_rows(min_row=2):
for cell in row:
cell.alignment = Alignment(horizontal="right" if cell.column in right_cols_idx else "left")
# Header fett, links, ohne Rahmen
no_border = Border(left=Side(border_style=None), right=Side(border_style=None),
top=Side(border_style=None), bottom=Side(border_style=None))
for cell in ws[1]:
cell.alignment = Alignment(horizontal="left")
#cell.font = Font(bold=True)
cell.border = no_border
wb.save(xlsx)
# ---------------------------------------------------------------------------
# Pipeline
# ---------------------------------------------------------------------------
def format_report(input_path: Path, output_path: Path) -> None:
df = _build_dataframe(input_path)
df.to_excel(output_path, index=False)
_autofit_and_align(output_path)
print(f"✅ Formatiertes Reporting gespeichert → {output_path}")
# ---------------------------------------------------------------------------
# CLIEntry
# ---------------------------------------------------------------------------
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Format ACRCloud XLSX für SUISA")
parser.add_argument("input", help="Pfad zur OriginalXLSX (ohne/mit .xlsx)")
parser.add_argument("-o", "--output", help="Pfad der ZielXLSX")
args = parser.parse_args()
in_path = Path(args.input)
if not in_path.exists() and in_path.suffix == "":
alt = in_path.with_suffix(".xlsx")
if alt.exists():
in_path = alt
else:
sys.exit(f"❌ Datei nicht gefunden: {in_path} (auch nicht {alt})")
elif not in_path.exists():
sys.exit(f"❌ Datei nicht gefunden: {in_path}")
out_path = Path(args.output) if args.output else in_path.with_name(in_path.stem + "_formatiert.xlsx")
format_report(in_path, out_path)