آخر الأخبار

Sans titre

#!/usr/bin/env python3 """ Blogger script: generate a post about upcoming football matches from a CSV file (NO API REQUIRED). Fully free, offline, no external dependencies except optional Blogger API libs. How it works: 1. Create/edit `matches.csv` with upcoming fixtures (copy from BBC, Flashscore, etc.) 2. Run script to generate SEO-friendly HTML. 3. Paste HTML into Blogger or auto-publish. CSV format (required columns marked *): league*,datetime_local*,home_team*,away_team,round,venue,city,country,home_badge_url,away_badge_url Example `matches.csv`: league,datetime_local,home_team,away_team,round,venue,city,country EPL,2024-10-20 15:00,Arsenal,Manchester City,10,Emirates Stadium,London,England,https://example.com/arsenal.svg,https://example.com/mancity.svg La Liga,2024-10-21 20:00,Real Madrid,Barcelona,9,Santiago Bernabeu,Madrid,Spain Notes: - datetime_local: ISO format (YYYY-MM-DD HH:MM) in your display timezone (e.g., Europe/London). - Script filters by --days, --start-hour/--end-hour. - Badge URLs: Optional SVGs/PNGs from Wikimedia or official sites. - Run `--generate-sample` for a starter CSV. Usage: python football_blogger_script.py --generate-sample > matches.csv # Edit matches.csv python football_blogger_script.py --days 7 --tz Europe/London > post.html # Or publish: see Blogger notes below. pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib # Optional for --publish """ import argparse import csv import datetime as dt import email.utils import json import logging import os import sys import textwrap from dataclasses import dataclass from pathlib import Path from typing import List, Optional, Dict, Any from zoneinfo import ZoneInfo # -------------------- # Configuration dataclass # -------------------- @dataclass class Config: csv_file: str # Path to matches.csv leagues: List[str] = None # Optional: filter to these leagues only days_ahead: int = 7 # Filter to next N days start_hour_local: int = 0 # Filter to local hours (inclusive) end_hour_local: int = 23 # Filter to local hours (inclusive) timezone: str = "UTC" # IANA tz for datetime_local and display (e.g., Europe/London) title: str = "Upcoming Football Matches" tags: List[str] = None # Blogger labels blog_id: Optional[str] = None publish: bool = False output_mode: str = "print" # "print", "html", "json", "blogger" blogger_mode: str = "oauth" # "oauth" or "service-account" generate_sample: bool = False def __post_init__(self): if self.tags is None: self.tags = ["Football", "Fixtures", "Soccer"] if self.timezone not in ("UTC", "local"): try: ZoneInfo(self.timezone) except Exception: raise ValueError(f"Invalid timezone '{self.timezone}'. Use IANA name like 'Europe/London'.") # -------------------- # Badge fallbacks (Wikimedia SVGs) # -------------------- TEAM_BADGE_FALLBACKS = { "Liverpool": "https://upload.wikimedia.org/wikipedia/en/0/0c/Liverpool_FC.svg", "Man City": "https://upload.wikimedia.org/wikipedia/en/e/eb/Manchester_City_FC_badge.svg", "Arsenal": "https://upload.wikimedia.org/wikipedia/en/5/53/Arsenal_FC.svg", "Chelsea": "https://upload.wikimedia.org/wikipedia/en/c/cc/Chelsea_FC.svg", "Man United": "https://upload.wikimedia.org/wikipedia/en/7/7a/Manchester_United_FC.svg", "Real Madrid": "https://upload.wikimedia.org/wikipedia/en/5/56/Real_Madrid_CF.svg", "Barcelona": "https://upload.wikimedia.org/wikipedia/en/4/47/FC_Barcelona.svg", "Bayern Munich": "https://upload.wikimedia.org/wikipedia/en/1/1f/FC_Bayern_M%C3%BCnchen_logo_%282017%29.svg", # Add more as needed } # -------------------- # Utilities # -------------------- def is_within_local_window(dt_event: dt.datetime, start_hour: int, end_hour: int) -> bool: hour = dt_event.hour if start_hour <= end_hour: return start_hour <= hour <= end_hour return hour >= start_hour or hour <= end_hour def format_dt_for_html(dt_event: dt.datetime, tz_str: str) -> str: tz = ZoneInfo(tz_str) local_dt = dt_event.astimezone(tz) return email.utils.format_datetime(local_dt, usetz=True) def safe_text(s: Optional[str]) -> str: return (s or "").strip() def get_badge_url(team: str, provided_url: Optional[str]) -> Optional[str]: if provided_url: return provided_url return TEAM_BADGE_FALLBACKS.get(team) # -------------------- # Data model # -------------------- @dataclass class Match: league_name: str home_team: str away_team: str timestamp: Optional[dt.datetime] = None round: Optional[str] = None venue: Optional[str] = None city: Optional[str] = None country: Optional[str] = None badge_home: Optional[str] = None badge_away: Optional[str] = None # -------------------- # CSV loader # -------------------- def generate_sample_csv() -> str: """Generate a sample matches.csv template with examples.""" sample = """# Paste upcoming matches here (edit dates/teams). # Sources: BBC Sport, ESPN, Flashscore, etc. # Times in your timezone, e.g., Europe/London. league,datetime_local,home_team,away_team,round,venue,city,country,home_badge_url,away_badge_url EPL,2024-10-27 15:00,Arsenal,Liverpool,9,Emirates Stadium,London,England,https://upload.wikimedia.org/wikipedia/en/5/53/Arsenal_FC.svg,https://upload.wikimedia.org/wikipedia/en/0/0c/Liverpool_FC.svg EPL,2024-10-27 17:30,Man City,Tottenham,9,Etihad Stadium,Manchester,England La Liga,2024-10-27 20:00,Real Madrid,Barcelona,10,Santiago Bernabeu,Madrid,Spain,https://upload.wikimedia.org/wikipedia/en/5/56/Real_Madrid_CF.svg,https://upload.wikimedia.org/wikipedia/en/4/47/FC_Barcelona.svg Bundesliga,2024-10-27 16:30,Bayern Munich,Borussia Dortmund,8,Allianz Arena,Munich,Germany Champions League,2024-10-29 20:00,PSG,Arsenal,4,Parc des Princes,Paris,France """ return sample def get_matches(config: Config) -> List[Match]: csv_path = Path(config.csv_file) if not csv_path.exists(): raise FileNotFoundError(f"CSV file not found: {csv_path}. Use --generate-sample > matches.csv") tz = ZoneInfo(config.timezone) now = dt.datetime.now(tz) cutoff = now + dt.timedelta(days=config.days_ahead) matches: List[Match] = [] with csv_path.open("r", encoding="utf-8") as f: reader = csv.DictReader(f) for row_num, row in enumerate(reader, start=2): try: dt_str = safe_text(row.get("datetime_local")) if not dt_str: continue ts_naive = dt.datetime.fromisoformat(dt_str) ts = ts_naive.replace(tzinfo=tz) # Filters if ts <= now or ts > cutoff: continue if not is_within_local_window(ts, config.start_hour_local, config.end_hour_local): continue if config.leagues and row.get("league") not in config.leagues: continue badge_home = get_badge_url(row["home_team"], row.get("home_badge_url")) badge_away = get_badge_url(row["away_team"], row.get("away_badge_url")) m = Match( league_name=safe_text(row["league"]), timestamp=ts, home_team=safe_text(row["home_team"]), away_team=safe_text(row["away_team"]), round=safe_text(row.get("round")), venue=safe_text(row.get("venue")), city=safe_text(row.get("city")), country=safe_text(row.get("country")), badge_home=badge_home, badge_away=badge_away, ) matches.append(m) except (ValueError, KeyError, IndexError) as e: logging.warning(f"Skipping invalid row {row_num}: {row} ({e})") # Sort by timestamp, then league matches.sort(key=lambda m: (m.timestamp or dt.datetime.max, m.league_name)) return matches # -------------------- # HTML rendering # -------------------- def render_html_post(matches: List[Match], config: Config) -> str: tz_name = config.timezone title = config.title now_str = dt.datetime.now(ZoneInfo(tz_name)).strftime("%Y-%m-%d %H:%M:%S") css = """ """ # Group by league by_league: Dict[str, List[Match]] = {} for m in matches: by_league.setdefault(m.league_name, []).append(m) league_html = "" for league, group in sorted(by_league.items()): matches_html = "" for m in group: time_str = format_dt_for_html(m.timestamp, tz_name) if m.timestamp else "TBD" venue_str = ", ".join(filter(None, [m.venue, m.city, m.country])) badge_home_html = f'{m.home_team}' if m.badge_home else "" badge_away_html = f'{m.away_team}' if m.badge_away else "" matches_html += f"""
{badge_home_html}
{m.home_team}
VS
{m.away_team}
{badge_away_html}
{time_str}
{venue_str}{' | Round: ' + m.round if m.round else ''}
""" league_html += f'

{league}

{matches_html}
' if not league_html: league_html = '

No matches match the filters. Edit matches.csv and try again.

' html = f""" {title} {css}

