import os import tempfile import threading import zipfile from flask import Flask, render_template, jsonify, request from app import state, db, session as sess, decoder from app.importer import load_xlsx from app import exporter as exp app = Flask(__name__, template_folder="templates") app.config["JSON_SORT_KEYS"] = False @app.route("/") def index(): return render_template("index.html") @app.route("/api/records") def api_records(): data = [] for i, r in enumerate(state.records): data.append({ "idx": i, "channel": r.channel, "weekday": r.weekday, "date": r.date.strftime("%d/%m/%Y") if r.date else "", "time": r.start_time.strftime("%H:%M") if r.start_time else "", "title": r.title, "description": r.description, "duration": r.duration, "share_4plus": r.share_4plus, "rtg_4plus": r.rtg_4plus, "share_comm": r.share_comm, "rtg_comm": r.rtg_comm, "share_men16plus": r.share_men16plus, "rtg_men16plus": r.rtg_men16plus, "share_women16plus": r.share_women16plus, "rtg_women16plus": r.rtg_women16plus, "share_men1659": r.share_men1659, "rtg_men1659": r.rtg_men1659, "share_women1659": r.share_women1659, "rtg_women1659": r.rtg_women1659, "cod_oss": r.cod_oss, "agganciato": r.agganciato, "prima_tv": r.prima_tv, "tooltip": r.tooltip, "raw_data": r.raw_data, }) return jsonify(data) @app.route("/api/raw_headers") def api_raw_headers(): return jsonify(state.raw_headers) @app.route("/api/search") def api_search(): q = request.args.get("q", "") contains = request.args.get("contains", "0") == "1" if len(q) < 1: return jsonify([]) results = db.search_scheda(q, contains=contains) return jsonify(results) @app.route("/api/prima_tv/", methods=["POST"]) def api_prima_tv(idx): if idx < 0 or idx >= len(state.records): return jsonify({"error": "indice non valido"}), 400 val = request.json.get("value", "") if val not in ("S", "N", "C_notte", "C_recap", ""): return jsonify({"error": "valore non valido"}), 400 state.records[idx].prima_tv = val if val != "S": state.records[idx].agganciato = False state.records[idx].cod_oss = "" _autosave() return jsonify({"ok": True}) @app.route("/api/aggancia/", methods=["POST"]) def api_aggancia(idx): if idx < 0 or idx >= len(state.records): return jsonify({"error": "indice non valido"}), 400 rec = state.records[idx] rec.prima_tv = "S" rec.cod_oss = request.json.get("cod_oss", "") rec.tooltip = request.json.get("tooltip", "") rec.agganciato = True _autosave() return jsonify({"ok": True}) @app.route("/api/rimuovi/", methods=["POST"]) def api_rimuovi(idx): if idx < 0 or idx >= len(state.records): return jsonify({"error": "indice non valido"}), 400 rec = state.records[idx] rec.cod_oss = "" rec.agganciato = False _autosave() return jsonify({"ok": True}) @app.route("/api/importa_file", methods=["POST"]) def api_importa_file(): f = request.files.get("file") if not f: return jsonify({"error": "nessun file"}), 400 basename = f.filename or "import.xlsx" # salva in temp per il parse, poi copia stabile in data/ tmp = os.path.join(tempfile.gettempdir(), "mo_upload_" + basename) f.save(tmp) try: state.records, state.raw_headers, state.export_header_row = load_xlsx(tmp) stable_path = sess.save_source(tmp) state.source_file = stable_path state.source_basename = basename # rileva paese prima di decodificare (detect_paese cerca per nome originale) stato_paese = decoder.detect_paese(state.records) for rec in state.records: decoded, _ = decoder.decode_channel(rec.channel) rec.channel = decoded state.paese = stato_paese or (basename.split("_")[0] if "_" in basename else "") sess.save_annotations(state.records, state.source_basename, state.paese) return jsonify({"ok": True, "count": len(state.records), "filename": basename}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: try: os.unlink(tmp) except OSError: pass @app.route("/api/db_status") def api_db_status(): return jsonify({"sync_ok": state.db_sync_ok}) @app.route("/api/session/status") def api_session_status(): session = sess.load_session() if not session: return jsonify({"exists": False}) return jsonify({ "exists": True, "filename": session.get("source_basename", ""), "saved_at": session.get("saved_at", ""), "count": len(session.get("annotations", [])), }) @app.route("/api/session/restore", methods=["POST"]) def api_session_restore(): session = sess.load_session() if not session: return jsonify({"error": "Nessuna sessione salvata"}), 404 try: state.records, state.raw_headers, state.export_header_row = load_xlsx(sess.source_path()) state.source_file = sess.source_path() state.source_basename = session["source_basename"] state.paese = session.get("paese", "") for rec in state.records: rec.channel, _ = decoder.decode_channel(rec.channel) sess.apply_annotations(state.records, session) return jsonify({ "ok": True, "count": len(state.records), "filename": state.source_basename, "saved_at": session.get("saved_at", ""), }) except Exception as e: return jsonify({"error": str(e)}), 500 def _create_backup_zip(export_dir: str, sidecar_filename: str, sidecar_path: str): from loguru import logger try: stem = sidecar_filename.rsplit(".", 1)[0] zip_path = os.path.join(export_dir, stem + "_backup.zip") with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: zf.write(sidecar_path, sidecar_filename) src = sess.source_path() if os.path.exists(src): zf.write(src, "sorgente_" + os.path.basename(src)) for i in range(1, 4): bak = sess._backup_file(i) if os.path.exists(bak): zf.write(bak, f"session_backup_{i}.json") logger.info("Backup ZIP creato: {}", zip_path) except Exception as e: logger.warning("Backup ZIP fallito: {}", e) @app.route("/api/esporta", methods=["POST"]) def api_esporta(): if not state.source_file: return jsonify({"error": "Nessun file caricato."}), 400 try: data, filename = exp.export_xlsx_sidecar( state.records, state.source_file, state.source_basename, header_row=state.export_header_row, ) export_dir = sess.export_dir() export_path = os.path.join(export_dir, filename) with open(export_path, "wb") as f: f.write(data) threading.Thread( target=_create_backup_zip, args=(export_dir, filename, export_path), daemon=True, ).start() return jsonify({"ok": True, "path": export_path, "filename": filename}) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/utenti") def api_utenti(): return jsonify(sess.list_users()) @app.route("/api/session/import_json", methods=["POST"]) def api_session_import_json(): import json as _json f = request.files.get("file") if not f: return jsonify({"error": "nessun file"}), 400 try: session = _json.loads(f.read().decode("utf-8")) except Exception as e: return jsonify({"error": f"File non valido: {e}"}), 400 username = session.get("username", "") if not username: return jsonify({"error": "Campo 'username' non trovato nel file sessione"}), 400 src_path = sess.source_path_for_user(username) if not src_path: return jsonify({"error": f"File dati non trovato per l'utente '{username}'"}), 404 try: state.records, state.raw_headers, state.export_header_row = load_xlsx(src_path) state.source_basename = session["source_basename"] state.paese = session.get("paese", "") for rec in state.records: rec.channel, _ = decoder.decode_channel(rec.channel) sess.apply_annotations(state.records, session) stable_path = sess.save_source(src_path) state.source_file = stable_path sess.save_annotations(state.records, state.source_basename, state.paese) return jsonify({ "ok": True, "count": len(state.records), "filename": state.source_basename, "saved_at": session.get("saved_at", ""), "from_user": username, }) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/session/restore_from/", methods=["POST"]) def api_session_restore_from(username): session, src_path = sess.load_session_from_user(username) if not session or not src_path: return jsonify({"error": f"Nessuna sessione attiva per {username}"}), 404 try: state.records, state.raw_headers, state.export_header_row = load_xlsx(src_path) state.source_basename = session["source_basename"] state.paese = session.get("paese", "") for rec in state.records: rec.channel, _ = decoder.decode_channel(rec.channel) sess.apply_annotations(state.records, session) stable_path = sess.save_source(src_path) state.source_file = stable_path sess.save_annotations(state.records, state.source_basename, state.paese) return jsonify({ "ok": True, "count": len(state.records), "filename": state.source_basename, "saved_at": session.get("saved_at", ""), "from_user": username, }) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/session/clear", methods=["POST"]) def api_session_clear(): state.records = [] state.source_file = "" state.source_basename = "" state.paese = "" state.raw_headers = [] sess.clear_session() return jsonify({"ok": True}) def _autosave(): if state.records and state.source_basename: records, basename, paese = state.records, state.source_basename, state.paese threading.Thread( target=lambda: sess.save_annotations(records, basename, paese), daemon=True, ).start() @app.route("/api/reti") def api_reti(): return jsonify(decoder.get_table()) @app.route("/api/reti", methods=["POST"]) def api_reti_add(): d = request.json or {} paese = d.get("paese", "").strip().upper() originale = d.get("originale", "").strip() decodificato = d.get("decodificato", "").strip() if not paese or not originale or not decodificato: return jsonify({"error": "Compilare tutti i campi"}), 400 try: decoder.add_row(paese, originale, decodificato) return jsonify({"ok": True}) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/reti", methods=["PUT"]) def api_reti_save(): rows = request.json if not isinstance(rows, list): return jsonify({"error": "Formato non valido"}), 400 try: decoder.save_table(rows) return jsonify({"ok": True, "count": len(decoder.get_table())}) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/reti/", methods=["DELETE"]) def api_reti_delete(idx): try: decoder.delete_row(idx) return jsonify({"ok": True}) except Exception as e: return jsonify({"error": str(e)}), 500