Ethscriptions commited on
Commit
1b8113d
·
verified ·
1 Parent(s): 60c5987

Upload schedule_api_client.py

Browse files
Files changed (1) hide show
  1. schedule_api_client.py +337 -0
schedule_api_client.py ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 与 app.py 一致的票务排程 API(Token、影厅排片),供多页面复用,避免 import app 时执行整站 UI。
3
+ """
4
+ import json
5
+ import os
6
+ import time
7
+
8
+ import pandas as pd
9
+ import requests
10
+ from dotenv import load_dotenv
11
+
12
+
13
+ class _NoopStreamlit:
14
+ @staticmethod
15
+ def error(*args, **kwargs):
16
+ return None
17
+
18
+ @staticmethod
19
+ def toast(*args, **kwargs):
20
+ return None
21
+
22
+ @staticmethod
23
+ def cache_data(*args, **kwargs):
24
+ def _decorator(func):
25
+ return func
26
+
27
+ return _decorator
28
+
29
+
30
+ def _resolve_streamlit():
31
+ """
32
+ - Streamlit 页面内:保留原能力(toast/cache_data)
33
+ - 非 Streamlit 运行(如 Flask/FastAPI):使用 no-op,避免无运行时警告
34
+ """
35
+ try:
36
+ import streamlit as _st
37
+ from streamlit.runtime.scriptrunner import get_script_run_ctx
38
+
39
+ if get_script_run_ctx(suppress_warning=True) is None:
40
+ return _NoopStreamlit()
41
+ return _st
42
+ except Exception:
43
+ return _NoopStreamlit()
44
+
45
+
46
+ st = _resolve_streamlit()
47
+
48
+ load_dotenv()
49
+
50
+ TOKEN_FILE = "token_data.json"
51
+ CINEMA_ID = os.getenv("CINEMA_ID")
52
+
53
+
54
+ def load_token():
55
+ if os.path.exists(TOKEN_FILE):
56
+ try:
57
+ with open(TOKEN_FILE, "r", encoding="utf-8") as f:
58
+ return json.load(f)
59
+ except (json.JSONDecodeError, FileNotFoundError):
60
+ return None
61
+ return None
62
+
63
+
64
+ def save_token(token_data):
65
+ try:
66
+ with open(TOKEN_FILE, "w", encoding="utf-8") as f:
67
+ json.dump(token_data, f, ensure_ascii=False, indent=4)
68
+ return True
69
+ except Exception as e:
70
+ st.error(f"保存Token失败: {e}")
71
+ return False
72
+
73
+
74
+ def login_and_get_token():
75
+ username = os.getenv("CINEMA_USERNAME")
76
+ password = os.getenv("CINEMA_PASSWORD")
77
+ res_code = os.getenv("CINEMA_RES_CODE")
78
+ device_id = os.getenv("CINEMA_DEVICE_ID")
79
+
80
+ if not all([username, password, res_code]):
81
+ st.error("登录失败:未配置用户名、密码或影院编码环境变量。")
82
+ return None
83
+
84
+ session = requests.Session()
85
+ session.headers.update({
86
+ "Host": "app.bi.piao51.cn",
87
+ "Accept": "application/json, text/javascript, */*; q=0.01",
88
+ "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
89
+ })
90
+
91
+ login_url = "https://app.bi.piao51.cn/cinema-app/credential/login.action"
92
+ login_headers = {
93
+ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
94
+ "Origin": "https://app.bi.piao51.cn",
95
+ }
96
+ login_data = {
97
+ "username": username,
98
+ "password": password,
99
+ "type": "1",
100
+ "resCode": res_code,
101
+ "deviceid": device_id,
102
+ "dtype": "ios",
103
+ }
104
+
105
+ try:
106
+ response_login = session.post(login_url, headers=login_headers, data=login_data, allow_redirects=False, timeout=15)
107
+ if not (300 <= response_login.status_code < 400 and "token" in session.cookies):
108
+ st.error(f"登录步骤 1 失败,未能获取 Session Token。状态码: {response_login.status_code}")
109
+ return None
110
+
111
+ user_info_url = "https://app.bi.piao51.cn/cinema-app/security/logined.action"
112
+ response_user_info = session.get(user_info_url, timeout=10)
113
+ response_user_info.raise_for_status()
114
+
115
+ user_info = response_user_info.json()
116
+ if user_info.get("success") and user_info.get("data", {}).get("token"):
117
+ token_data = user_info["data"]
118
+ if save_token(token_data):
119
+ st.toast("登录成功,已获取并保存新 Token!", icon="🔑")
120
+ return token_data
121
+ st.error(f"登录步骤 2 失败,未能从 JSON 中提取 Token。响应: {user_info.get('msg')}")
122
+ return None
123
+
124
+ except requests.exceptions.RequestException as e:
125
+ st.error(f"登录请求过程中发生网络错误: {e}")
126
+ return None
127
+
128
+
129
+ def fetch_hall_info(token):
130
+ url = "https://cawapi.yinghezhong.com/showInfo/getShowHallInfo"
131
+ params = {"token": token, "_": int(time.time() * 1000)}
132
+ headers = {"Origin": "https://caw.yinghezhong.com", "User-Agent": "Mozilla/5.0"}
133
+ response = requests.get(url, params=params, headers=headers, timeout=10)
134
+ response.raise_for_status()
135
+ data = response.json()
136
+ if data.get("code") == 1 and data.get("data"):
137
+ return {item["hallId"]: item["seatNum"] for item in data["data"]}
138
+ raise Exception(f"获取影厅信息失败: {data.get('msg', '未知错误')}")
139
+
140
+
141
+ def fetch_schedule_data(token, show_date):
142
+ url = "https://cawapi.yinghezhong.com/showInfo/getHallShowInfo"
143
+ params = {"showDate": show_date, "token": token, "_": int(time.time() * 1000)}
144
+ headers = {"Origin": "https://caw.yinghezhong.com", "User-Agent": "Mozilla/5.0"}
145
+ response = requests.get(url, params=params, headers=headers, timeout=15)
146
+ response.raise_for_status()
147
+ data = response.json()
148
+ if data.get("code") == 1:
149
+ return data.get("data", [])
150
+ if data.get("code") == 500:
151
+ raise ValueError("Token 可能已失效")
152
+ raise Exception(f"获取排片数据失败: {data.get('msg', '未知错误')}")
153
+
154
+
155
+ def get_api_data_with_token_management(show_date):
156
+ token_data = load_token()
157
+ token = token_data.get("token") if token_data else None
158
+ if not token:
159
+ token_data = login_and_get_token()
160
+ if not token_data:
161
+ return None, None
162
+ token = token_data.get("token")
163
+
164
+ try:
165
+ schedule_list = fetch_schedule_data(token, show_date)
166
+ hall_seat_map = fetch_hall_info(token)
167
+ return schedule_list, hall_seat_map
168
+ except ValueError:
169
+ st.toast("Token 已失效,正在尝试重新登录并重试...", icon="🔄")
170
+ token_data = login_and_get_token()
171
+ if not token_data:
172
+ return None, None
173
+ token = token_data.get("token")
174
+ try:
175
+ schedule_list = fetch_schedule_data(token, show_date)
176
+ hall_seat_map = fetch_hall_info(token)
177
+ return schedule_list, hall_seat_map
178
+ except Exception as e:
179
+ st.error(f"重试获取数据失败: {e}")
180
+ return None, None
181
+ except Exception as e:
182
+ st.error(f"获取 API 数据时发生错误: {e}")
183
+ return None, None
184
+
185
+
186
+ @st.cache_data(show_spinner=False, ttl=600)
187
+ def fetch_canonical_movie_names(token, date_str):
188
+ if not CINEMA_ID:
189
+ return []
190
+ url = "https://app.bi.piao51.cn/cinema-app/mycinema/movieSellGross.action"
191
+ params = {
192
+ "token": token,
193
+ "startDate": date_str,
194
+ "endDate": date_str,
195
+ "dateType": "day",
196
+ "cinemaId": CINEMA_ID,
197
+ }
198
+ headers = {
199
+ "Host": "app.bi.piao51.cn",
200
+ "X-Requested-With": "XMLHttpRequest",
201
+ "jwt": "0",
202
+ "Accept": "application/json, text/javascript, */*; q=0.01",
203
+ "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
204
+ }
205
+
206
+ try:
207
+ response = requests.get(url, params=params, headers=headers, timeout=10)
208
+ response.raise_for_status()
209
+ data = response.json()
210
+ if data.get("code") == "A00000" and data.get("results"):
211
+ return [
212
+ item["movieName"]
213
+ for item in data["results"]
214
+ if item.get("movieName") and item["movieName"] != "总计"
215
+ ]
216
+ except Exception as e:
217
+ print(f"获取标准电影名称失败: {e}")
218
+ return []
219
+
220
+
221
+ def clean_movie_title(raw_title, canonical_names=None):
222
+ if not isinstance(raw_title, str):
223
+ return raw_title
224
+
225
+ base_name = None
226
+
227
+ if canonical_names:
228
+ sorted_names = sorted(canonical_names, key=len, reverse=True)
229
+ for name in sorted_names:
230
+ if name in raw_title:
231
+ base_name = name
232
+ break
233
+
234
+ if not base_name:
235
+ base_name = raw_title.split(" ", 1)[0]
236
+
237
+ raw_upper = raw_title.upper()
238
+ suffix = ""
239
+
240
+ if "HDR LED" in raw_upper:
241
+ suffix = "(HDR LED)"
242
+ elif "CINITY" in raw_upper:
243
+ suffix = "(CINITY)"
244
+ elif "杜比" in raw_upper or "DOLBY" in raw_upper:
245
+ suffix = "(杜比视界)"
246
+ elif "IMAX" in raw_upper:
247
+ suffix = "(数字IMAX3D)" if "3D" in raw_upper else "(数字IMAX)"
248
+ elif "巨幕" in raw_upper:
249
+ suffix = "(中国巨幕立体)" if "立体" in raw_upper else "(中国巨幕)"
250
+ elif "3D" in raw_upper:
251
+ suffix = "(数字3D)"
252
+
253
+ if suffix and suffix not in base_name:
254
+ return f"{base_name}{suffix}"
255
+
256
+ return base_name
257
+
258
+
259
+ def get_valid_token(force_refresh=False):
260
+ token_data = None if force_refresh else load_token()
261
+ if not token_data:
262
+ token_data = login_and_get_token()
263
+ if not token_data:
264
+ return None
265
+ return token_data.get("token")
266
+
267
+
268
+ def fetch_schedule_api_bundle(show_date):
269
+ """
270
+ 一次性获取排程相关 API 原始数据:
271
+ - getHallShowInfo(场次列表)
272
+ - getShowHallInfo(影厅座位映射)
273
+ - movieSellGross(标准影片名称)
274
+ """
275
+ schedule_list, hall_seat_map = get_api_data_with_token_management(show_date)
276
+ if schedule_list is None or hall_seat_map is None:
277
+ return None
278
+
279
+ token_data = load_token()
280
+ token = token_data.get("token") if token_data else None
281
+ canonical_names = fetch_canonical_movie_names(token, show_date) if token else []
282
+
283
+ return {
284
+ "show_date": show_date,
285
+ "token": token,
286
+ "schedule_list": schedule_list,
287
+ "hall_seat_map": hall_seat_map,
288
+ "canonical_names": canonical_names,
289
+ }
290
+
291
+
292
+ def process_schedule_dataframe(schedule_list, hall_seat_map, canonical_names=None):
293
+ """将排程 API 原始数据整理成便于展示的表格。"""
294
+ if not schedule_list:
295
+ return pd.DataFrame()
296
+
297
+ df = pd.DataFrame(schedule_list)
298
+ if df.empty:
299
+ return pd.DataFrame()
300
+
301
+ df["座位数"] = df["hallId"].map(hall_seat_map or {}).fillna(0).astype(int)
302
+ df.rename(
303
+ columns={
304
+ "movieName": "影片名称",
305
+ "showStartTime": "放映时间",
306
+ "soldBoxOffice": "总收入",
307
+ "soldTicketNum": "总人次",
308
+ "hallName": "影厅名称",
309
+ "showEndTime": "散场时间",
310
+ },
311
+ inplace=True,
312
+ )
313
+
314
+ if "影片名称" in df.columns:
315
+ df["影片名称_清洗后"] = df["影片名称"].apply(
316
+ lambda x: clean_movie_title(x, canonical_names)
317
+ )
318
+
319
+ required_cols = [
320
+ "影片名称",
321
+ "影片名称_清洗后",
322
+ "放映时间",
323
+ "散场时间",
324
+ "影厅名称",
325
+ "座位数",
326
+ "总收入",
327
+ "总人次",
328
+ ]
329
+ for col in required_cols:
330
+ if col not in df.columns:
331
+ df[col] = None
332
+
333
+ df = df[required_cols]
334
+ for col in ["座位数", "总收入", "总人次"]:
335
+ df[col] = pd.to_numeric(df[col], errors="coerce").fillna(0)
336
+
337
+ return df