{title}

Filters: Next {config.days_ahead} days, {config.start_hour_local}:00–{config.end_hour_local}:00 {tz_name}. Generated: {now_str}.

{"".join(f'{tag}' for tag in config.tags)}
{league_html}
""" return html.strip() # -------------------- # Blogger (optional) # -------------------- def get_google_auth(mode: str): from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import InstalledAppFlow from googleapiclient.discovery import build SCOPES = ["https://www.googleapis.com/auth/blogger"] token_file = os.environ.get("GOOGLE_TOKEN_FILE") or "token.json" client_secrets = os.environ.get("GOOGLE_CLIENT_SECRETS") or "client_secrets.json" creds = None if os.path.exists(token_file): creds = Credentials.from_authorized_user_file(token_file, SCOPES) if mode == "service-account": refresh_token = os.environ.get("GOOGLE_OAUTH_REFRESH_TOKEN") client_id = os.environ.get("GOOGLE_CLIENT_ID") client_secret = os.environ.get("GOOGLE_CLIENT_SECRET") if not all([refresh_token, client_id, client_secret]): raise SystemExit("Set GOOGLE_OAUTH_REFRESH_TOKEN, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET") creds = Credentials( None, refresh_token=refresh_token, token_uri="https://oauth2.googleapis.com/token", client_id=client_id, client_secret=client_secret, scopes=SCOPES ) creds.refresh(Request()) if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) else: if not os.path.exists(client_secrets): raise SystemExit(f"Create {client_secrets} from Google Cloud Console (OAuth Desktop app). Run once to authorize.") flow = InstalledAppFlow.from_client_secrets_file(client_secrets, SCOPES) creds = flow.run_local_server(port=0) with open(token_file, "w") as token: token.write(creds.to_json()) service = build("blogger", "v3", credentials=creds, cache_discovery=False) return service def publish_to_blogger(service, blog_id: str, title: str, content: str, labels: List[str]) -> Dict[str, Any]: from googleapiclient.http import set_user_agent body = {"kind": "blogger#post", "blog": {"id": blog_id}, "title": title, "content": content, "labels": labels} return service.posts().insert(blogId=blog_id, body=body).execute() # -------------------- # CLI # -------------------- def parse_args(argv: List[str]) -> Config: p = argparse.ArgumentParser(description="Generate Blogger post from matches.csv (no API needed).") p.add_argument("--csv", type=str, default="matches.csv", help="Path to matches.csv") p.add_argument("--leagues", type=str, help="Filter: comma-separated leagues (e.g., EPL,La Liga)") p.add_argument("--days", type=int, default=7, help="Next N days (default 7)") p.add_argument("--start-hour", type=int, default=12, help="Local start hour (0-23)") p.add_argument("--end-hour", type=int, default=23, help="Local end hour (0-23)") p.add_argument("--tz", type=str, default="Europe/London", help="Timezone (IANA)") p.add_argument("--title", type=str, default="Upcoming Football Matches") p.add_argument("--tags", type=str, default="Football,Fixtures") p.add_argument("--blog-id", help="Blogger blog ID (for --publish)") p.add_argument("--publish", action="store_true", help="Publish to Blogger") p.add_argument("--mode", choices=["print", "html", "json", "blogger"], default="print") p.add_argument("--blogger-mode", choices=["oauth", "service-account"], default="oauth") p.add_argument("--generate-sample", action="store_true", help="Print sample matches.csv") args = p.parse_args(argv) return Config( csv_file=args.csv, generate_sample=args.generate_sample, leagues=[l.strip() for l in (args.leagues or "").split(",") if l.strip()], days_ahead=max(1, args.days), start_hour_local=max(0, min(23, args.start_hour)), end_hour_local=max(0, min(23, args.end_hour)), timezone=args.tz, title=args.title, tags=[t.strip() for t in args.tags.split(",") if t.strip()], blog_id=args.blog_id, publish=args.publish, output_mode=args.mode if not args.publish else "blogger", blogger_mode=args.blogger_mode, ) # -------------------- # Main # -------------------- def main(argv: List[str]) -> int: config = parse_args(argv) logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") if config.generate_sample: print(generate_sample_csv()) return 0 matches = get_matches(config) logging.info("Loaded %d matches from %s", len(matches), config.csv_file) if config.output_mode == "json": data = [{"league": m.league_name, "home": m.home_team, "away": m.away_team, "datetime_local": m.timestamp.isoformat() if m.timestamp else None, "venue": m.venue, "round": m.round} for m in matches] print(json.dumps({"matches": data, "config": config.__dict__}, indent=2, ensure_ascii=False)) return 0 html = render_html_post(matches, config) if config.output_mode in ("html", "print"): if config.output_mode == "html": out_path = Path(f"fixtures_{dt.datetime.now().strftime('%Y%m%d')}.html") out_path.write_text(html, "utf-8") print(f"Saved: {out_path}") else: print(html) return 0 # Blogger if config.output_mode == "blogger": if not config.blog_id: print("Error: --blog-id required", file=sys.stderr) return 1 service = get_google_auth(config.blogger_mode) post = publish_to_blogger(service, config.blog_id, config.title, html, config.tags) print(f"Published: https://www.blogger.com/blog/post/edit?id={post['id']}") print(f"View: {post['url']}") return 0 return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv[1:]))

Enregistrer un commentaire

Plus récente Plus ancienne

نموذج الاتصال