diff --git a/Multi-Musikanteil.py b/Multi-Musikanteil.py new file mode 100644 index 0000000..dd00030 --- /dev/null +++ b/Multi-Musikanteil.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Mehrmonats‑Auswertung ACRCloud → PDF‑Report + Druck +——————————————————————————————————————————————— +• Timestamp‑Spalte : "Timestamp(UTC+02:00)" +• Dauer‑Spalte : "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 Querformat‑PDF‑Report mit Tabelle.""" + c = canvas.Canvas(str(ausgabe), pagesize=landscape(A4)) + w, h = landscape(A4) + + # Überschrift + title = "Musikanteil pro Monat (ACRCloud‑Auswertung)" + 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 Windows‑Standarddrucker (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 ACRCloud‑Excel‑Dateien wählen", + filetypes=[("Excel‑Dateien", "*.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])) + + # PDF‑Datei speichern + save_path = filedialog.asksaveasfilename( + title="PDF‑Report speichern unter …", + defaultextension=".pdf", + filetypes=[("PDF‑Datei", "*.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() diff --git a/sampleACRCloudDataCEST.xlsx b/sampleACRCloudDataCEST.xlsx new file mode 100644 index 0000000..40cc71e Binary files /dev/null and b/sampleACRCloudDataCEST.xlsx differ diff --git a/sampleACRCloudDataCET.xlsx b/sampleACRCloudDataCET.xlsx new file mode 100644 index 0000000..5d73df2 Binary files /dev/null and b/sampleACRCloudDataCET.xlsx differ diff --git a/suisa-convert-acr-v4.py b/suisa-convert-acr-v4.py new file mode 100644 index 0000000..532ae05 --- /dev/null +++ b/suisa-convert-acr-v4.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +""" +Format ACRCloud CSV/XLSX reports (Radio Stadtfilter) in das SUISA‑Layout +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 < 1 h wird 00:MM:SS geschrieben, Excel‑Format bleibt "hh:mm:ss". + +Usage: + python format_radio_report.py [-o ] + +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 + +# --------------------------------------------------------------------------- +# Excel‑Nachformatierung +# --------------------------------------------------------------------------- + +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}") + +# --------------------------------------------------------------------------- +# CLI‑Entry +# --------------------------------------------------------------------------- +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Format ACRCloud XLSX für SUISA") + parser.add_argument("input", help="Pfad zur Original‑XLSX (ohne/mit .xlsx)") + parser.add_argument("-o", "--output", help="Pfad der Ziel‑XLSX") + 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)