Ethscriptions commited on
Commit
e4c92c1
·
verified ·
1 Parent(s): 69197ae

Upload schedule_api_client.py

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