Dateien nach "/" hochladen
This commit is contained in:
169
Multi-Musikanteil.py
Normal file
169
Multi-Musikanteil.py
Normal file
@@ -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()
|
||||
BIN
sampleACRCloudDataCEST.xlsx
Normal file
BIN
sampleACRCloudDataCEST.xlsx
Normal file
Binary file not shown.
BIN
sampleACRCloudDataCET.xlsx
Normal file
BIN
sampleACRCloudDataCET.xlsx
Normal file
Binary file not shown.
158
suisa-convert-acr-v4.py
Normal file
158
suisa-convert-acr-v4.py
Normal file
@@ -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 <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
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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)
|
||||
Reference in New Issue
Block a user