""" MoviePilot Cookie功能示例程序 从CookieCloud下载数据并注入浏览器访问网站 """ import json import time import hashlib import base64 from typing import Dict, List, Optional, Tuple from dataclasses import dataclass import urllib.request import urllib.error from Crypto.Cipher import AES @dataclass class CookieCloudConfig: """CookieCloud配置""" server: str username: str password: str timeout: int = 30 class CookieCloudDownloader: """从CookieCloud服务器下载cookie数据""" def __init__(self, config: CookieCloudConfig): self.config = config def _get_crypt_key(self) -> bytes: combined_string = f"{self.config.username}-{self.config.password}" return hashlib.md5(combined_string.encode('utf-8')).hexdigest()[:16].encode("utf-8") def _bytes_to_key(self, data: bytes, salt: bytes, output: int = 48) -> bytes: assert len(salt) == 8, len(salt) data += salt key = hashlib.md5(data).digest() final_key = key while len(final_key) < output: key = hashlib.md5(key + data).digest() final_key += key return final_key[:output] def _decrypt(self, encrypted: str, passphrase: bytes) -> bytes: encrypted_bytes = base64.b64decode(encrypted) assert encrypted_bytes.startswith(b"Salted__"), "Invalid encrypted data format" salt = encrypted_bytes[8:16] key_iv = self._bytes_to_key(passphrase, salt, 32 + 16) key = key_iv[:32] iv = key_iv[32:] aes = AES.new(key, AES.MODE_CBC, iv) decrypted_padded = aes.decrypt(encrypted_bytes[16:]) padding_length = decrypted_padded[-1] if isinstance(padding_length, str): padding_length = ord(padding_length) return decrypted_padded[:-padding_length] def _get_url_domain(self, domain: str) -> str: if not domain: return "" domain = domain.lstrip('.') if ":" in domain: domain = domain.split(":")[0] parts = domain.split(".") if all(part.isdigit() for part in parts): return domain if len(parts) >= 2: return ".".join(parts[-2:]) return domain def download_all(self) -> Tuple[Optional[Dict], str]: """下载所有cookie和local storage数据""" try: url = f"{self.config.server}/get/{self.config.username}" request = urllib.request.Request( url, headers={ 'Content-Type': 'application/json', 'User-Agent': 'MoviePilot-Cookie-Client/1.0' }, method='GET' ) response = urllib.request.urlopen(request, timeout=self.config.timeout) if response.status != 200: return None, f"服务器返回错误状态码: {response.status}" result = json.loads(response.read().decode('utf-8')) if not result: return None, "服务器返回数据为空" encrypted = result.get("encrypted") if not encrypted: return None, "未获取到cookie密文" crypt_key = self._get_crypt_key() try: decrypted_data = self._decrypt(encrypted, crypt_key) result = json.loads(decrypted_data.decode("utf-8")) except Exception as e: return None, f"cookie解密失败: {str(e)}" if not result: return None, "cookie解密为空" cookie_data = result.get("cookie_data", {}) local_storage_data = result.get("local_storage_data", {}) total_cookies = sum(len(v) for v in cookie_data.values()) total_ls = sum(len(v) for v in local_storage_data.values()) print(f" 下载完成: {len(cookie_data)} 个域名的Cookie({total_cookies}个), {len(local_storage_data)} 个域名的Local Storage({total_ls}个)") return {"cookies": cookie_data, "local_storage": local_storage_data}, "" except urllib.error.HTTPError as e: if e.code == 401: return None, "认证失败,请检查用户名和密码" else: return None, f"HTTP错误: {e.code} {e.reason}" except urllib.error.URLError as e: return None, f"网络连接失败: {e.reason}" except Exception as e: return None, f"下载失败: {str(e)}" class BrowserController: """使用Playwright控制浏览器""" def __init__(self, headless: bool = False, use_edge: bool = False): self.headless = headless self.use_edge = use_edge self.browser = None self.context = None self.page = None self.playwright = None def start(self) -> bool: try: from playwright.sync_api import sync_playwright self.playwright = sync_playwright().start() if self.use_edge: self.browser = self.playwright.chromium.launch( channel="msedge", headless=self.headless, args=['--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage', '--no-sandbox'] ) else: self.browser = self.playwright.chromium.launch( headless=self.headless, args=['--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage', '--no-sandbox'] ) self.context = self.browser.new_context( user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' ) self.page = self.context.new_page() print(f"✓ 浏览器启动成功 ({'Edge' if self.use_edge else 'Chromium'})") return True except Exception as e: print(f"✗ 浏览器启动失败: {str(e)}") return False def inject_cookies(self, cookies: Dict): """注入cookie到浏览器""" if not self.context or not cookies: return try: playwright_cookies = [] for domain_key, cookie_list in cookies.items(): for cookie in cookie_list: cookie_domain = cookie.get("domain", "").lstrip('.') if not cookie_domain: cookie_domain = domain_key.lstrip('.') pw_cookie = { "name": cookie.get("name", ""), "value": cookie.get("value", ""), "domain": cookie_domain, "path": cookie.get("path", "/"), } if cookie.get("secure"): pw_cookie["secure"] = True if cookie.get("httpOnly"): pw_cookie["httpOnly"] = True if cookie.get("expirationDate"): pw_cookie["expires"] = int(cookie.get("expirationDate")) playwright_cookies.append(pw_cookie) self.context.add_cookies(playwright_cookies) print(f"✓ Cookie注入成功 ({len(playwright_cookies)} 个)") except Exception as e: print(f"✗ Cookie注入失败: {str(e)}") def navigate(self, url: str, wait_time: int = 5, timeout: int = 60) -> bool: if not self.page: return False try: print(f"正在访问: {url}") self.page.goto(url, timeout=timeout * 1000) try: self.page.wait_for_load_state("networkidle", timeout=timeout * 1000) except Exception: print(" networkidle超时,尝试load状态...") try: self.page.wait_for_load_state("load", timeout=30000) except Exception: print(" load超时,尝试domcontentloaded状态...") self.page.wait_for_load_state("domcontentloaded", timeout=10000) time.sleep(wait_time) print(f"✓ 页面加载完成") return True except Exception as e: print(f"✗ 页面访问失败: {str(e)}") return False def check_login_status(self, login_indicator: str) -> bool: if not self.page: return False try: html_content = self.page.content() if login_indicator: if login_indicator in html_content: print(f"✓ 检测到登录指示器: {login_indicator}") return True else: print(f"✗ 未检测到登录指示器: {login_indicator}") return False return False except Exception as e: print(f"✗ 登录状态检查失败: {str(e)}") return False def take_screenshot(self, filepath: str): if not self.page: return try: self.page.screenshot(path=filepath) print(f"✓ 截图已保存: {filepath}") except Exception as e: print(f"✗ 截图失败: {str(e)}") def close(self): try: if self.browser: self.browser.close() if self.playwright: self.playwright.stop() print("✓ 浏览器已关闭") except Exception as e: print(f"✗ 关闭浏览器失败: {str(e)}") class CookieDemo: """Cookie功能示例""" def __init__(self, cookiecloud_config: CookieCloudConfig): self.downloader = CookieCloudDownloader(cookiecloud_config) self.browser = None def run_sites(self, sites: list, headless: bool = False, use_edge: bool = False): """下载一次数据,依次访问多个网站""" # 下载数据 print("=" * 60) print("从CookieCloud下载数据") print("=" * 60) data, error = self.downloader.download_all() if error: print(f"✗ 数据下载失败: {error}") return # 启动浏览器 print(f"\n启动浏览器") print("-" * 60) self.browser = BrowserController(headless=headless, use_edge=use_edge) if not self.browser.start(): return # 注入所有cookie if data and data.get("cookies"): print(f"\n注入所有cookie到浏览器") print("-" * 60) self.browser.inject_cookies(data["cookies"]) # 依次访问每个网站 for idx, site in enumerate(sites, 1): print(f"\n\n{'=' * 60}") print(f"访问网站 {idx}/{len(sites)}: {site['name']}") print(f"URL: {site['url']}") print("=" * 60) if not self.browser.navigate(site['url']): print(f"✗ 页面访问失败") continue # 验证登录状态 from urllib.parse import urlparse parsed = urlparse(site['url']) target_domain = parsed.netloc login_indicator = site.get('login_indicator') is_logged_in = self.browser.check_login_status(login_indicator) if is_logged_in: print("✓ Cookie验证成功:已登录状态") else: print("⚠ Cookie验证失败:未检测到登录状态") # 截图 screenshot_path = f"{target_domain.replace('.', '_').replace(':', '_')}_screenshot.png" self.browser.take_screenshot(screenshot_path) if idx < len(sites): print("\n等待3秒后继续下一个网站...") time.sleep(3) # 关闭浏览器 print(f"\n关闭浏览器") print("-" * 60) self.browser.close() print("\n" + "=" * 60) print("所有网站访问完成") print("=" * 60) def main(): # 配置CookieCloud config = CookieCloudConfig( server="http://192.168.1.100:3000/cookiecloud", username="qyZpTrgiP5mVwiRZwfBhjz", password="iGn1B4FnWftp3oj4Ko3jxA", timeout=30 ) # 创建示例程序 demo = CookieDemo(config) # 测试网站列表 test_sites = [ { "url": "https://lmkbi.95155.com/bi-system/#/login", "name": "BI系统", "login_indicator": "彭峰" }, { "url": "http://192.168.1.100:3000/#/subscribe/movie", "name": "MoviePilot订阅", "login_indicator": "搜索" }, { "url": "http://192.168.1.102:8418/bwadmin/QueryCarInfo2", "name": "QueryCarInfo2", "login_indicator": "工单管理" } ] # 下载一次数据,依次访问所有网站 demo.run_sites( sites=test_sites, headless=False, use_edge=True ) if __name__ == "__main__": main()