summaryrefslogtreecommitdiff
path: root/providers/rinky.py
blob: c8e30fbe9c8a0a1152eb019c307cf11d44a59ef2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
"""リンキィディンク(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'<form name="form1".*?</form>', page, re.S)
    if not form:
        raise RuntimeError("予約状況の表が見つかりません")
    rows = re.findall(r"<tr>(.*?)</tr>", form.group(0), re.S)
    times = []
    for h1, m1, h2, m2 in re.findall(r'<td class="item_base">\s*(\d+):(\d+)\s*<br\s*/?>\s*[~~]\s*<br\s*/?>\s*(\d+):(\d+)', rows[0]):
        start, end = int(h1) * 60 + int(m1), int(h2) * 60 + int(m2)
        # 「翌日」表示の列は時刻が巻き戻るので 24 時間足す
        while times and start < times[-1][0]:
            start += 24 * 60
        while end <= start:
            end += 24 * 60
        times.append((start, end))

    rooms = []
    for row in rows[1:]:
        cells = re.findall(r"<td([^>]*)>(.*?)</td>", row, re.S)
        if not cells:
            continue
        name = html.unescape(re.sub(r"<[^>]*>", "", cells[0][1])).strip()
        if not name:  # 表の最後にある見出しの繰り返し行
            continue
        slots, col = [], 0
        for attrs, body in cells[1:]:
            if col >= len(times):
                break
            span = int(m.group(1)) if (m := re.search(r'colspan="(\d+)"', attrs)) else 1
            cls = m.group(1) if (m := re.search(r'class="([^"]*)"', attrs)) else ""
            if "checkbox" in body:
                status = "closed" if "disabled" in body else "free"
            else:
                status = "booked" if cls.startswith("koma") else "closed"
            for i in range(span):
                if col + i < len(times):
                    start, end = times[col + i]
                    slots.append({"start": start, "end": end, "status": status})
            col += span
        rooms.append({"name": name, "slots": slots})
    return rooms


def fetch(studio, day):
    global _opener
    grand, day = studio["grand"], day.isoformat()
    with _lock:
        for _ in range(2):
            if _opener is None:
                _login(grand)
            page = _day_page(grand, day)
            if 'name="form1"' in page:
                return parse(page)
            # セッション切れなど。ログインし直して 1 回だけやり直す
            _opener = None
        raise RuntimeError("予約状況を取得できませんでした")