summaryrefslogtreecommitdiff
path: root/server.py
diff options
context:
space:
mode:
authoryyamashita <yyamashita@hetzner.yyamashita.com>2026-11-01 09:19:55 +0900
committeryyamashita <yyamashita@hetzner.yyamashita.com>2026-11-01 09:19:55 +0900
commitdcb35334905f914dfd3eaa7453f0b019bd777588 (patch)
tree2a78d999c28b40abf682b0c2b7c3f1802b98c4f3 /server.py
parentf0044f886359d942fe479a9a8a940af17c7047ac (diff)
スタジオセットごとにリンキィディンクの実際の空き枠を表示する
ダミーデータをやめ、Reserve Mart の会員ページから日別状況を読む。 取得結果は 5 分キャッシュし、ノアなど取れないスタジオは電話番号とリンクを出す。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'server.py')
-rw-r--r--server.py67
1 files changed, 55 insertions, 12 deletions
diff --git a/server.py b/server.py
index 286fec8..9763aae 100644
--- a/server.py
+++ b/server.py
@@ -3,12 +3,15 @@
python3 server.py # http://localhost:8000
python3 server.py 8080 # ポート指定
HOST=0.0.0.0 python3 server.py # コンテナ内など外から受ける場合
+
+リンキィディンクの取得には RINKY_ID / RINKY_PASSWORD が要る(.env に書けば読み込む)。
"""
import json
import os
import sys
-from datetime import date
+from concurrent.futures import ThreadPoolExecutor
+from datetime import date, timedelta
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlparse
@@ -17,22 +20,53 @@ import providers
ROOT = Path(__file__).resolve().parent
STATIC = ROOT / "static"
+MAX_DAYS_AHEAD = 92 # リンキィディンクは 3 ヶ月先まで予約可
+
+
+def load_env():
+ env = ROOT / ".env"
+ if not env.exists():
+ return
+ for line in env.read_text(encoding="utf-8").splitlines():
+ if "=" in line and not line.lstrip().startswith("#"):
+ key, value = line.split("=", 1)
+ os.environ.setdefault(key.strip(), value.strip())
-def load_studios():
+def load_config():
return json.loads((ROOT / "studios.json").read_text(encoding="utf-8"))
-def slots_for(day):
- result = []
- for studio in load_studios():
+def studio_info(studio_id, studio):
+ return {
+ "id": studio_id,
+ "name": studio["name"],
+ "area": studio["area"],
+ "url": studio["url"],
+ "phone": studio["phone"],
+ "booking": providers.booking_url(studio),
+ "online": bool(studio.get("provider")),
+ }
+
+
+def slots_for(set_id, day):
+ config = load_config()
+ target = next((s for s in config["sets"] if s["id"] == set_id), None)
+ if not target:
+ return None
+
+ def one(studio_id):
+ studio = config["studios"][studio_id]
+ info = studio_info(studio_id, studio)
try:
- slots = providers.fetch(studio, day)
- error = None
+ rooms, fetched_at = providers.fetch(studio_id, studio, day)
+ return {**info, "rooms": rooms, "fetched_at": fetched_at, "error": None}
except Exception as e: # 1軒の取得失敗で一覧全体を落とさない
- slots, error = [], str(e)
- result.append({**studio, "slots": slots, "error": error})
- return result
+ return {**info, "rooms": None, "fetched_at": None, "error": str(e)}
+
+ with ThreadPoolExecutor(max_workers=4) as pool:
+ studios = list(pool.map(one, target["studios"]))
+ return {"set": target["id"], "date": day.isoformat(), "studios": studios}
class Handler(SimpleHTTPRequestHandler):
@@ -41,14 +75,22 @@ class Handler(SimpleHTTPRequestHandler):
def do_GET(self):
url = urlparse(self.path)
+ qs = parse_qs(url.query)
+ if url.path == "/api/sets":
+ config = load_config()
+ return self.send_json([{"id": s["id"], "name": s["name"]} for s in config["sets"]])
if url.path != "/api/slots":
return super().do_GET()
- qs = parse_qs(url.query)
try:
day = date.fromisoformat(qs.get("date", [date.today().isoformat()])[0])
except ValueError:
return self.send_json({"error": "date は YYYY-MM-DD で指定してください"}, 400)
- self.send_json({"date": day.isoformat(), "studios": slots_for(day)})
+ if not date.today() - timedelta(days=1) <= day <= date.today() + timedelta(days=MAX_DAYS_AHEAD):
+ return self.send_json({"error": f"date は今日から {MAX_DAYS_AHEAD} 日先までです"}, 400)
+ body = slots_for(qs.get("set", [""])[0], day)
+ if body is None:
+ return self.send_json({"error": "set が見つかりません"}, 404)
+ self.send_json(body)
def send_json(self, body, status=200):
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
@@ -60,6 +102,7 @@ class Handler(SimpleHTTPRequestHandler):
if __name__ == "__main__":
+ load_env()
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
host = os.environ.get("HOST", "127.0.0.1")
print(f"http://{host}:{port}", flush=True)