169 lines
5.2 KiB
Python
169 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
Mehrmonats Auswertung ACRCloud PDF Report und Druck
|
||
———————————————————————————————————————————————
|
||
• Timestamp Spalten : "Timestamp(UTC+0x: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()
|