From 090cf7d92e8e660b1dccf8d64780bd2430660cbc Mon Sep 17 00:00:00 2001 From: yyamashita Date: Mon, 15 Jun 2026 23:25:39 +0900 Subject: Add official-site-2023-dev as git submodule at official-site/ - Move management scripts from official-site/ to official-site-scripts/ - Update CLAUDE.md paths accordingly - Deploy key configured at ~/.ssh/mosquitone_official_site_deploy with SSH alias github-mosquitone-official-site Co-Authored-By: Claude Sonnet 4.6 --- .gitmodules | 3 + CLAUDE.md | 22 +- official-site | 1 + official-site-scripts/add-event.sh | 47 ++++ official-site-scripts/deploy.sh | 41 +++ official-site-scripts/enrich-event.py | 305 +++++++++++++++++++++ .../events/2026-06-27-cre4m-sod4.json | 17 ++ official-site-scripts/list-events.sh | 25 ++ official-site/add-event.sh | 47 ---- official-site/deploy.sh | 41 --- official-site/enrich-event.py | 305 --------------------- official-site/events/2026-06-27-cre4m-sod4.json | 17 -- official-site/list-events.sh | 25 -- 13 files changed, 450 insertions(+), 446 deletions(-) create mode 100644 .gitmodules create mode 160000 official-site create mode 100755 official-site-scripts/add-event.sh create mode 100755 official-site-scripts/deploy.sh create mode 100755 official-site-scripts/enrich-event.py create mode 100644 official-site-scripts/events/2026-06-27-cre4m-sod4.json create mode 100755 official-site-scripts/list-events.sh delete mode 100755 official-site/add-event.sh delete mode 100755 official-site/deploy.sh delete mode 100755 official-site/enrich-event.py delete mode 100644 official-site/events/2026-06-27-cre4m-sod4.json delete mode 100755 official-site/list-events.sh diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b54b732 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "official-site"] + path = official-site + url = git@github-mosquitone-official-site:mosquitone/official-site-2023-dev.git diff --git a/CLAUDE.md b/CLAUDE.md index 81782dc..fc843f0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,7 +51,7 @@ golive に登録されていない会場は `enrich-event.py` が `detailURL` ```bash # events/ ディレクトリに JSON ファイルを作成 # ファイル名: YYYY-MM-DD-.json -cp official-site/events/2026-06-27-cre4m-sod4.json official-site/events/.json +cp official-site-scripts/events/2026-06-27-cre4m-sod4.json official-site-scripts/events/.json # 内容を編集 ``` @@ -66,7 +66,7 @@ cp official-site/events/2026-06-27-cre4m-sod4.json official-site/events/.json +bash official-site-scripts/add-event.sh official-site-scripts/events/.json ``` 返ってきた `id` を控えておく。 @@ -75,7 +75,7 @@ bash official-site/add-event.sh official-site/events/.json ```bash # サーバー上で実行(golive SQLite DB に直接アクセスできる環境) -python3 official-site/enrich-event.py +python3 official-site-scripts/enrich-event.py ``` エンリッチが自動で行うこと: @@ -91,19 +91,19 @@ golive にまだデータがない場合(スクレイプが古い等)は先 curl -H "X-Crawler-Access: f9a3c1e847b2d056" \ "https://golive.yyamashita.com/api/scrape?venue_id=navey-floor" # 完了を待ってから再実行 -python3 official-site/enrich-event.py +python3 official-site-scripts/enrich-event.py ``` 未エンリッチのイベントをまとめて処理する場合: ```bash -python3 official-site/enrich-event.py --all-unenriched +python3 official-site-scripts/enrich-event.py --all-unenriched ``` ### ステップ 3: サイトをデプロイ ```bash -bash official-site/deploy.sh +bash official-site-scripts/deploy.sh ``` Amplify が `main` ブランチを再ビルドし、https://www.mosquit.one に反映される。完了まで約 5〜10 分。 @@ -114,12 +114,12 @@ Amplify が `main` ブランチを再ビルドし、https://www.mosquit.one に ```bash # ライブ一覧(公開フラグ付き) -bash official-site/list-events.sh +bash official-site-scripts/list-events.sh # イベント追加 → エンリッチ → デプロイ(一連の流れ) -bash official-site/add-event.sh official-site/events/.json -python3 official-site/enrich-event.py -bash official-site/deploy.sh +bash official-site-scripts/add-event.sh official-site-scripts/events/.json +python3 official-site-scripts/enrich-event.py +bash official-site-scripts/deploy.sh ``` ## AWS 認証 @@ -135,7 +135,7 @@ AWS_DEFAULT_REGION=ap-northeast-1 `aws` コマンドが未インストールの場合は `/tmp/awscli/aws/dist/aws` を使用: ```bash -AWS_CLI=/tmp/awscli/aws/dist/aws python3 official-site/enrich-event.py +AWS_CLI=/tmp/awscli/aws/dist/aws python3 official-site-scripts/enrich-event.py ``` ## AppSync Event スキーマ diff --git a/official-site b/official-site new file mode 160000 index 0000000..bb6b424 --- /dev/null +++ b/official-site @@ -0,0 +1 @@ +Subproject commit bb6b424e91001ba6f9309162af90613a02299291 diff --git a/official-site-scripts/add-event.sh b/official-site-scripts/add-event.sh new file mode 100755 index 0000000..109c2c8 --- /dev/null +++ b/official-site-scripts/add-event.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Usage: bash add-event.sh +# The JSON file should match the Event schema (see CLAUDE.md). +# id, createdAt, updatedAt, _version, _deleted, _lastChangedAt are auto-managed. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +[ -f "$REPO_ROOT/.env" ] && set -a && source "$REPO_ROOT/.env" && set +a + +ENDPOINT="${APP_SYNC_ENDPOINT:?APP_SYNC_ENDPOINT not set}" +API_KEY="${APP_SYNC_API_KEY:?APP_SYNC_API_KEY not set}" + +JSON_FILE="${1:-}" +if [ -z "$JSON_FILE" ]; then + echo "Usage: $0 " + exit 1 +fi + +INPUT=$(python3 -c " +import json, sys +with open('$JSON_FILE') as f: + data = json.load(f) +# Remove read-only / auto-managed fields +for k in ['id', 'createdAt', 'updatedAt', '_version', '_deleted', '_lastChangedAt', '_note']: + data.pop(k, None) +# Remove null fields (optional) +data = {k: v for k, v in data.items() if v is not None} +print(json.dumps(data)) +") + +QUERY='mutation CreateEvent($input: CreateEventInput!) { createEvent(input: $input) { id type date title venueName published } }' +PAYLOAD=$(python3 -c " +import json, sys +q = '$QUERY' +inp = $INPUT +print(json.dumps({'query': q, 'variables': {'input': inp}})) +") + +echo "Creating event from $JSON_FILE ..." +RESULT=$(curl -s -X POST "$ENDPOINT" \ + -H "Content-Type: application/json" \ + -H "x-api-key: $API_KEY" \ + -d "$PAYLOAD") + +echo "$RESULT" | python3 -m json.tool +echo "" +echo "Done. Run 'bash deploy.sh' to rebuild the site." diff --git a/official-site-scripts/deploy.sh b/official-site-scripts/deploy.sh new file mode 100755 index 0000000..32574e0 --- /dev/null +++ b/official-site-scripts/deploy.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +[ -f "$REPO_ROOT/.env" ] && set -a && source "$REPO_ROOT/.env" && set +a + +AWS_CLI="${AWS_CLI:-/tmp/awscli/aws/dist/aws}" +APP_ID="d33773t2lyrxkx" +BRANCH="main" +REGION="${AWS_DEFAULT_REGION:-ap-northeast-1}" + +echo "Starting Amplify build: app=$APP_ID branch=$BRANCH" +JOB=$("$AWS_CLI" amplify start-job \ + --app-id "$APP_ID" \ + --branch-name "$BRANCH" \ + --job-type RELEASE \ + --region "$REGION" \ + --output json) + +JOB_ID=$(echo "$JOB" | python3 -c "import json,sys; print(json.load(sys.stdin)['jobSummary']['jobId'])") +echo "Job started: $JOB_ID" +echo "Console: https://ap-northeast-1.console.aws.amazon.com/amplify/apps/$APP_ID/branches/$BRANCH/deployments/$JOB_ID" + +echo "Waiting for build to complete..." +while true; do + STATUS=$("$AWS_CLI" amplify get-job \ + --app-id "$APP_ID" \ + --branch-name "$BRANCH" \ + --job-id "$JOB_ID" \ + --region "$REGION" \ + --output json | python3 -c "import json,sys; print(json.load(sys.stdin)['job']['summary']['status'])") + echo " Status: $STATUS" + if [[ "$STATUS" == "SUCCEED" ]]; then + echo "Build succeeded! https://www.mosquit.one" + break + elif [[ "$STATUS" == "FAILED" || "$STATUS" == "CANCELLED" ]]; then + echo "Build $STATUS" + exit 1 + fi + sleep 15 +done diff --git a/official-site-scripts/enrich-event.py b/official-site-scripts/enrich-event.py new file mode 100755 index 0000000..846a61e --- /dev/null +++ b/official-site-scripts/enrich-event.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python3 +""" +Enrich an AppSync Event with details from golive DB and venue website. + +Usage: + python3 enrich-event.py + python3 enrich-event.py --all-unenriched # process events missing ticketURL/detailURL + +Sources (tried in order): + 1. golive SQLite DB (/app/tokyo-livehouse-events/data/events.db) + 2. Venue website (direct fetch) + +What gets filled in: + ticketURL ← golive ticket_url + detailURL ← golive source_url + ticketFee ← golive price (if current value looks incomplete) + description ← "OPEN HH:MM / START HH:MM\\n出演: " + mainImageURL← first non-logo from the venue event page, uploaded to S3 +""" + +import json +import os +import re +import sqlite3 +import sys +import tempfile +import urllib.request +from pathlib import Path + +# ── config ────────────────────────────────────────────────────────────────── +REPO_ROOT = Path(__file__).parent.parent +ENV_FILE = REPO_ROOT / ".env" + +# Load .env early so os.environ is populated before constants are evaluated +if ENV_FILE.exists(): + for _line in ENV_FILE.read_text().splitlines(): + _line = _line.strip() + if _line and not _line.startswith("#") and "=" in _line: + _k, _, _v = _line.partition("=") + os.environ.setdefault(_k.strip(), _v.strip()) + +APPSYNC_ENDPOINT = os.environ["APP_SYNC_ENDPOINT"] +APPSYNC_KEY = os.environ["APP_SYNC_API_KEY"] +S3_BUCKET = os.environ["S3_BUCKET"] +S3_REGION = "ap-northeast-1" +S3_PREFIX = "public/site/live" +AWS_CLI = os.environ.get("AWS_CLI", "/tmp/awscli/aws/dist/aws") + +GOLIVE_DB_PATHS = [ + "/app/tokyo-livehouse-events/data/events.db", +] + +# AppSync venueName patterns → golive venue_id +VENUE_MAP: list[tuple[list[str], str]] = [ + (["navey floor", "navey floor AKASAKA"], "navey-floor"), + (["Meets 大塚", "MEETS大塚"], "meets-otsuka"), + (["国分寺MORGANA", "国分寺 MORGANA"], "morgana-kokubunji"), + (["BuzzFront Yokohama", "BuzzFront"], "buzzfront-yokohama"), + (["下北沢ERA", "shimokitazawa ERA"], "shimokitazawa-era"), + (["新代田FEVER", "FEVER"], "fever-shindaita"), + (["吉祥寺WARP", "WARP 吉祥寺"], "warp-kichijoji"), + (["LIQUID ROOM", "liquidroom"], "liquid-room"), + (["WWW", "WWW X"], "www-shibuya"), +] + +# ── helpers ────────────────────────────────────────────────────────────────── + +def load_env(): + if ENV_FILE.exists(): + for line in ENV_FILE.read_text().splitlines(): + line = line.strip() + if line and not line.startswith("#") and "=" in line: + k, _, v = line.partition("=") + os.environ.setdefault(k.strip(), v.strip()) + +def appsync(query: str, variables: dict) -> dict: + payload = json.dumps({"query": query, "variables": variables}).encode() + req = urllib.request.Request( + APPSYNC_ENDPOINT, + data=payload, + headers={"Content-Type": "application/json", "x-api-key": APPSYNC_KEY}, + ) + with urllib.request.urlopen(req) as r: + return json.loads(r.read()) + +def fetch_event(event_id: str) -> dict: + res = appsync( + "query GetEvent($id: ID!) { getEvent(id: $id) { " + "id type date title venueName published description ticketFee " + "ticketURL detailURL mainImageURL shortTitle key venueURL _version } }", + {"id": event_id}, + ) + return res["data"]["getEvent"] + +def list_unenriched_events() -> list[dict]: + res = appsync( + "{ listEvents(limit: 200) { items { " + "id date title venueName published ticketURL detailURL mainImageURL _version } } }", + {}, + ) + items = res["data"]["listEvents"]["items"] + return [ + e for e in items + if not e.get("ticketURL") or not e.get("detailURL") or not e.get("mainImageURL") + ] + +def golive_db() -> sqlite3.Connection | None: + for p in GOLIVE_DB_PATHS: + if os.path.exists(p): + db = sqlite3.connect(p) + db.row_factory = sqlite3.Row + return db + return None + +def venue_id_for(venue_name: str) -> str | None: + vn = venue_name.lower() + for patterns, vid in VENUE_MAP: + if any(p.lower() in vn or vn in p.lower() for p in patterns): + return vid + return None + +def search_golive(db: sqlite3.Connection, date: str, venue_id: str, title: str) -> dict | None: + # Exact date + venue_id match, pick the row whose title is closest + rows = db.execute( + "SELECT * FROM events WHERE date=? AND venue_id=?", (date, venue_id) + ).fetchall() + if not rows: + return None + if len(rows) == 1: + return dict(rows[0]) + # Multiple events same day/venue — pick closest title match + title_l = title.lower() + def score(r): + words = [w for w in re.split(r'\W+', title_l) if w] + return sum(1 for w in words if w in r["title"].lower()) + best = max(rows, key=score) + return dict(best) + +def fetch_url(url: str) -> str: + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + with urllib.request.urlopen(req, timeout=15) as r: + return r.read().decode("utf-8", errors="replace") + +def extract_flyer_image(html: str, base_url: str) -> str | None: + """Return first that looks like a real flyer (not logo/icon).""" + skip_patterns = [ + r"/logo", r"/icon", r"placeholder", r"noimage", r"instagram-feed", + r"/themes/", r"nf_logo", + ] + for src in re.findall(r']+src=["\']([^"\']+)["\']', html): + if any(re.search(p, src, re.I) for p in skip_patterns): + continue + if src.startswith("//"): + src = "https:" + src + elif src.startswith("/"): + m = re.match(r"(https?://[^/]+)", base_url) + src = m.group(1) + src if m else src + return src + return None + +def upload_to_s3(local_path: str, s3_key: str) -> str: + import subprocess + env = {**os.environ, "AWS_DEFAULT_REGION": S3_REGION} + subprocess.run( + [AWS_CLI, "s3", "cp", local_path, f"s3://{S3_BUCKET}/{s3_key}", + "--content-type", "image/jpeg", "--acl", "public-read"], + check=True, env=env, + ) + return f"https://{S3_BUCKET}.s3.{S3_REGION}.amazonaws.com/{s3_key}" + +def download_image(url: str) -> str: + """Download to a temp file. Returns local path.""" + suffix = ".jpg" if url.lower().endswith(".jpg") else ".png" + tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix) + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + with urllib.request.urlopen(req, timeout=20) as r: + tmp.write(r.read()) + tmp.close() + return tmp.name + +def build_description(golive_row: dict) -> str: + parts = [] + open_t = golive_row.get("open_time") + start_t = golive_row.get("start_time") + if open_t and start_t: + parts.append(f"OPEN {open_t} / START {start_t}") + elif start_t: + parts.append(f"START {start_t}") + artists = golive_row.get("artist") or "" + if artists: + parts.append(f"出演: {artists}") + return "\n".join(parts) + +def enrich(event_id: str, dry_run: bool = False): + load_env() + print(f"\n── enriching {event_id}") + + ev = fetch_event(event_id) + if not ev: + print(" ERROR: event not found") + return + + print(f" {ev['date']} {ev['title']} @ {ev['venueName']}") + + updates: dict = {} + + # ── 1. golive DB lookup ─────────────────────────────────────────────── + db = golive_db() + golive_row = None + if db: + vid = venue_id_for(ev["venueName"] or "") + if vid: + golive_row = search_golive(db, ev["date"], vid, ev["title"]) + if golive_row: + print(f" golive match: [{golive_row['venue_id']}] {golive_row['title']}") + else: + print(f" golive: no match for venue_id={vid} date={ev['date']}") + else: + print(f" golive: venue not mapped ({ev['venueName']})") + else: + print(" golive DB not found (non-server environment)") + + if golive_row: + if not ev.get("ticketURL") and golive_row.get("ticket_url"): + updates["ticketURL"] = golive_row["ticket_url"] + if not ev.get("detailURL") and golive_row.get("source_url"): + updates["detailURL"] = golive_row["source_url"] + if golive_row.get("price"): + updates["ticketFee"] = golive_row["price"] + desc = build_description(golive_row) + if desc: + updates["description"] = desc + + # ── 2. flyer image from venue event page ───────────────────────────── + detail_url = updates.get("detailURL") or ev.get("detailURL") + if detail_url and not ev.get("mainImageURL"): + print(f" fetching event page: {detail_url}") + try: + html = fetch_url(detail_url) + img_url = extract_flyer_image(html, detail_url) + if img_url: + print(f" flyer image: {img_url}") + # derive S3 key from date + sanitized title + slug = re.sub(r'[^\w-]', '-', ev["date"] + "-" + (ev["key"] or ev["title"][:20])) + slug = re.sub(r'-+', '-', slug).strip("-").lower() + ext = ".jpg" if "jpg" in img_url.lower() else ".png" + s3_key = f"{S3_PREFIX}/{slug}-flyer{ext}" + local = download_image(img_url) + if not dry_run: + s3_url = upload_to_s3(local, s3_key) + updates["mainImageURL"] = s3_url + print(f" uploaded: {s3_url}") + else: + print(f" [dry-run] would upload to s3://{S3_BUCKET}/{s3_key}") + os.unlink(local) + else: + print(" no flyer image found on event page") + except Exception as e: + print(f" WARNING: could not fetch event page: {e}") + + if not updates: + print(" nothing to update") + return + + print(f" updates: {list(updates.keys())}") + + if dry_run: + print(f" [dry-run] would update: {json.dumps(updates, ensure_ascii=False, indent=2)}") + return + + updates["id"] = event_id + updates["_version"] = ev["_version"] + + res = appsync( + "mutation UpdateEvent($input: UpdateEventInput!) { updateEvent(input: $input) { " + "id date title ticketURL detailURL mainImageURL description ticketFee _version } }", + {"input": updates}, + ) + if "errors" in res: + print(f" ERROR: {res['errors']}") + else: + print(f" ✓ updated (version {res['data']['updateEvent']['_version']})") + +# ── main ───────────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + args = sys.argv[1:] + dry_run = "--dry-run" in args + args = [a for a in args if not a.startswith("--")] + + if not args: + print("Usage:") + print(" python3 enrich-event.py [--dry-run]") + print(" python3 enrich-event.py --all-unenriched [--dry-run]") + sys.exit(1) + + load_env() + + if "--all-unenriched" in sys.argv: + events = list_unenriched_events() + print(f"Found {len(events)} unenriched events") + for e in sorted(events, key=lambda x: x["date"] or ""): + enrich(e["id"], dry_run=dry_run) + else: + enrich(args[0], dry_run=dry_run) diff --git a/official-site-scripts/events/2026-06-27-cre4m-sod4.json b/official-site-scripts/events/2026-06-27-cre4m-sod4.json new file mode 100644 index 0000000..ca9ebef --- /dev/null +++ b/official-site-scripts/events/2026-06-27-cre4m-sod4.json @@ -0,0 +1,17 @@ +{ + "id": "4c99e242-c13d-404b-92a9-4c2cd59ad68a", + "type": "LIVE", + "date": "2026-06-27", + "title": "cre4m sod4", + "shortTitle": "cre4m sod4", + "venueName": "navey floor AKASAKA", + "published": true, + "description": "雨傘とペトリコール / mosquitone / 人造ネオン / LOOM", + "ticketFee": "一般 ¥3,000(+1D ¥700) ※学生証提示で¥1,000ディスカウント / 配信 ¥2,000", + "key": "260627", + "ticketURL": "https://tiget.net/events/494018", + "detailURL": "https://navey-floor.com/event/2026-6-27n/", + "mainImageURL": "https://S3_BUCKET_REDACTED.s3.ap-northeast-1.amazonaws.com/public/site/live/260627-cre4m-sod4-flyer.jpg", + "venueURL": null, + "_note": "OPEN 18:00 / START 18:30 (当初発表から変更あり)" +} diff --git a/official-site-scripts/list-events.sh b/official-site-scripts/list-events.sh new file mode 100755 index 0000000..d56aabf --- /dev/null +++ b/official-site-scripts/list-events.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +[ -f "$REPO_ROOT/.env" ] && set -a && source "$REPO_ROOT/.env" && set +a + +AWS_CLI="${AWS_CLI:-/tmp/awscli/aws/dist/aws}" +ENDPOINT="${APP_SYNC_ENDPOINT:?APP_SYNC_ENDPOINT not set}" +API_KEY="${APP_SYNC_API_KEY:?APP_SYNC_API_KEY not set}" + +QUERY='{ listEvents(limit: 200) { items { id type date title venueName published shortTitle ticketFee key } } }' + +curl -s -X POST "$ENDPOINT" \ + -H "Content-Type: application/json" \ + -H "x-api-key: $API_KEY" \ + -d "{\"query\": $(echo "$QUERY" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')}" \ +| python3 -c " +import json, sys +d = json.load(sys.stdin) +items = d['data']['listEvents']['items'] +items.sort(key=lambda x: x['date'] or '') +for e in items: + pub = '✓' if e['published'] else ' ' + print(f\"[{pub}] {e['date']} {e['title'][:50]:<50} {e['venueName'] or ''}\") +" diff --git a/official-site/add-event.sh b/official-site/add-event.sh deleted file mode 100755 index 109c2c8..0000000 --- a/official-site/add-event.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash -# Usage: bash add-event.sh -# The JSON file should match the Event schema (see CLAUDE.md). -# id, createdAt, updatedAt, _version, _deleted, _lastChangedAt are auto-managed. -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -[ -f "$REPO_ROOT/.env" ] && set -a && source "$REPO_ROOT/.env" && set +a - -ENDPOINT="${APP_SYNC_ENDPOINT:?APP_SYNC_ENDPOINT not set}" -API_KEY="${APP_SYNC_API_KEY:?APP_SYNC_API_KEY not set}" - -JSON_FILE="${1:-}" -if [ -z "$JSON_FILE" ]; then - echo "Usage: $0 " - exit 1 -fi - -INPUT=$(python3 -c " -import json, sys -with open('$JSON_FILE') as f: - data = json.load(f) -# Remove read-only / auto-managed fields -for k in ['id', 'createdAt', 'updatedAt', '_version', '_deleted', '_lastChangedAt', '_note']: - data.pop(k, None) -# Remove null fields (optional) -data = {k: v for k, v in data.items() if v is not None} -print(json.dumps(data)) -") - -QUERY='mutation CreateEvent($input: CreateEventInput!) { createEvent(input: $input) { id type date title venueName published } }' -PAYLOAD=$(python3 -c " -import json, sys -q = '$QUERY' -inp = $INPUT -print(json.dumps({'query': q, 'variables': {'input': inp}})) -") - -echo "Creating event from $JSON_FILE ..." -RESULT=$(curl -s -X POST "$ENDPOINT" \ - -H "Content-Type: application/json" \ - -H "x-api-key: $API_KEY" \ - -d "$PAYLOAD") - -echo "$RESULT" | python3 -m json.tool -echo "" -echo "Done. Run 'bash deploy.sh' to rebuild the site." diff --git a/official-site/deploy.sh b/official-site/deploy.sh deleted file mode 100755 index 32574e0..0000000 --- a/official-site/deploy.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -[ -f "$REPO_ROOT/.env" ] && set -a && source "$REPO_ROOT/.env" && set +a - -AWS_CLI="${AWS_CLI:-/tmp/awscli/aws/dist/aws}" -APP_ID="d33773t2lyrxkx" -BRANCH="main" -REGION="${AWS_DEFAULT_REGION:-ap-northeast-1}" - -echo "Starting Amplify build: app=$APP_ID branch=$BRANCH" -JOB=$("$AWS_CLI" amplify start-job \ - --app-id "$APP_ID" \ - --branch-name "$BRANCH" \ - --job-type RELEASE \ - --region "$REGION" \ - --output json) - -JOB_ID=$(echo "$JOB" | python3 -c "import json,sys; print(json.load(sys.stdin)['jobSummary']['jobId'])") -echo "Job started: $JOB_ID" -echo "Console: https://ap-northeast-1.console.aws.amazon.com/amplify/apps/$APP_ID/branches/$BRANCH/deployments/$JOB_ID" - -echo "Waiting for build to complete..." -while true; do - STATUS=$("$AWS_CLI" amplify get-job \ - --app-id "$APP_ID" \ - --branch-name "$BRANCH" \ - --job-id "$JOB_ID" \ - --region "$REGION" \ - --output json | python3 -c "import json,sys; print(json.load(sys.stdin)['job']['summary']['status'])") - echo " Status: $STATUS" - if [[ "$STATUS" == "SUCCEED" ]]; then - echo "Build succeeded! https://www.mosquit.one" - break - elif [[ "$STATUS" == "FAILED" || "$STATUS" == "CANCELLED" ]]; then - echo "Build $STATUS" - exit 1 - fi - sleep 15 -done diff --git a/official-site/enrich-event.py b/official-site/enrich-event.py deleted file mode 100755 index 846a61e..0000000 --- a/official-site/enrich-event.py +++ /dev/null @@ -1,305 +0,0 @@ -#!/usr/bin/env python3 -""" -Enrich an AppSync Event with details from golive DB and venue website. - -Usage: - python3 enrich-event.py - python3 enrich-event.py --all-unenriched # process events missing ticketURL/detailURL - -Sources (tried in order): - 1. golive SQLite DB (/app/tokyo-livehouse-events/data/events.db) - 2. Venue website (direct fetch) - -What gets filled in: - ticketURL ← golive ticket_url - detailURL ← golive source_url - ticketFee ← golive price (if current value looks incomplete) - description ← "OPEN HH:MM / START HH:MM\\n出演: " - mainImageURL← first non-logo from the venue event page, uploaded to S3 -""" - -import json -import os -import re -import sqlite3 -import sys -import tempfile -import urllib.request -from pathlib import Path - -# ── config ────────────────────────────────────────────────────────────────── -REPO_ROOT = Path(__file__).parent.parent -ENV_FILE = REPO_ROOT / ".env" - -# Load .env early so os.environ is populated before constants are evaluated -if ENV_FILE.exists(): - for _line in ENV_FILE.read_text().splitlines(): - _line = _line.strip() - if _line and not _line.startswith("#") and "=" in _line: - _k, _, _v = _line.partition("=") - os.environ.setdefault(_k.strip(), _v.strip()) - -APPSYNC_ENDPOINT = os.environ["APP_SYNC_ENDPOINT"] -APPSYNC_KEY = os.environ["APP_SYNC_API_KEY"] -S3_BUCKET = os.environ["S3_BUCKET"] -S3_REGION = "ap-northeast-1" -S3_PREFIX = "public/site/live" -AWS_CLI = os.environ.get("AWS_CLI", "/tmp/awscli/aws/dist/aws") - -GOLIVE_DB_PATHS = [ - "/app/tokyo-livehouse-events/data/events.db", -] - -# AppSync venueName patterns → golive venue_id -VENUE_MAP: list[tuple[list[str], str]] = [ - (["navey floor", "navey floor AKASAKA"], "navey-floor"), - (["Meets 大塚", "MEETS大塚"], "meets-otsuka"), - (["国分寺MORGANA", "国分寺 MORGANA"], "morgana-kokubunji"), - (["BuzzFront Yokohama", "BuzzFront"], "buzzfront-yokohama"), - (["下北沢ERA", "shimokitazawa ERA"], "shimokitazawa-era"), - (["新代田FEVER", "FEVER"], "fever-shindaita"), - (["吉祥寺WARP", "WARP 吉祥寺"], "warp-kichijoji"), - (["LIQUID ROOM", "liquidroom"], "liquid-room"), - (["WWW", "WWW X"], "www-shibuya"), -] - -# ── helpers ────────────────────────────────────────────────────────────────── - -def load_env(): - if ENV_FILE.exists(): - for line in ENV_FILE.read_text().splitlines(): - line = line.strip() - if line and not line.startswith("#") and "=" in line: - k, _, v = line.partition("=") - os.environ.setdefault(k.strip(), v.strip()) - -def appsync(query: str, variables: dict) -> dict: - payload = json.dumps({"query": query, "variables": variables}).encode() - req = urllib.request.Request( - APPSYNC_ENDPOINT, - data=payload, - headers={"Content-Type": "application/json", "x-api-key": APPSYNC_KEY}, - ) - with urllib.request.urlopen(req) as r: - return json.loads(r.read()) - -def fetch_event(event_id: str) -> dict: - res = appsync( - "query GetEvent($id: ID!) { getEvent(id: $id) { " - "id type date title venueName published description ticketFee " - "ticketURL detailURL mainImageURL shortTitle key venueURL _version } }", - {"id": event_id}, - ) - return res["data"]["getEvent"] - -def list_unenriched_events() -> list[dict]: - res = appsync( - "{ listEvents(limit: 200) { items { " - "id date title venueName published ticketURL detailURL mainImageURL _version } } }", - {}, - ) - items = res["data"]["listEvents"]["items"] - return [ - e for e in items - if not e.get("ticketURL") or not e.get("detailURL") or not e.get("mainImageURL") - ] - -def golive_db() -> sqlite3.Connection | None: - for p in GOLIVE_DB_PATHS: - if os.path.exists(p): - db = sqlite3.connect(p) - db.row_factory = sqlite3.Row - return db - return None - -def venue_id_for(venue_name: str) -> str | None: - vn = venue_name.lower() - for patterns, vid in VENUE_MAP: - if any(p.lower() in vn or vn in p.lower() for p in patterns): - return vid - return None - -def search_golive(db: sqlite3.Connection, date: str, venue_id: str, title: str) -> dict | None: - # Exact date + venue_id match, pick the row whose title is closest - rows = db.execute( - "SELECT * FROM events WHERE date=? AND venue_id=?", (date, venue_id) - ).fetchall() - if not rows: - return None - if len(rows) == 1: - return dict(rows[0]) - # Multiple events same day/venue — pick closest title match - title_l = title.lower() - def score(r): - words = [w for w in re.split(r'\W+', title_l) if w] - return sum(1 for w in words if w in r["title"].lower()) - best = max(rows, key=score) - return dict(best) - -def fetch_url(url: str) -> str: - req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) - with urllib.request.urlopen(req, timeout=15) as r: - return r.read().decode("utf-8", errors="replace") - -def extract_flyer_image(html: str, base_url: str) -> str | None: - """Return first that looks like a real flyer (not logo/icon).""" - skip_patterns = [ - r"/logo", r"/icon", r"placeholder", r"noimage", r"instagram-feed", - r"/themes/", r"nf_logo", - ] - for src in re.findall(r']+src=["\']([^"\']+)["\']', html): - if any(re.search(p, src, re.I) for p in skip_patterns): - continue - if src.startswith("//"): - src = "https:" + src - elif src.startswith("/"): - m = re.match(r"(https?://[^/]+)", base_url) - src = m.group(1) + src if m else src - return src - return None - -def upload_to_s3(local_path: str, s3_key: str) -> str: - import subprocess - env = {**os.environ, "AWS_DEFAULT_REGION": S3_REGION} - subprocess.run( - [AWS_CLI, "s3", "cp", local_path, f"s3://{S3_BUCKET}/{s3_key}", - "--content-type", "image/jpeg", "--acl", "public-read"], - check=True, env=env, - ) - return f"https://{S3_BUCKET}.s3.{S3_REGION}.amazonaws.com/{s3_key}" - -def download_image(url: str) -> str: - """Download to a temp file. Returns local path.""" - suffix = ".jpg" if url.lower().endswith(".jpg") else ".png" - tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix) - req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) - with urllib.request.urlopen(req, timeout=20) as r: - tmp.write(r.read()) - tmp.close() - return tmp.name - -def build_description(golive_row: dict) -> str: - parts = [] - open_t = golive_row.get("open_time") - start_t = golive_row.get("start_time") - if open_t and start_t: - parts.append(f"OPEN {open_t} / START {start_t}") - elif start_t: - parts.append(f"START {start_t}") - artists = golive_row.get("artist") or "" - if artists: - parts.append(f"出演: {artists}") - return "\n".join(parts) - -def enrich(event_id: str, dry_run: bool = False): - load_env() - print(f"\n── enriching {event_id}") - - ev = fetch_event(event_id) - if not ev: - print(" ERROR: event not found") - return - - print(f" {ev['date']} {ev['title']} @ {ev['venueName']}") - - updates: dict = {} - - # ── 1. golive DB lookup ─────────────────────────────────────────────── - db = golive_db() - golive_row = None - if db: - vid = venue_id_for(ev["venueName"] or "") - if vid: - golive_row = search_golive(db, ev["date"], vid, ev["title"]) - if golive_row: - print(f" golive match: [{golive_row['venue_id']}] {golive_row['title']}") - else: - print(f" golive: no match for venue_id={vid} date={ev['date']}") - else: - print(f" golive: venue not mapped ({ev['venueName']})") - else: - print(" golive DB not found (non-server environment)") - - if golive_row: - if not ev.get("ticketURL") and golive_row.get("ticket_url"): - updates["ticketURL"] = golive_row["ticket_url"] - if not ev.get("detailURL") and golive_row.get("source_url"): - updates["detailURL"] = golive_row["source_url"] - if golive_row.get("price"): - updates["ticketFee"] = golive_row["price"] - desc = build_description(golive_row) - if desc: - updates["description"] = desc - - # ── 2. flyer image from venue event page ───────────────────────────── - detail_url = updates.get("detailURL") or ev.get("detailURL") - if detail_url and not ev.get("mainImageURL"): - print(f" fetching event page: {detail_url}") - try: - html = fetch_url(detail_url) - img_url = extract_flyer_image(html, detail_url) - if img_url: - print(f" flyer image: {img_url}") - # derive S3 key from date + sanitized title - slug = re.sub(r'[^\w-]', '-', ev["date"] + "-" + (ev["key"] or ev["title"][:20])) - slug = re.sub(r'-+', '-', slug).strip("-").lower() - ext = ".jpg" if "jpg" in img_url.lower() else ".png" - s3_key = f"{S3_PREFIX}/{slug}-flyer{ext}" - local = download_image(img_url) - if not dry_run: - s3_url = upload_to_s3(local, s3_key) - updates["mainImageURL"] = s3_url - print(f" uploaded: {s3_url}") - else: - print(f" [dry-run] would upload to s3://{S3_BUCKET}/{s3_key}") - os.unlink(local) - else: - print(" no flyer image found on event page") - except Exception as e: - print(f" WARNING: could not fetch event page: {e}") - - if not updates: - print(" nothing to update") - return - - print(f" updates: {list(updates.keys())}") - - if dry_run: - print(f" [dry-run] would update: {json.dumps(updates, ensure_ascii=False, indent=2)}") - return - - updates["id"] = event_id - updates["_version"] = ev["_version"] - - res = appsync( - "mutation UpdateEvent($input: UpdateEventInput!) { updateEvent(input: $input) { " - "id date title ticketURL detailURL mainImageURL description ticketFee _version } }", - {"input": updates}, - ) - if "errors" in res: - print(f" ERROR: {res['errors']}") - else: - print(f" ✓ updated (version {res['data']['updateEvent']['_version']})") - -# ── main ───────────────────────────────────────────────────────────────────── - -if __name__ == "__main__": - args = sys.argv[1:] - dry_run = "--dry-run" in args - args = [a for a in args if not a.startswith("--")] - - if not args: - print("Usage:") - print(" python3 enrich-event.py [--dry-run]") - print(" python3 enrich-event.py --all-unenriched [--dry-run]") - sys.exit(1) - - load_env() - - if "--all-unenriched" in sys.argv: - events = list_unenriched_events() - print(f"Found {len(events)} unenriched events") - for e in sorted(events, key=lambda x: x["date"] or ""): - enrich(e["id"], dry_run=dry_run) - else: - enrich(args[0], dry_run=dry_run) diff --git a/official-site/events/2026-06-27-cre4m-sod4.json b/official-site/events/2026-06-27-cre4m-sod4.json deleted file mode 100644 index ca9ebef..0000000 --- a/official-site/events/2026-06-27-cre4m-sod4.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "4c99e242-c13d-404b-92a9-4c2cd59ad68a", - "type": "LIVE", - "date": "2026-06-27", - "title": "cre4m sod4", - "shortTitle": "cre4m sod4", - "venueName": "navey floor AKASAKA", - "published": true, - "description": "雨傘とペトリコール / mosquitone / 人造ネオン / LOOM", - "ticketFee": "一般 ¥3,000(+1D ¥700) ※学生証提示で¥1,000ディスカウント / 配信 ¥2,000", - "key": "260627", - "ticketURL": "https://tiget.net/events/494018", - "detailURL": "https://navey-floor.com/event/2026-6-27n/", - "mainImageURL": "https://S3_BUCKET_REDACTED.s3.ap-northeast-1.amazonaws.com/public/site/live/260627-cre4m-sod4-flyer.jpg", - "venueURL": null, - "_note": "OPEN 18:00 / START 18:30 (当初発表から変更あり)" -} diff --git a/official-site/list-events.sh b/official-site/list-events.sh deleted file mode 100755 index d56aabf..0000000 --- a/official-site/list-events.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -[ -f "$REPO_ROOT/.env" ] && set -a && source "$REPO_ROOT/.env" && set +a - -AWS_CLI="${AWS_CLI:-/tmp/awscli/aws/dist/aws}" -ENDPOINT="${APP_SYNC_ENDPOINT:?APP_SYNC_ENDPOINT not set}" -API_KEY="${APP_SYNC_API_KEY:?APP_SYNC_API_KEY not set}" - -QUERY='{ listEvents(limit: 200) { items { id type date title venueName published shortTitle ticketFee key } } }' - -curl -s -X POST "$ENDPOINT" \ - -H "Content-Type: application/json" \ - -H "x-api-key: $API_KEY" \ - -d "{\"query\": $(echo "$QUERY" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')}" \ -| python3 -c " -import json, sys -d = json.load(sys.stdin) -items = d['data']['listEvents']['items'] -items.sort(key=lambda x: x['date'] or '') -for e in items: - pub = '✓' if e['published'] else ' ' - print(f\"[{pub}] {e['date']} {e['title'][:50]:<50} {e['venueName'] or ''}\") -" -- cgit v1.2.3