Spaces:
Running
Running
| 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 | |