""" Stáhne daily Str8ts puzzle data ze solitaire.org, uloží do MySQL, vygeneruje vlastní PDF (reportlab) a odešle emailem. """ import asyncio import json import sys sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") from datetime import date from pathlib import Path from playwright.async_api import async_playwright import os from reportlab.lib import colors from reportlab.lib.pagesizes import A4 from reportlab.lib.units import cm from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfgen.canvas import Canvas _fonts_dir = os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts") pdfmetrics.registerFont(TTFont("Arial", os.path.join(_fonts_dir, "arial.ttf"))) pdfmetrics.registerFont(TTFont("ArialBold", os.path.join(_fonts_dir, "arialbd.ttf"))) sys.path.insert(0, str(Path(__file__).parent.parent.parent / "Knihovny")) from EmailMessagingGraph import send_mail from mysql_db import connect_mysql from najdi_dropbox import get_dropbox_root OUTPUT_DIR = Path(get_dropbox_root()) / "!!!Days" / "Downloads Z230" URL = "https://www.solitaire.org/daily-str8ts/" RECIPIENT = ["vladimir.buzalka@buzalka.cz"] # TODO: vrátit alica.buzalkova@buzalka.cz EMAIL_BODY = """Str8ts — pravidla Hrací pole 9×9, každá buňka je buď bílá nebo černá. 1. Číslice 1–9 — do bílých buněk piš čísla 1–9. 2. Žádné opakování v řádku/sloupci — stejné číslo se nesmí opakovat v celém řádku ani sloupci (jako Sudoku). 3. Straights (sekvence) — bílé buňky oddělené černými tvoří skupiny. Čísla v každé skupině musí tvořit sadu po sobě jdoucích čísel (v libovolném pořadí). Např. skupinka tří buněk může obsahovat {3,4,5} nebo {7,8,9}, ale ne {1,3,5}. 4. Délka sekvence — skupina o délce n musí obsahovat právě n různých čísel jdoucích za sebou. 5. Černé buňky s číslem — někdy mají předvyplněné číslo jako nápovědu; toto číslo se nepočítá do sekvencí, ale blokuje opakování v řádku/sloupci. Rozdíl od Sudoku: nemusíš vyplnit 1–9 do každé skupiny — jen zajistit, že čísla v každé skupině tvoří „straight" (jako v pokeru). """ GRID = 9 BOARD_CM = 11 BOARD = BOARD_CM * cm CELL = BOARD / GRID DIFFICULTIES = ["easy", "medium", "hard"] # --- PDF generování --- def draw_str8ts(c: Canvas, x0: float, y0: float, puzzle: str, bw: str, title: str = ""): font_size = CELL * 0.55 if title: c.setFont("ArialBold", 12) c.drawString(x0, y0 + 5, title) for idx in range(81): row, col = divmod(idx, 9) cell_x = x0 + col * CELL cell_y = y0 - (row + 1) * CELL cx = cell_x + CELL / 2 cy = cell_y + CELL * 0.3 is_black = bw[idx] == "1" ch = puzzle[idx] if is_black: c.setFillColor(colors.black) c.rect(cell_x, cell_y, CELL, CELL, fill=1, stroke=0) if ch in "123456789": c.setFillColor(colors.yellow if is_black else colors.black) c.setFont("ArialBold", font_size) c.drawCentredString(cx, cy, ch) c.setFillColor(colors.black) for i in range(GRID + 1): c.setLineWidth(0.8) c.line(x0, y0 - i * CELL, x0 + BOARD, y0 - i * CELL) c.line(x0 + i * CELL, y0, x0 + i * CELL, y0 - BOARD) def draw_str8ts_small(c: Canvas, x0: float, y0: float, board_cm: float, solution: str, bw: str, title: str = ""): board = board_cm * cm cell = board / GRID font_size = cell * 0.55 if title: c.setFont("ArialBold", 8) c.drawString(x0, y0 + 4, title) for idx in range(81): row, col = divmod(idx, 9) cell_x = x0 + col * cell cell_y = y0 - (row + 1) * cell cx = cell_x + cell / 2 cy = cell_y + cell * 0.3 is_black = bw[idx] == "1" ch = solution[idx] if is_black: c.setFillColor(colors.black) c.rect(cell_x, cell_y, cell, cell, fill=1, stroke=0) if ch in "123456789": c.setFillColor(colors.yellow if is_black else colors.black) c.setFont("ArialBold", max(font_size, 4)) c.drawCentredString(cx, cy, ch) c.setFillColor(colors.black) for i in range(GRID + 1): c.setLineWidth(0.5) c.line(x0, y0 - i * cell, x0 + board, y0 - i * cell) c.line(x0 + i * cell, y0, x0 + i * cell, y0 - board) def generate_pdf(output_path: Path, puzzles: list[dict], title_date: str): """puzzles = [{"difficulty": ..., "puzzle": ..., "bw": ..., "solution": ...}, ...]""" page_w, page_h = A4 x0 = (page_w - BOARD) / 2 c = Canvas(str(output_path), pagesize=A4) # Stránky se zadáním — 2 puzzle nad sebou for i in range(0, len(puzzles), 2): page_puzzles = puzzles[i:i + 2] for j, p in enumerate(page_puzzles): y0 = page_h - 2 * cm - j * (BOARD + 3 * cm) draw_str8ts(c, x0, y0, p["puzzle"], p["bw"], p["difficulty"].capitalize()) c.showPage() # Poslední stránka — řešení 6×6 cm sol_cm = 6 sol_board = sol_cm * cm gap = 1.5 * cm sol_x0 = (page_w - sol_board) / 2 c.setFont("ArialBold", 14) c.drawCentredString(page_w / 2, page_h - 2 * cm, "Řešení") y_cursor = page_h - 3.5 * cm for p in puzzles: draw_str8ts_small(c, sol_x0, y_cursor, sol_cm, p["solution"], p["bw"], p["difficulty"].capitalize()) y_cursor -= sol_board + gap c.showPage() c.save() # --- MySQL --- def save_to_mysql(puzzle_date: str, puzzles: list[dict]): conn = connect_mysql(database="puzzle") cur = conn.cursor() for p in puzzles: cur.execute( "INSERT IGNORE INTO puzzles (game_type, difficulty, puzzle_date, puzzle, solution, extra, source) " "VALUES (%s, %s, %s, %s, %s, %s, %s)", ("str8ts", p["difficulty"], puzzle_date, p["puzzle"], p["solution"], json.dumps({"bw": p["bw"]}), "solitaire.org"), ) inserted = cur.rowcount cur.close() conn.close() return inserted # --- Extrakce z webu --- async def fetch_puzzles(mmdd: str) -> list[dict]: async with async_playwright() as p: browser = await p.chromium.launch(headless=True) context = await browser.new_context(viewport={"width": 1280, "height": 900}) page = await context.new_page() print(f"Načítám {URL} ...") await page.goto(URL, wait_until="networkidle", timeout=60_000) game_url = None for frame in page.frames: if frame.url != page.url and frame.url.strip() not in ("", "about:blank"): game_url = frame.url break if not game_url: iframe_src = await page.get_attribute("iframe", "src") if iframe_src: game_url = iframe_src if iframe_src.startswith("http") else f"https://www.solitaire.org{iframe_src}" await page.close() game_page = await context.new_page() target_url = game_url if game_url else URL print(f"Načítám hru: {target_url} ...") await game_page.goto(target_url, wait_until="networkidle", timeout=60_000) data = await game_page.evaluate("""(key) => { const result = {}; for (const diff of ['easy', 'medium', 'hard']) { if (gameLevels[diff] && gameLevels[diff][key]) { result[diff] = gameLevels[diff][key]; } } return result; }""", mmdd) await browser.close() puzzles = [] for diff in DIFFICULTIES: if diff in data: puzzles.append({ "difficulty": diff, "puzzle": data[diff]["puzzle"], "bw": data[diff]["bw"], "solution": data[diff]["solution"], }) return puzzles # --- Main --- async def main(): today = date.today() today_str = today.strftime("%Y-%m-%d") mmdd = today.strftime("%m-%d") output_path = OUTPUT_DIR / f"{today_str} Daily Str8ts puzzle.pdf" if output_path.exists(): print(f"Soubor již existuje: {output_path}") return puzzles = await fetch_puzzles(mmdd) if not puzzles: raise RuntimeError(f"Žádná data pro {mmdd}") print(f"Staženo {len(puzzles)} puzzle pro {today_str}") inserted = save_to_mysql(today_str, puzzles) print(f"MySQL: vloženo {inserted} nových řádků") generate_pdf(output_path, puzzles, today_str) print(f"PDF uloženo: {output_path}") send_mail( to=RECIPIENT, subject="Posílám dnešní Str8ts puzzle v příloze", body=EMAIL_BODY, attachments=output_path, ) print(f"Email odeslán na {RECIPIENT}") if __name__ == "__main__": asyncio.run(main())