""" ReportReti — Flask server. """ import sys if hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(encoding='utf-8', errors='replace') import os import uuid import logging from datetime import date from pathlib import Path from flask import Flask, render_template, jsonify, request, send_from_directory logger = logging.getLogger(__name__) def _win_save_dialog(initial_dir: str, default_name: str, filter_pairs: list, title: str) -> str: """Show Windows Save As dialog via ctypes. Returns chosen path or ''.""" import ctypes from ctypes import wintypes filter_str = ''.join(f'{d}\0{p}\0' for d, p in filter_pairs) + '\0' buf = ctypes.create_unicode_buffer(default_name, 32768) class _OFN(ctypes.Structure): _fields_ = [ ('lStructSize', wintypes.DWORD), ('hwndOwner', wintypes.HWND), ('hInstance', wintypes.HINSTANCE), ('lpstrFilter', wintypes.LPCWSTR), ('lpstrCustomFilter', ctypes.c_void_p), ('nMaxCustFilter', wintypes.DWORD), ('nFilterIndex', wintypes.DWORD), ('lpstrFile', ctypes.c_void_p), # mutable buffer via addressof ('nMaxFile', wintypes.DWORD), ('lpstrFileTitle', ctypes.c_void_p), ('nMaxFileTitle', wintypes.DWORD), ('lpstrInitialDir', wintypes.LPCWSTR), ('lpstrTitle', wintypes.LPCWSTR), ('Flags', wintypes.DWORD), ('nFileOffset', wintypes.WORD), ('nFileExtension', wintypes.WORD), ('lpstrDefExt', wintypes.LPCWSTR), ('lCustData', ctypes.c_ssize_t), ('lpfnHook', ctypes.c_void_p), ('lpTemplateName', ctypes.c_void_p), ('pvReserved', ctypes.c_void_p), ('dwReserved', wintypes.DWORD), ('FlagsEx', wintypes.DWORD), ] defext = filter_pairs[0][1].lstrip('*.') ofn = _OFN() ofn.lStructSize = ctypes.sizeof(_OFN) ofn.lpstrFilter = filter_str ofn.nFilterIndex = 1 ofn.lpstrFile = ctypes.addressof(buf) ofn.nMaxFile = 32768 ofn.lpstrInitialDir = initial_dir ofn.lpstrTitle = title ofn.lpstrDefExt = defext ofn.Flags = 0x00000002 | 0x00000800 # OFN_OVERWRITEPROMPT | OFN_NOCHANGEDIR ok = ctypes.windll.comdlg32.GetSaveFileNameW(ctypes.byref(ofn)) return buf.value if ok else '' app = Flask( __name__, template_folder='frontend', static_folder='frontend', ) def _app_root() -> Path: return Path(__file__).resolve().parent.parent.parent.parent def _parquet_dir() -> Path: try: from app.config_loader import load_config cfg = load_config() return _app_root() / cfg.get('parquet_dir', 'local_db/parquet') except Exception: return _app_root() / 'local_db' / 'parquet' def _mediatrack_db() -> Path: try: from app.config_loader import get_db_path return get_db_path('mediatrack') except Exception: return _app_root() / 'sync_db' / 'mediatrack.db' def _output_dir() -> Path: d = _app_root() / 'local_db' / 'report_reti' d.mkdir(parents=True, exist_ok=True) return d def _tmp_dir() -> Path: d = _output_dir() / 'tmp' d.mkdir(parents=True, exist_ok=True) return d # ── Routes ──────────────────────────────────────────────────────────────────── @app.route('/') def index(): return render_template('index.html') @app.route('/scheda') def scheda(): return render_template('scheda.html') # ── Data API ────────────────────────────────────────────────────────────────── @app.route('/api/titoli', methods=['POST']) def api_titoli(): """Returns list of titles for the grid.""" payload = request.get_json(force=True) or {} try: date_from = date.fromisoformat(payload['date_from']) date_to = date.fromisoformat(payload['date_to']) except (KeyError, ValueError) as e: return jsonify({'ok': False, 'error': f'Date non valide: {e}'}), 400 try: from app.modules.report_reti.report_db import fetch_titles, V_LABELS data = fetch_titles(_parquet_dir(), _mediatrack_db(), date_from, date_to) rows = [] for paese, titles in data.items(): for t in titles: rows.append({ 'gemma_id': t['gemma_id'], 'titolo': t['titolo'], 'paese': paese, 'distributore':t['distributore'], 'data_gem': t['data_gem'], 'cod_imdb': t['cod_imdb'], 'regia': t.get('regia_gem', ''), 'cast': t.get('cast_gem', ''), 'tipologia': t.get('tipologia_gem', ''), 'acq_vend': t.get('acq_vend', ''), 'em_concorrenza': t.get('em_concorrenza', False), 'em': { 'rete': t.get('em_rete', ''), 'data': t.get('em_data', ''), 'ora': t.get('em_ora'), 'fascia': t.get('em_fascia', ''), 'audience': t.get('em_audience'), 'share': t.get('em_share'), }, 'has_omdb': bool(t.get('omdb')), 'val': {lbl: t.get(f'V_{lbl}', '') or '' for lbl in V_LABELS}, }) return jsonify({'ok': True, 'titoli': rows}) except Exception as e: logger.error(f'[SisterCo] api_titoli error: {e}', exc_info=True) return jsonify({'ok': False, 'error': str(e)}), 500 @app.route('/api/titolo') def api_titolo(): """Returns full data for a single title by gemma_id.""" gemma_id = request.args.get('id', '').strip() if not gemma_id: return jsonify({'ok': False, 'error': 'id mancante'}), 400 try: from app.modules.report_reti.report_db import fetch_title_by_id, V_LABELS t = fetch_title_by_id(_parquet_dir(), _mediatrack_db(), gemma_id) if t is None: return jsonify({'ok': False, 'error': 'Titolo non trovato'}), 404 omdb = t.get('omdb', {}) s = lambda x: str(x or '').strip() result = { 'gemma_id': t['gemma_id'], 'titolo': t['titolo'], 'paese': t.get('paese', ''), 'distributore':t['distributore'], 'data_gem': t['data_gem'], 'cod_imdb': t['cod_imdb'], 'regia_gem': t.get('regia_gem', ''), # OMDB fields (pre-populate form) 'anno': s(omdb.get('anno_produzione')), 'durata': s(omdb.get('durata')), 'generi': s(omdb.get('generi')), 'regia': s(omdb.get('registi')) or t.get('regia_gem', ''), 'attori': s(omdb.get('attori')), 'imdb_rating': s(omdb.get('imdb_rating')), 'premi': s(omdb.get('premi')), 'sinossi': s(omdb.get('sinossi')), 'poster_url': s(omdb.get('url_poster')), # Valutazioni 'valutazioni': { lbl: t.get(f'V_{lbl}', '') for lbl in V_LABELS }, } return jsonify({'ok': True, 'titolo': result}) except Exception as e: logger.error(f'[SisterCo] api_titolo error: {e}', exc_info=True) return jsonify({'ok': False, 'error': str(e)}), 500 # ── Upload poster ───────────────────────────────────────────────────────────── @app.route('/api/upload_poster', methods=['POST']) def api_upload_poster(): """Accepts a poster image upload, saves to tmp/, returns temp path.""" if 'file' not in request.files: return jsonify({'ok': False, 'error': 'Nessun file inviato'}), 400 f = request.files['file'] if not f.filename: return jsonify({'ok': False, 'error': 'Nome file vuoto'}), 400 ext = Path(f.filename).suffix.lower() if ext not in ('.jpg', '.jpeg', '.png', '.webp'): return jsonify({'ok': False, 'error': 'Formato non supportato (jpg/png/webp)'}), 400 tmp_name = f'{uuid.uuid4().hex}{ext}' tmp_path = _tmp_dir() / tmp_name f.save(str(tmp_path)) return jsonify({'ok': True, 'path': str(tmp_path)}) # ── Generate scheda PDF ─────────────────────────────────────────────────────── @app.route('/api/genera_scheda', methods=['POST']) def api_genera_scheda(): """Generate single-title PDF, show Windows Save As dialog.""" scheda = request.get_json(force=True) or {} if not scheda.get('titolo'): return jsonify({'ok': False, 'error': 'Titolo mancante'}), 400 safe_title = ''.join( c if c.isalnum() or c in (' ', '-', '_') else '_' for c in scheda['titolo'] )[:40].strip() default_name = f'scheda_{safe_title}.pdf' try: path = _win_save_dialog( initial_dir=str(_output_dir()), default_name=default_name, filter_pairs=[('PDF', '*.pdf'), ('Tutti i file', '*.*')], title='Salva PDF scheda', ) except Exception as e: logger.error(f'[SisterCo] genera_scheda dialog error: {e}', exc_info=True) return jsonify({'ok': False, 'error': str(e)}), 500 if not path: return jsonify({'ok': False, 'cancelled': True}) try: from app.modules.report_reti.exporter import generate_scheda_pdf generate_scheda_pdf(scheda, Path(path)) return jsonify({'ok': True, 'pdf_path': path}) except Exception as e: logger.error(f'[SisterCo] genera_scheda error: {e}', exc_info=True) return jsonify({'ok': False, 'error': str(e)}), 500 # ── Batch Excel (from index page) ───────────────────────────────────────────── @app.route('/api/genera_excel', methods=['POST']) def api_genera_excel(): payload = request.get_json(force=True) or {} try: date_from = date.fromisoformat(payload['date_from']) date_to = date.fromisoformat(payload['date_to']) except (KeyError, ValueError) as e: return jsonify({'ok': False, 'error': f'Date non valide: {e}'}), 400 try: from app.modules.report_reti.report_db import fetch_titles from app.modules.report_reti.exporter import generate_excel data = fetch_titles(_parquet_dir(), _mediatrack_db(), date_from, date_to) tag = f"{date_from.strftime('%Y%m')}_{date_to.strftime('%Y%m')}" xl_path = _output_dir() / f'report_reti_{tag}.xlsx' generate_excel(data, xl_path) stats = {p: len(t) for p, t in data.items()} return jsonify({'ok': True, 'excel_path': str(xl_path), 'stats': stats}) except Exception as e: logger.error(f'[SisterCo] genera_excel error: {e}', exc_info=True) return jsonify({'ok': False, 'error': str(e)}), 500 # ── Open file / URL ─────────────────────────────────────────────────────────── @app.route('/api/salva_scheda', methods=['POST']) def api_salva_scheda(): """Show Windows Save As dialog and save scheda JSON to chosen path.""" import json as _json data = request.get_json(force=True) or {} titolo = data.get('titolo', 'scheda') safe = ''.join(c if c.isalnum() or c in (' ', '-', '_') else '_' for c in titolo)[:40].strip() default_name = f'{safe}.scheda' try: path = _win_save_dialog( initial_dir=str(_output_dir()), default_name=default_name, filter_pairs=[('Scheda SisterCo MFE', '*.scheda'), ('Tutti i file', '*.*')], title='Salva scheda', ) except Exception as e: logger.error(f'[SisterCo] salva_scheda dialog error: {e}', exc_info=True) return jsonify({'ok': False, 'error': str(e)}), 500 if not path: return jsonify({'ok': False, 'cancelled': True}) try: Path(path).write_text(_json.dumps(data, indent=2, ensure_ascii=False), encoding='utf-8') return jsonify({'ok': True, 'path': path}) except Exception as e: logger.error(f'[SisterCo] salva_scheda write error: {e}', exc_info=True) return jsonify({'ok': False, 'error': str(e)}), 500 @app.route('/api/download') def api_download(): """Serve a file as attachment (triggers Windows Save As dialog).""" path = request.args.get('path', '').strip() if not path or not Path(path).exists(): return jsonify({'ok': False, 'error': 'File non trovato'}), 404 p = Path(path) return send_from_directory(str(p.parent), p.name, as_attachment=True) @app.route('/api/apri_url', methods=['POST']) def api_apri_url(): import webbrowser payload = request.get_json(force=True) or {} url = payload.get('url', '').strip() if not url.startswith('https://'): return jsonify({'ok': False, 'error': 'URL non valido'}), 400 webbrowser.open(url) return jsonify({'ok': True}) @app.route('/api/apri', methods=['POST']) def api_apri(): payload = request.get_json(force=True) or {} path = payload.get('path', '') if not path or not Path(path).exists(): return jsonify({'ok': False, 'error': 'File non trovato'}), 404 try: if sys.platform == 'win32': os.startfile(path) else: import subprocess subprocess.call(['open' if sys.platform == 'darwin' else 'xdg-open', path]) return jsonify({'ok': True}) except Exception as e: return jsonify({'ok': False, 'error': str(e)}), 500