nextday / schedule_api_client.py
Ethscriptions's picture
Upload schedule_api_client.py
1b8113d verified
"""
与 app.py 一致的票务排程 API(Token、影厅排片),供多页面复用,避免 import app 时执行整站 UI。
"""
import json
import os
import time
import pandas as pd
import requests
from dotenv import load_dotenv
class _NoopStreamlit:
@staticmethod
def error(*args, **kwargs):
return None
@staticmethod
def toast(*args, **kwargs):
return None
@staticmethod
def cache_data(*args, **kwargs):
def _decorator(func):
return func
return _decorator
def _resolve_streamlit():
"""
- Streamlit 页面内:保留原能力(toast/cache_data)
- 非 Streamlit 运行(如 Flask/FastAPI):使用 no-op,避免无运行时警告
"""
try:
import streamlit as _st
from streamlit.runtime.scriptrunner import get_script_run_ctx
if get_script_run_ctx(suppress_warning=True) is None:
return _NoopStreamlit()
return _st
except Exception:
return _NoopStreamlit()
st = _resolve_streamlit()
load_dotenv()
TOKEN_FILE = "token_data.json"
CINEMA_ID = os.getenv("CINEMA_ID")
def load_token():
if os.path.exists(TOKEN_FILE):
try:
with open(TOKEN_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
return None
return None
def save_token(token_data):
try:
with open(TOKEN_FILE, "w", encoding="utf-8") as f:
json.dump(token_data, f, ensure_ascii=False, indent=4)
return True
except Exception as e:
st.error(f"保存Token失败: {e}")
return False
def login_and_get_token():
username = os.getenv("CINEMA_USERNAME")
password = os.getenv("CINEMA_PASSWORD")
res_code = os.getenv("CINEMA_RES_CODE")
device_id = os.getenv("CINEMA_DEVICE_ID")
if not all([username, password, res_code]):
st.error("登录失败:未配置用户名、密码或影院编码环境变量。")
return None
session = requests.Session()
session.headers.update({
"Host": "app.bi.piao51.cn",
"Accept": "application/json, text/javascript, */*; q=0.01",
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
})
login_url = "https://app.bi.piao51.cn/cinema-app/credential/login.action"
login_headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Origin": "https://app.bi.piao51.cn",
}
login_data = {
"username": username,
"password": password,
"type": "1",
"resCode": res_code,
"deviceid": device_id,
"dtype": "ios",
}
try:
response_login = session.post(login_url, headers=login_headers, data=login_data, allow_redirects=False, timeout=15)
if not (300 <= response_login.status_code < 400 and "token" in session.cookies):
st.error(f"登录步骤 1 失败,未能获取 Session Token。状态码: {response_login.status_code}")
return None
user_info_url = "https://app.bi.piao51.cn/cinema-app/security/logined.action"
response_user_info = session.get(user_info_url, timeout=10)
response_user_info.raise_for_status()
user_info = response_user_info.json()
if user_info.get("success") and user_info.get("data", {}).get("token"):
token_data = user_info["data"]
if save_token(token_data):
st.toast("登录成功,已获取并保存新 Token!", icon="🔑")
return token_data
st.error(f"登录步骤 2 失败,未能从 JSON 中提取 Token。响应: {user_info.get('msg')}")
return None
except requests.exceptions.RequestException as e:
st.error(f"登录请求过程中发生网络错误: {e}")
return None
def fetch_hall_info(token):
url = "https://cawapi.yinghezhong.com/showInfo/getShowHallInfo"
params = {"token": token, "_": int(time.time() * 1000)}
headers = {"Origin": "https://caw.yinghezhong.com", "User-Agent": "Mozilla/5.0"}
response = requests.get(url, params=params, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
if data.get("code") == 1 and data.get("data"):
return {item["hallId"]: item["seatNum"] for item in data["data"]}
raise Exception(f"获取影厅信息失败: {data.get('msg', '未知错误')}")
def fetch_schedule_data(token, show_date):
url = "https://cawapi.yinghezhong.com/showInfo/getHallShowInfo"
params = {"showDate": show_date, "token": token, "_": int(time.time() * 1000)}
headers = {"Origin": "https://caw.yinghezhong.com", "User-Agent": "Mozilla/5.0"}
response = requests.get(url, params=params, headers=headers, timeout=15)
response.raise_for_status()
data = response.json()
if data.get("code") == 1:
return data.get("data", [])
if data.get("code") == 500:
raise ValueError("Token 可能已失效")
raise Exception(f"获取排片数据失败: {data.get('msg', '未知错误')}")
def get_api_data_with_token_management(show_date):
token_data = load_token()
token = token_data.get("token") if token_data else None
if not token:
token_data = login_and_get_token()
if not token_data:
return None, None
token = token_data.get("token")
try:
schedule_list = fetch_schedule_data(token, show_date)
hall_seat_map = fetch_hall_info(token)
return schedule_list, hall_seat_map
except ValueError:
st.toast("Token 已失效,正在尝试重新登录并重试...", icon="🔄")
token_data = login_and_get_token()
if not token_data:
return None, None
token = token_data.get("token")
try:
schedule_list = fetch_schedule_data(token, show_date)
hall_seat_map = fetch_hall_info(token)
return schedule_list, hall_seat_map
except Exception as e:
st.error(f"重试获取数据失败: {e}")
return None, None
except Exception as e:
st.error(f"获取 API 数据时发生错误: {e}")
return None, None
@st.cache_data(show_spinner=False, ttl=600)
def fetch_canonical_movie_names(token, date_str):
if not CINEMA_ID:
return []
url = "https://app.bi.piao51.cn/cinema-app/mycinema/movieSellGross.action"
params = {
"token": token,
"startDate": date_str,
"endDate": date_str,
"dateType": "day",
"cinemaId": CINEMA_ID,
}
headers = {
"Host": "app.bi.piao51.cn",
"X-Requested-With": "XMLHttpRequest",
"jwt": "0",
"Accept": "application/json, text/javascript, */*; q=0.01",
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
}
try:
response = requests.get(url, params=params, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
if data.get("code") == "A00000" and data.get("results"):
return [
item["movieName"]
for item in data["results"]
if item.get("movieName") and item["movieName"] != "总计"
]
except Exception as e:
print(f"获取标准电影名称失败: {e}")
return []
def clean_movie_title(raw_title, canonical_names=None):
if not isinstance(raw_title, str):
return raw_title
base_name = None
if canonical_names:
sorted_names = sorted(canonical_names, key=len, reverse=True)
for name in sorted_names:
if name in raw_title:
base_name = name
break
if not base_name:
base_name = raw_title.split(" ", 1)[0]
raw_upper = raw_title.upper()
suffix = ""
if "HDR LED" in raw_upper:
suffix = "(HDR LED)"
elif "CINITY" in raw_upper:
suffix = "(CINITY)"
elif "杜比" in raw_upper or "DOLBY" in raw_upper:
suffix = "(杜比视界)"
elif "IMAX" in raw_upper:
suffix = "(数字IMAX3D)" if "3D" in raw_upper else "(数字IMAX)"
elif "巨幕" in raw_upper:
suffix = "(中国巨幕立体)" if "立体" in raw_upper else "(中国巨幕)"
elif "3D" in raw_upper:
suffix = "(数字3D)"
if suffix and suffix not in base_name:
return f"{base_name}{suffix}"
return base_name
def get_valid_token(force_refresh=False):
token_data = None if force_refresh else load_token()
if not token_data:
token_data = login_and_get_token()
if not token_data:
return None
return token_data.get("token")
def fetch_schedule_api_bundle(show_date):
"""
一次性获取排程相关 API 原始数据:
- getHallShowInfo(场次列表)
- getShowHallInfo(影厅座位映射)
- movieSellGross(标准影片名称)
"""
schedule_list, hall_seat_map = get_api_data_with_token_management(show_date)
if schedule_list is None or hall_seat_map is None:
return None
token_data = load_token()
token = token_data.get("token") if token_data else None
canonical_names = fetch_canonical_movie_names(token, show_date) if token else []
return {
"show_date": show_date,
"token": token,
"schedule_list": schedule_list,
"hall_seat_map": hall_seat_map,
"canonical_names": canonical_names,
}
def process_schedule_dataframe(schedule_list, hall_seat_map, canonical_names=None):
"""将排程 API 原始数据整理成便于展示的表格。"""
if not schedule_list:
return pd.DataFrame()
df = pd.DataFrame(schedule_list)
if df.empty:
return pd.DataFrame()
df["座位数"] = df["hallId"].map(hall_seat_map or {}).fillna(0).astype(int)
df.rename(
columns={
"movieName": "影片名称",
"showStartTime": "放映时间",
"soldBoxOffice": "总收入",
"soldTicketNum": "总人次",
"hallName": "影厅名称",
"showEndTime": "散场时间",
},
inplace=True,
)
if "影片名称" in df.columns:
df["影片名称_清洗后"] = df["影片名称"].apply(
lambda x: clean_movie_title(x, canonical_names)
)
required_cols = [
"影片名称",
"影片名称_清洗后",
"放映时间",
"散场时间",
"影厅名称",
"座位数",
"总收入",
"总人次",
]
for col in required_cols:
if col not in df.columns:
df[col] = None
df = df[required_cols]
for col in ["座位数", "总收入", "总人次"]:
df[col] = pd.to_numeric(df[col], errors="coerce").fillna(0)
return df