hengdian / cinema_api_client.py
Ethscriptions's picture
Upload cinema_api_client.py
8c4699d verified
import json
import os
import time
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import requests
from dotenv import load_dotenv
load_dotenv()
ROOT_DIR = Path(__file__).resolve().parent
TOKEN_FILE = ROOT_DIR / "token_data.json"
CINEMA_ID = os.getenv("CINEMA_ID")
DEFAULT_SELF_CINEMA_ID = "44001291"
SCHEDULE_API_TIMEOUT_SECONDS = 30
SCHEDULE_API_RETRY_ATTEMPTS = 2
API_BASE_URL = "https://cawapi.yinghezhong.com"
COMMON_HEADERS = {
"Host": "cawapi.yinghezhong.com",
"Accept": "*/*",
"Origin": "https://caw.yinghezhong.com",
"Connection": "keep-alive",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-site",
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
"Referer": "https://caw.yinghezhong.com/",
"Sec-Fetch-Dest": "empty",
"Accept-Language": "zh-CN,zh-Hans;q=0.9",
}
class RetryableAPIError(RuntimeError):
"""适合重试的接口异常。"""
def load_token() -> Optional[dict]:
if not TOKEN_FILE.exists():
return None
try:
return json.loads(TOKEN_FILE.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return None
def save_token(token_data: dict) -> bool:
try:
TOKEN_FILE.write_text(json.dumps(token_data, ensure_ascii=False, indent=2), encoding="utf-8")
return True
except OSError:
return False
def login_and_get_token() -> dict:
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]):
raise RuntimeError("未配置 CINEMA_USERNAME / CINEMA_PASSWORD / CINEMA_RES_CODE。")
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):
raise RetryableAPIError(f"票务登录失败,状态码:{response_login.status_code}")
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"]
save_token(token_data)
return token_data
raise RetryableAPIError(f"票务登录未获取到 Token:{user_info.get('msg')}")
except RetryableAPIError:
raise
except requests.exceptions.RequestException as exc:
raise RetryableAPIError(f"票务登录接口异常:{exc}") from exc
except Exception as exc:
raise RetryableAPIError(f"票务登录处理异常:{exc}") from exc
def get_valid_cinema_token() -> str:
token_data = load_token()
token = token_data.get("token") if token_data else None
if token:
return token
token_data = login_and_get_token()
token = token_data.get("token") if token_data else None
if not token:
raise RetryableAPIError("未获取到票务 Token。")
return token
def fetch_hall_info(token: str) -> Dict[str, int]:
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"}
try:
response = requests.get(url, params=params, headers=headers, timeout=10)
response.raise_for_status()
payload = response.json()
except requests.exceptions.RequestException as exc:
raise RetryableAPIError(f"获取影厅信息接口异常:{exc}") from exc
except Exception as exc:
raise RetryableAPIError(f"获取影厅信息处理异常:{exc}") from exc
if payload.get("code") == 1 and payload.get("data") is not None:
return {str(item["hallId"]): int(item.get("seatNum") or 0) for item in payload["data"]}
if payload.get("code") == 500:
raise ValueError("Token 可能已失效")
raise RetryableAPIError(f"获取影厅信息失败:{payload.get('msg', '未知错误')}")
def fetch_schedule_data(token: str, show_date: str) -> List[dict]:
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"}
last_request_error = None
for attempt in range(1, SCHEDULE_API_RETRY_ATTEMPTS + 1):
try:
response = requests.get(
url,
params=params,
headers=headers,
timeout=SCHEDULE_API_TIMEOUT_SECONDS,
)
response.raise_for_status()
payload = response.json()
except requests.exceptions.RequestException as exc:
last_request_error = exc
if attempt < SCHEDULE_API_RETRY_ATTEMPTS:
time.sleep(2)
continue
raise RetryableAPIError(f"获取 {show_date} 排片接口异常:{exc}") from exc
except Exception as exc:
raise RetryableAPIError(f"获取 {show_date} 排片处理异常:{exc}") from exc
if payload.get("code") == 1:
return payload.get("data", [])
if payload.get("code") == 500:
raise ValueError("Token 可能已失效")
if attempt < SCHEDULE_API_RETRY_ATTEMPTS:
time.sleep(2)
continue
raise RetryableAPIError(f"获取 {show_date} 排片失败:{payload.get('msg', '未知错误')}")
raise RetryableAPIError(f"获取 {show_date} 排片接口异常:{last_request_error}")
def fetch_schedule_data_by_cinema(token: str, cinema_id: str, show_date: str) -> List[dict]:
cinema_id = str(cinema_id or "").strip()
self_cinema_id = (CINEMA_ID or "").strip() or DEFAULT_SELF_CINEMA_ID
if cinema_id == self_cinema_id:
return fetch_schedule_data(token, show_date)
url = f"{API_BASE_URL}/competition/play/showInfoList"
params = {
"date": datetime.strptime(show_date, "%Y-%m-%d").strftime("%Y/%m/%d"),
"cinemaNum": cinema_id,
"token": token,
"_": str(int(time.time() * 1000)),
}
last_request_error = None
for attempt in range(1, SCHEDULE_API_RETRY_ATTEMPTS + 1):
try:
response = requests.get(
url,
params=params,
headers=COMMON_HEADERS,
timeout=SCHEDULE_API_TIMEOUT_SECONDS,
)
response.raise_for_status()
payload = response.json()
except requests.exceptions.RequestException as exc:
last_request_error = exc
if attempt < SCHEDULE_API_RETRY_ATTEMPTS:
time.sleep(2)
continue
raise RetryableAPIError(f"获取影院 {cinema_id}{show_date} 的竞对排片接口异常:{exc}") from exc
except Exception as exc:
raise RetryableAPIError(f"获取影院 {cinema_id}{show_date} 的竞对排片处理异常:{exc}") from exc
if payload.get("code") == 1:
return payload.get("data", []) or []
if payload.get("code") == 500:
raise ValueError("Token 可能已失效")
if attempt < SCHEDULE_API_RETRY_ATTEMPTS:
time.sleep(2)
continue
raise RetryableAPIError(
f"获取影院 {cinema_id}{show_date} 的竞对排片失败:{payload.get('msg', '未知错误')}"
)
raise RetryableAPIError(f"获取影院 {cinema_id}{show_date} 的竞对排片接口异常:{last_request_error}")
def fetch_available_movie_info(token: str, show_date: str) -> List[dict]:
url = "https://cawapi.yinghezhong.com/show/getMovieInfo"
params = {
"showDate": show_date,
"token": token,
"_": int(time.time() * 1000),
}
headers = {
"Origin": "https://caw.yinghezhong.com",
"Referer": "https://caw.yinghezhong.com/",
"User-Agent": "Mozilla/5.0",
}
try:
response = requests.get(url, params=params, headers=headers, timeout=15)
response.raise_for_status()
payload = response.json()
except requests.exceptions.RequestException as exc:
raise RetryableAPIError(f"获取可排片池接口异常:{exc}") from exc
except Exception as exc:
raise RetryableAPIError(f"获取可排片池处理异常:{exc}") from exc
if payload.get("code") == 1:
return payload.get("data", [])
if payload.get("code") == 500:
raise ValueError("Token 可能已失效")
raise RetryableAPIError(f"获取可排片池失败:{payload.get('msg', '未知错误')}")
def fetch_realtime_box_office(token: str, date_str: str) -> dict:
url = "https://app.bi.piao51.cn/cinema-app/market/realtimeDailyBoxOffice.action"
params = {
"qTime": date_str,
"token": token,
}
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=15)
response.raise_for_status()
payload = response.json()
except requests.exceptions.RequestException as exc:
raise RetryableAPIError(f"获取 {date_str} 全国大盘接口异常:{exc}") from exc
except Exception as exc:
raise RetryableAPIError(f"获取 {date_str} 全国大盘处理异常:{exc}") from exc
if payload.get("code") == "A00000":
return payload.get("results", {}) or {}
if "login" in response.text or payload.get("code") in {500, "500"}:
raise ValueError("Token 可能已失效")
raise RetryableAPIError(f"获取 {date_str} 全国大盘失败:{payload.get('msg', '未知错误')}")
def fetch_canonical_movie_names(token: str, date_str: str) -> List[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:
return []
return []
def get_schedule_and_hall_info(show_date: str) -> Tuple[List[dict], Dict[str, int], str]:
token = get_valid_cinema_token()
try:
schedule = fetch_schedule_data(token, show_date)
hall_map = fetch_hall_info(token)
return schedule, hall_map, token
except ValueError:
token_data = login_and_get_token()
token = token_data.get("token") if token_data else None
if not token:
raise RetryableAPIError("重新登录后仍未获取到票务 Token。")
schedule = fetch_schedule_data(token, show_date)
hall_map = fetch_hall_info(token)
return schedule, hall_map, token
def get_schedule_by_cinema_with_token_management(cinema_id: str, show_date: str) -> Tuple[List[dict], str]:
token = get_valid_cinema_token()
try:
return fetch_schedule_data_by_cinema(token, cinema_id, show_date), token
except ValueError:
token_data = login_and_get_token()
token = token_data.get("token") if token_data else None
if not token:
raise RetryableAPIError("重新登录后仍未获取到票务 Token。")
return fetch_schedule_data_by_cinema(token, cinema_id, show_date), token
def get_available_movies_with_token_management(show_date: str) -> Tuple[List[dict], str]:
token = get_valid_cinema_token()
try:
return fetch_available_movie_info(token, show_date), token
except ValueError:
token_data = login_and_get_token()
token = token_data.get("token") if token_data else None
if not token:
raise RetryableAPIError("重新登录后仍未获取到票务 Token。")
return fetch_available_movie_info(token, show_date), token
def get_realtime_box_office_with_token_management(date_str: str) -> Tuple[dict, str]:
token = get_valid_cinema_token()
try:
return fetch_realtime_box_office(token, date_str), token
except ValueError:
token_data = login_and_get_token()
token = token_data.get("token") if token_data else None
if not token:
raise RetryableAPIError("重新登录后仍未获取到票务 Token。")
return fetch_realtime_box_office(token, date_str), token