"""リンキィディンク(Reserve Mart / reserve1.jp)の予約状況を会員ページから読む。 RINKY_ID / RINKY_PASSWORD 環境変数の会員でログインし、日別状況の表を解析する。 1回ログインすれば form0 の grand(店舗)と day_btn(日付)を変えて全店舗を見られる。 """ import html import http.cookiejar import os import re import threading import urllib.parse import urllib.request BASE = "https://www.reserve1.jp/studio/member/" OFFICE = "1000528" TIMEOUT = 20 _lock = threading.Lock() _opener = None def booking_url(studio): return f"https://www.reserve1.jp/studio/visitor/VisitorLogin.php?lc=olcatdves&mn=1&gr={studio['grand']}&stt=MLRRX" def _post(data): body = urllib.parse.urlencode(data, encoding="euc-jp").encode() res = _opener.open(BASE + "member_select.php", body, timeout=TIMEOUT) return res.read().decode("euc-jp", "replace") def _login(grand): global _opener user, password = os.environ.get("RINKY_ID"), os.environ.get("RINKY_PASSWORD") if not user or not password: raise RuntimeError("RINKY_ID / RINKY_PASSWORD が設定されていません") _opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar())) _opener.open(f"{BASE}VisitorLogin.php?lc=olcatdves&mn=1&gr={grand}", timeout=TIMEOUT).read() page = _post({ "in_member": user, "in_mpassword": password, "office": OFFICE, "grand": grand, "ct": "", "mngfg": "1", "c_menu": "", "member_select": "2", }) if 'name="form1"' not in page: _opener = None raise RuntimeError("リンキィディンクにログインできませんでした") def _day_page(grand, day): return _post({ "office": OFFICE, "mngfg": "1", "grand": grand, "rdate": day, "member_select": "3", "month_btn": "", "day_btn": day, }) def parse(page): """日別状況の表を [{"name", "slots": [{"start": 分, "end": 分, "status"}]}] にする。 start / end は表示日の 0:00 からの分。翌日にかかる枠は 1440 以上になる。 通常店は 30 分刻み、深夜/早朝専用店は 90 分刻み。 """ form = re.search(r'