diff --git a/icons/playlet-fortune-wheel.png b/icons/playlet-fortune-wheel.png deleted file mode 100644 index d329492..0000000 Binary files a/icons/playlet-fortune-wheel.png and /dev/null differ diff --git a/package.json b/package.json index 194733d..ed6ab9d 100644 --- a/package.json +++ b/package.json @@ -1052,18 +1052,5 @@ "author": "cddjr", "level": 1, "v2": true - }, - "PlayletFortuneWheel": { - "name": "PlayLet幸运大转盘", - "description": "每日自动抽奖,坚持抽奖,越抽越幸运...", - "labels": "站点", - "version": "1.1.0", - "icon": "playlet-fortune-wheel.png", - "author": "ArvinChen9539", - "level": 1, - "v2": true, - "history": { - "v1.1.0": "修复抽中彩虹id时报错的问题\n修复抽奖发生异常时没有提示和终止的问题" - } } } diff --git a/plugins/playletfortunewheel/__init__.py b/plugins/playletfortunewheel/__init__.py deleted file mode 100644 index 7b6758a..0000000 --- a/plugins/playletfortunewheel/__init__.py +++ /dev/null @@ -1,936 +0,0 @@ -import pytz -import requests -import re -import time - -from datetime import datetime, timedelta -from typing import Any, List, Dict, Tuple, Optional - -from apscheduler.triggers.cron import CronTrigger -from apscheduler.schedulers.background import BackgroundScheduler - -from app.log import logger -from app.core.config import settings -from app.plugins import _PluginBase -from app.schemas import NotificationType -from app.db.site_oper import SiteOper - - -class PlayletFortuneWheel(_PluginBase): - # 插件名称 - plugin_name = "Playlet幸运大转盘" - # 插件描述 - plugin_desc = "每日抽奖,越抽越有" - # 插件图标 - plugin_icon = "playlet-fortune-wheel.png" - # 插件版本 - plugin_version = "1.1.0" - # 插件作者 - plugin_author = "ArvinChen9539" - # 作者主页 - author_url = "https://github.com/ArvinChen9539" - # 插件配置项ID前缀 - plugin_config_prefix = "playletfortunewheel_" - # 加载顺序 - plugin_order = 25 - # 可使用的用户级别 - auth_level = 2 - - # 基本设置 - _enabled: bool = False - _onlyonce: bool = False - _notify: bool = True - _use_proxy: bool = False - _auto_cookie: bool = True - - # 只抽免费 - _only_free: bool = False - - # 保存最后一次抽奖报告 - _last_report: Optional[str] = None - - # 参数 - _cookie: Optional[str] = None - _cron: Optional[str] = None - _max_raffle_num: Optional[int] = None - - _site_url: str = "https://playletpt.xyz/" - - # 定时器 - _scheduler: Optional[BackgroundScheduler] = None - - # 站点操作实例 - _siteoper = None - - def init_plugin(self, config: Optional[dict] = None) -> None: - """ - 初始化插件 - """ - # 停止现有任务 - self.stop_service() - - # 创建站点操作实例 - self._siteoper = SiteOper() - - if config: - self._enabled = config.get("enabled", False) - self._cron = config.get("cron", '0 9 * * *') - self._max_raffle_num = config.get("max_raffle_num") - self._cookie = config.get("cookie") - self._notify = config.get("notify", True) - self._onlyonce = config.get("onlyonce", False) - self._use_proxy = config.get("use_proxy", False) - self._only_free = config.get("only_free", False) - self._auto_cookie = config.get("auto_cookie", True) - self._last_report = config.get("last_report") - - # 处理自动获取cookie - if self._auto_cookie: - self._cookie = self.get_site_cookie() - else: - self._cookie = config.get("cookie") - - if self._onlyonce: - try: - self._scheduler = BackgroundScheduler(timezone=settings.TZ) - logger.info(f"Playlet幸运大转盘服务启动,立即运行一次") - - # 执行每日任务 - self._scheduler.add_job(func=self._auto_task, trigger='date', - run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3), - name="Playlet幸运大转盘-自动执行") - - # 关闭一次性开关 - self._onlyonce = False - self.update_config({ - "onlyonce": False, - "cron": self._cron, - "max_raffle_num": self._max_raffle_num, - "enabled": self._enabled, - "cookie": self._cookie, - "notify": self._notify, - "use_proxy": self._use_proxy, - "only_free": self._only_free, - "auto_cookie": self._auto_cookie, - "last_report": self._last_report - }) - - # 启动任务 - if self._scheduler.get_jobs(): - self._scheduler.print_jobs() - self._scheduler.start() - except Exception as e: - logger.error(f"Playlet幸运大转盘服务启动失败: {str(e)}") - - # 清理Cookie无效值 - @staticmethod - def clean_cookie_value(cookie_value): - # 移除前导和尾随空白字符 - cleaned = cookie_value.strip() - # 移除非法字符 - cleaned = ''.join(char for char in cleaned if char not in ['\r', '\n']) - return cleaned - - # 执行抽奖 - def exec_raffle(self): - raffle_url = self._site_url + "/fortune-wheel-spin.php" - - # content-type: multipart/form-data - self.headers = { - "cookie": self.clean_cookie_value(self._cookie), - "referer": self._site_url, - # "content-type": "multipart/form-data", - "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0.0" - } - - results = [] - - # 获取代理设置 - proxies = self._get_proxies() - - response = requests.get(self._site_url + "/fortune-wheel.php", headers=self.headers, proxies=proxies) - response_data = response.text - # 正则截取id="free-count">和
之间的字符串 - free_count_html = re.search(r'id="free-count">(.*?)
', response_data) - today_count_html = re.search(r'id="today-count">(.*?)', response_data) - free_count = 0 - today_num_str = '' - if free_count_html: - free_count = int(free_count_html.group(1)) - - if today_count_html: - today_num_str = today_count_html.group(1) - - if not today_num_str: - logger.error(f"登录异常") - return results - # 将today_num_str 拆分成今日次数和已用次数两个数字变量 字符串的格式为 "今日次数 / 已用次数" - used_count, today_count = map(int, today_num_str.split("/")) - # 今日剩余次数 - remain_count = today_count - used_count - logger.info(f"免费抽奖次数:{free_count},今日剩余次数:{remain_count},已用抽奖次数:{used_count}") - - if self._only_free: - exec_count = free_count - logger.info(f"使用剩余免费次数:{exec_count}") - else: - if not self._max_raffle_num or int(self._max_raffle_num) >= remain_count: - exec_count = remain_count - logger.info(f"使用剩余抽奖次数:{exec_count}") - else: - exec_count = int(self._max_raffle_num) - logger.info(f"使用最大抽奖次数:{exec_count}") - - if exec_count > 0: - # 只能进行1次 10次 20次 50次的抽取 需要把exec_count转换为调用多次 - all_results = [] - - while exec_count > 0: - num = 1 - if exec_count >= 50: - num = 50 - elif exec_count >= 20: - num = 20 - elif exec_count >= 10: - num = 10 - - - # 解析返回结果 - try: - logger.info(f"执行抽奖次数{num}") - response = requests.post(raffle_url, headers=self.headers, files={"count": (None, num)}, - proxies=proxies) - response_json = response.json() - flag = response_json.get("success", False) - if not flag: - logger.error(f"抽奖失败: {str(response_json)}") - error_msg = response_json.get("message", "未知错误") - results = self.process_raffle_results({"success": True, "results": all_results}) - results.append("") - results.append(f"❌ 抽奖失败: {error_msg}") - results.append("") - results.append(f"🎯 剩余次数: {remain_count - len(all_results)}") - return results - - # 累积结果 - all_results.extend(response_json["results"]) - exec_count -= num - logger.info(f"抽奖成功") - except Exception as e: - logger.error(f"转换接口返回数据时异常: {str(e)}",e) - results = self.process_raffle_results({"success": True, "results": all_results}) - results.append("") - results.append(f"❌ 执行异常: {str(e)}") - return results - - # 间隔2秒后执行 - time.sleep(2) - - results = self.process_raffle_results({"success": True, "results": all_results}) - - else: - logger.info(f"抽奖次数已用完") - - return results - - def process_raffle_results(self, response_data: dict) -> List[str]: - results = [] - - if not response_data.get("success", False): - error_msg = response_data.get("message", "未知错误") - results.append(f"❌ 抽奖失败: {error_msg}") - return results - - # 获取抽奖结果列表 - raffle_results = response_data.get("results", []) - - if not raffle_results: - results.append("ℹ️ 暂无抽奖结果") - return results - - # 分类统计各类奖励 - prize_stats = {} - grade_stats = {} - total_count = len(raffle_results) - win_count = 0 # 中奖次数(非"谢谢参与") - - # 图标映射 - type_icons = { - "upload": "📤", - "attendance_card": "📋", - "vip": "⭐", - "bonus": "💎", - "nothing": "😞", - "invite_perm": "🎉", - "invite_temp": "🎉", - "rainbow_id" : "🌈", - } - type_name = { - "upload": "流量", - "attendance_card": "道具", - "vip": "会员", - "bonus": "魔力", - "nothing": "谢谢参与", - "invite_perm": "永久邀请", - "invite_temp": "临时邀请", - "rainbow_id" : "彩虹ID" - } - - grade_icons = { - "1": "🥇", - "2": "🥈", - "3": "🥉", - "4": "🏅", - "5": "🏅", - "6": "🏅", - "7": "🎖️", - "8": "🎖️", - "9": "🎖️", - "10": "🎗️", - "11": "🎗️", - "12": "🎗️" - } - - # 统计数据 - for item in raffle_results: - result = item.get("result", {}) - prize = item.get("prize", {}) - grade = item.get("grade", "未知等级") - - # 提取等级数字 - grade_num = re.search(r'(\d+)等奖', grade) - grade_key = grade_num.group(1) if grade_num else "未知" - - # 统计等级分布 - grade_stats[grade] = grade_stats.get(grade, 0) + 1 - - # 统计奖励类型 - status = result.get("status", "") - if status == "nothing": - prize_type = "nothing" - prize_name = "谢谢参与" - else: - prize_type = result.get("type", "unknown") - prize_name = prize.get("name", "未知奖励") - win_count += 1 - - # 按奖励类型统计 - if prize_type not in prize_stats: - prize_stats[prize_type] = { - "count": 0, - "details": {}, - "icon": type_icons.get(prize_type, "🎁") - } - - prize_stats[prize_type]["count"] += 1 - - # 统计具体奖励详情 - if status != "nothing": - value = result.get("value", 0) - unit = result.get("unit", "") - detail_key = f"{prize_name} ({unit})" - - if detail_key not in prize_stats[prize_type]["details"]: - prize_stats[prize_type]["details"][detail_key] = { - "count": 0, - "total_value": 0 - } - - prize_stats[prize_type]["details"][detail_key]["count"] += 1 - prize_stats[prize_type]["details"][detail_key]["total_value"] += value - - # 生成报告 - results.append(f"🎰 总抽奖次数: {total_count}") - results.append(f"🎯 中奖次数: {win_count}") - results.append(f"💔 谢谢参与: {total_count - win_count}") - - if win_count > 0: - win_rate = (win_count / total_count) * 100 - results.append(f"📊 中奖率: {win_rate:.1f}%") - - # 添加分隔线 - results.append("─" * 40) - - # 按奖励类型展示详情 - results.append("🏆 奖励详情:") - for prize_type, stat in prize_stats.items(): - if prize_type == "nothing": - continue - - icon = stat["icon"] - count = stat["count"] - results.append(f" {icon} {type_name.get(prize_type,'未知') or prize_type.upper()} 类奖励 ({count}次)") - - for detail, info in stat["details"].items(): - total_value = info["total_value"] - detail_count = info["count"] - results.append(f" 🎁 {detail}: {total_value} ({detail_count}次)") - - results.append("") - - # 添加分隔线 - results.append("─" * 40) - - # 等级分布统计 - results.append("🏅 等级分布:") - # 按等级排序显示 - sorted_grades = sorted(grade_stats.items(), - key=lambda x: int(re.search(r'(\d+)等奖', x[0]).group(1)) if re.search(r'(\d+)等奖', - x[0]) else 99) - - for grade, count in sorted_grades: - grade_num = re.search(r'(\d+)等奖', grade) - if grade_num: - grade_key = grade_num.group(1) - icon = grade_icons.get(grade_key, "🎗️") - else: - icon = "❓" - results.append(f" {icon} {grade}: {count}次") - - return results - - def _auto_task(self): - """ - 执行每日自动抽奖 - """ - try: - logger.info("执行每日自动抽奖") - results = self.exec_raffle() # 免费次数 - - # 生成报告 - if results: - report = self.generate_report(results) - - # 发送通知 - if self._notify: - self.post_message( - mtype=NotificationType.SiteMessage, - title="【Playlet幸运大转盘】每日任务完成", - text=report) - self._last_report = report - self.update_config({ - "onlyonce": False, - "cron": self._cron, - "max_raffle_num": self._max_raffle_num, - "enabled": self._enabled, - "cookie": self._cookie, - "notify": self._notify, - "use_proxy": self._use_proxy, - "only_free": self._only_free, - "auto_cookie": self._auto_cookie, - "last_report": self._last_report - }) - logger.info(f"每日抽奖任务完成:\n{report}") - else: - logger.info("抽奖次数已用完,未发送通知") - - except Exception as e: - logger.error(f"执行每日抽奖任务时发生异常: {str(e)}") - logger.error("异常详情: ", exc_info=True) - - def generate_report(self, results: List[str]) -> str: - """ - 生成完整的抽奖报告 - :param results: 抽奖结果列表 - :return: 格式化的报告文本 - """ - try: - if not results: - return "ℹ️ 没有抽奖次数" - - # 生成报告 - report = "🎮 Playlet幸运大转盘抽奖报告\n" - report += "━━━━━━━━━━━━━━\n" - - # 添加抽奖结果 - report += "\n".join(results) - - # 添加时间戳 - report += f"\n\n⏱️ 抽奖时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - - return report - - except Exception as e: - logger.error(f"生成报告时发生异常: {str(e)}") - return "❌ 生成报告时发生错误,请检查日志以获取更多信息。" - - def _get_proxies(self): - """ - 获取代理设置 - """ - if not self._use_proxy: - logger.info("未启用代理") - return None - - try: - # 获取系统代理设置 - if hasattr(settings, 'PROXY') and settings.PROXY: - logger.info(f"使用系统代理: {settings.PROXY}") - return settings.PROXY - else: - logger.warning("系统代理未配置") - return None - except Exception as e: - logger.error(f"获取代理设置出错: {str(e)}") - return None - - def get_site_cookie(self, domain: str = 'playletpt.xyz') -> str: - """ - 获取站点cookie - - Args: - domain: 站点域名,默认为织梦站点 - - Returns: - str: 有效的cookie字符串,如果获取失败则返回空字符串 - """ - try: - # 优先使用手动配置的cookie - if self._cookie: - if str(self._cookie).strip().lower() == "cookie": - logger.warning("手动配置的cookie无效") - return "" - return self._cookie - - # 如果手动配置的cookie无效,则从站点配置获取 - site = self._siteoper.get_by_domain(domain) - if not site: - logger.warning(f"未找到站点: {domain}") - return "" - - cookie = site.cookie - if not cookie or str(cookie).strip().lower() == "cookie": - logger.warning(f"站点 {domain} 的cookie无效") - return "" - - # 将获取到的cookie保存到实例变量 - self._cookie = cookie - return cookie - - except Exception as e: - logger.error(f"获取站点cookie失败: {str(e)}") - return "" - - def get_state(self) -> bool: - """获取插件状态""" - return bool(self._enabled) - - @staticmethod - def get_command() -> List[Dict[str, Any]]: - """获取命令""" - pass - - def get_api(self) -> List[Dict[str, Any]]: - """获取API""" - pass - - def get_page(self) -> List[dict]: - """数据页面""" - pass - - def get_service(self) -> List[Dict[str, Any]]: - """ - 注册插件公共服务 - """ - service = [] - if self._cron: - service.append({ - "id": "autoPlayletFortuneWheel", - "name": "Playlet幸运大转盘 - 自动执行", - "trigger": CronTrigger.from_crontab(self._cron), - "func": self._auto_task, - "kwargs": {} - }) - - if service: - return service - - def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: - """ - 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 - """ - # 动态判断MoviePilot版本,决定定时任务输入框组件类型 - version = getattr(settings, "VERSION_FLAG", "v1") - cron_field_component = "VCronField" if version == "v2" else "VTextField" - return [ - { - 'component': 'VForm', - 'content': [ - # 基本设置 - { - 'component': 'VCard', - 'props': { - 'variant': 'flat', - 'class': 'mb-6', - 'color': 'surface' - }, - 'content': [ - { - 'component': 'VCardItem', - 'props': { - 'class': 'pa-6' - }, - 'content': [ - { - 'component': 'VCardTitle', - 'props': { - 'class': 'd-flex align-center text-h6' - }, - 'content': [ - { - 'component': 'VIcon', - 'props': { - 'style': 'color: #16b1ff', - 'class': 'mr-3', - 'size': 'default' - }, - 'text': 'mdi-cog' - }, - { - 'component': 'span', - 'text': '基本设置' - } - ] - } - ] - }, - { - 'component': 'VCardText', - 'props': { - 'class': 'px-6 pb-6' - }, - 'content': [ - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'sm': 3 - }, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'enabled', - 'label': '启用插件', - 'color': 'primary', - 'hide-details': True - } - } - ] - }, - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'sm': 3 - }, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'use_proxy', - 'label': '使用代理', - 'color': 'primary', - 'hide-details': True - } - } - ] - }, - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'sm': 3 - }, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'notify', - 'label': '开启通知', - 'color': 'primary', - 'hide-details': True - } - } - ] - }, - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'sm': 3 - }, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'onlyonce', - 'label': '立即运行一次', - 'color': 'primary', - 'hide-details': True - } - } - ] - } - ] - } - ] - } - ] - }, - # 功能设置 - { - 'component': 'VCard', - 'props': { - 'variant': 'flat', - 'class': 'mb-6', - 'color': 'surface' - }, - 'content': [ - { - 'component': 'VCardItem', - 'props': { - 'class': 'pa-6' - }, - 'content': [ - { - 'component': 'VCardTitle', - 'props': { - 'class': 'd-flex align-center text-h6' - }, - 'content': [ - { - 'component': 'VIcon', - 'props': { - 'style': 'color: #16b1ff', - 'class': 'mr-3', - 'size': 'default' - }, - 'text': 'mdi-tools' - }, - { - 'component': 'span', - 'text': '功能设置' - } - ] - } - ] - }, - { - 'component': 'VCardText', - 'props': { - 'class': 'px-6 pb-6' - }, - 'content': [ - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'sm': 3 - }, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'auto_cookie', - 'label': '使用站点Cookie', - 'color': 'primary', - 'hide-details': True - } - } - ] - }, - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'sm': 3 - }, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'only_free', - 'label': '只抽免费', - 'color': 'primary', - 'hide-details': True - } - } - ] - }, - ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'sm': 4 - }, - 'content': [ - { - 'component': 'VTextField', - 'props': { - 'model': 'cookie', - 'label': '站点Cookie', - 'variant': 'outlined', - 'color': 'primary', - 'hide-details': True, - 'class': 'mt-2', - 'disabled': 'auto_cookie' - } - } - ] - }, - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'sm': 4 - }, - 'content': [ - { - 'component': cron_field_component, # 动态切换 - 'props': { - 'model': 'cron', - 'label': '执行周期(cron)', - 'variant': 'outlined', - 'color': 'primary', - 'hide-details': True, - 'placeholder': '默认每天执行', - 'class': 'mt-2' - } - } - ] - }, - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'sm': 4 - }, - 'content': [ - { - 'component': "VTextField", # 动态切换 - 'props': { - 'model': 'max_raffle_num', - 'label': '最大抽奖次数', - 'variant': 'outlined', - 'color': 'primary', - 'hide-details': True, - 'placeholder': '默认全部抽完', - 'class': 'mt-2' - } - } - ] - } - ] - } - ] - } - ] - }, - # 使用说明 - { - 'component': 'VCard', - 'props': { - 'variant': 'flat', - 'class': 'mb-6', - 'color': 'surface' - }, - 'content': [ - { - 'component': 'VCardItem', - 'props': { - 'class': 'pa-6' - }, - 'content': [ - { - 'component': 'VCardTitle', - 'props': { - 'class': 'd-flex align-center text-h6' - }, - 'content': [ - { - 'component': 'VIcon', - 'props': { - 'style': 'color: #16b1ff', - 'class': 'mr-3', - 'size': 'default' - }, - 'text': 'mdi-treasure-chest' - }, - { - 'component': 'span', - 'text': '最后一次抽奖报告' - } - ] - } - ] - }, - { - 'component': 'VCardText', - 'props': { - 'class': 'px-6 pb-6' - }, - 'content': [ - { - 'component': 'div', - 'props': { - 'class': 'text-body-1' - }, - 'content': [ - { - 'component': 'div', - 'props': { - 'class': 'mb-4 text-pre-wrap' - }, - 'content': [ - { - 'component': 'div', - 'class': 'text-subtitle-1 font-weight-bold mb-2 ', - 'text': self._last_report or '暂无数据,可以点击立即运行一次查看' - }, - ] - }, - ] - } - ] - } - ] - } - ] - } - ], { - "enabled": False, - "onlyonce": False, - "notify": True, - "use_proxy": False, - "only_free": False, - "cookie": "", - "auto_cookie": True, - "cron": "0 9 * * *", - "max_raffle_num": None, - "last_report": "", - } - - def stop_service(self) -> None: - """ - 退出插件 - """ - try: - if self._scheduler: - self._scheduler.remove_all_jobs() - if self._scheduler.running: - self._scheduler.shutdown() - self._scheduler = None - except Exception as e: - logger.error("退出插件失败:%s" % str(e))