diff --git a/README.md b/README.md index 294cfe7..c8015f3 100644 --- a/README.md +++ b/README.md @@ -80,3 +80,4 @@ _✨ 从哔哩哔哩会员购获取展览简易信息 ✨_ | 配置项 | 必填 | 默认值 | 说明 | | :---------------: | :--: | :----: | :----------------------------------------------------------: | | ACGNSHOW_PAGESIZE | 否 | 8 | 单个图片的条目数,最大为 20,条目数过大可能导致 Bot 无法发送 | +| ACGNSHOW_BGIMAGE_PATH | 否 | 插件内置背景图 | 插件返回图片的背景图目录路径 | \ No newline at end of file diff --git a/nonebot_plugin_acgnshow/__init__.py b/nonebot_plugin_acgnshow/__init__.py index c50cd57..3c2abde 100644 --- a/nonebot_plugin_acgnshow/__init__.py +++ b/nonebot_plugin_acgnshow/__init__.py @@ -10,14 +10,19 @@ usage = """命令格式: 展览 <地区> [页码] 或 <地区>展览 [页码] +获取指定地区的展览列表 其中地区为省级行政区或地级行政区(不包含后缀) (如北京,福建,平顶山,绍兴,香港...,或海外/全国) +展览详情 +其中ID为展览列表处返回的ID + 示例: 展览 福建 2 福建展览 2 全国展览 海外展览 +展览详情 86827 ※数据来源于哔哩哔哩会员购,由于API返回结果与实际存在差异,数据可能不准确。""" __author__ = "Asankilp" diff --git a/nonebot_plugin_acgnshow/acgnapis.py b/nonebot_plugin_acgnshow/acgnapis.py index 098eb0e..502707b 100644 --- a/nonebot_plugin_acgnshow/acgnapis.py +++ b/nonebot_plugin_acgnshow/acgnapis.py @@ -7,6 +7,7 @@ from .util import * CITY_API_ROOT = "https://show.bilibili.com/api/ticket/city/list?channel=3" SHOWS_API_ROOT = "https://show.bilibili.com/api/ticket/project/listV2" +SHOW_DETAILS_API_ROOT = "https://show.bilibili.com/api/ticket/project/getV2" HEADERS = { "user-agent": "Mozilla/5.0 (Linux; Android 14; 114514YAJU Build/UKQ1.114514.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/125.0.6422.165 Mobile Safari/537.36 BiliApp/7810200 mobi_app/android isNotchWindow/0 NotchHeight=34 mallVersion/7810200 mVersion/242 disable_rcmd/0 7.81.0 os/android model/114514YAJU mobi_app/android build/7810200 channel/bilih5 innerVer/7810210 osVer/14 network/2" } @@ -57,28 +58,74 @@ async def get_shows_data(region_id: int, page=1, pagesize=20): shows_data = await resp.json() return shows_data +async def get_show_details(show_id: int): + param = { + "id": show_id, + "project_id": show_id, + "requestSource": "neul-next" + } + async with ClientSession() as session: + async with session.get(SHOW_DETAILS_API_ROOT, headers=HEADERS, params=param) as resp: + show_details_data = await resp.json() + return show_details_data -# def process_shows_data_to_text(shows_data: dict): -# showlist = [] -# data = shows_data["data"] -# total_pages = data["numPages"] -# result = data["result"] -# for i in result: -# name = i["project_name"] -# venue_name = i["venue_name"] -# project_id = i["project_id"] -# sale_flag = i["sale_flag"] -# # start_time = i["start_time"] -# start_unix = i["start_unix"] -# start_time = convert_timestamp(start_unix) -# end_time = i["end_time"] -# price_low = i["price_low"] / 100 -# price_high = i["price_high"] / 100 -# district_name = i["district_name"] -# text = f"名称:{name}\n举办地:{venue_name}\nid:{project_id}\nflag:{sale_flag}\n开始时间:{start_time}\n结束时间:{end_time}\n最低票价:{price_low}\n最高票价:{price_high}\n区名:{district_name}\n\n" -# showlist.append(text) -# return showlist +def process_show_details_data_to_template(show_details_data: dict): + data = show_details_data["data"] + + banner_url = "https:"+data["banner"] + # banner_url = extract_banner_url(data["performance_image"]) + + # 提取事件基本信息 + name = data["name"] + start_time = convert_timestamp(data["start_time"]) + end_time = convert_timestamp(data["end_time"]) + + # 提取场馆信息 + venue_name = data["venue_info"]["name"] + venue_detail = data["venue_info"]["address_detail"] + + # 提取主办方信息 + organizer = data["merchant"]["company"] + + # 提取实名制,退票等信息 + is_refund = data["is_refund"] + id_bind = data["id_bind"] + has_eticket = data["has_eticket"] + # 提取票务信息 + ticket_info = [] + for screen in data.get("screen_list", []): + for ticket in screen.get("ticket_list", []): + ticket_info.append({ + "description": ticket.get("desc", ""), + "price": ticket.get("price", 0), + "sale_start": convert_timestamp(ticket.get("saleStart", 0)), + "sale_end": convert_timestamp(ticket.get("saleEnd", 0)), + "status": ticket.get("sale_flag", {}).get("display_name", "") + }) + guests_list = data["guests"] + if guests_list != None: + guests = "、".join(n["name"] for n in guests_list) + else: + guests = "" + + # 构建返回的字典 + item_dict = { + "banner_url": banner_url, + "name": name, + "start_time": start_time, + "end_time": end_time, + "venue_name": venue_name, + "venue_detail": venue_detail, + "organizer": organizer, + "ticket_info": ticket_info, + "guests": guests, + "is_refund": is_refund, + "id_bind": id_bind, + "has_eticket": has_eticket + } + + return item_dict def process_shows_data_to_template(shows_data: dict): showlist = [] diff --git a/nonebot_plugin_acgnshow/acgnshower.py b/nonebot_plugin_acgnshow/acgnshower.py index 87dd264..4600cef 100644 --- a/nonebot_plugin_acgnshow/acgnshower.py +++ b/nonebot_plugin_acgnshow/acgnshower.py @@ -7,7 +7,7 @@ from nonebot_plugin_htmlrender import template_to_pic from nonebot_plugin_alconna import on_alconna from nonebot_plugin_alconna.uniseg import UniMessage from arclet.alconna import Alconna, Args -from .config import RES_PATH, TEMPLATE_NAME, config +from .config import RES_PATH, LIST_TEMPLATE_NAME, DETAILS_TEMPLATE_NAME, config from .util import * from .__init__ import __plugin_meta__ @@ -25,10 +25,43 @@ showcmd.shortcut( "args": ["{region}", "{page}", "{date}"], }, ) +showcmd_details = on_alconna( + Alconna( + "展览详情", + Args["id?", int], # 这里定义了一个必需的 int 参数 "id" + ) +) +showcmd_details.shortcut( + r"展览详情\s*(?P\d+)", # 正则表达式匹配 "展览详情" 后跟一个整数 ID + { + "prefix": True, + "command": "展览详情", + "args": ["{id}"], # 将 ID 参数传递给命令 + }, +) +@showcmd_details.handle() +async def get_show_details_cmd( + id: Optional[int] = None +): + show_details = await get_show_details(id) + if show_details["errno"] != 0: await UniMessage("发生错误").send() ; return + try: + show_details_data = process_show_details_data_to_template(show_details) + print(show_details_data) + template = { + "show": show_details_data, + "bgimage": choose_random_bgimage(), + } + pic = await template_to_pic(str(RES_PATH), DETAILS_TEMPLATE_NAME, template) + except Exception as e: + await UniMessage(f"图片生成时产生错误:\n{str(e)}").send() + traceback.print_exc() + return + await UniMessage.image(raw=pic).send() @showcmd.handle() -async def find_show( +async def find_shows_cmd( region: Optional[str] = None, page: Optional[int] = None, date: Optional[str] = None, @@ -36,6 +69,8 @@ async def find_show( if not region: await UniMessage(__plugin_meta__.usage).send() return + if len(region) > 5: + return if not page: page = 1 if not date: @@ -55,9 +90,9 @@ async def find_show( "bgimage": choose_random_bgimage(), "global_data": shows_data[1], } - pic = await template_to_pic(str(RES_PATH), TEMPLATE_NAME, template) + pic = await template_to_pic(str(RES_PATH), LIST_TEMPLATE_NAME, template) except Exception as e: - await UniMessage("图片生成时产生未知错误").send() + await UniMessage(f"图片生成时产生错误:\n{str(e)}").send() traceback.print_exc() return diff --git a/nonebot_plugin_acgnshow/config.py b/nonebot_plugin_acgnshow/config.py index 88327af..7da792f 100644 --- a/nonebot_plugin_acgnshow/config.py +++ b/nonebot_plugin_acgnshow/config.py @@ -3,7 +3,8 @@ from pydantic import BaseModel from nonebot import get_plugin_config RES_PATH = Path(__file__).parent / "res" -TEMPLATE_NAME = "template.html" +LIST_TEMPLATE_NAME = "template.html" +DETAILS_TEMPLATE_NAME = "details.html" BGIMAGE_PATH = RES_PATH / "bgimage" diff --git a/nonebot_plugin_acgnshow/res/css/style.css b/nonebot_plugin_acgnshow/res/css/style.css index 5478aee..0324ba5 100644 --- a/nonebot_plugin_acgnshow/res/css/style.css +++ b/nonebot_plugin_acgnshow/res/css/style.css @@ -7,31 +7,52 @@ body { font-family: "Source Han Sans", sans-serif; } -.container, .header, .content, .footer, .details, .image { - border: 1px solid black; - box-sizing: border-box; +.background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-size: cover; + filter: blur(40%); + z-index: -1; } .container { width: 98%; margin: 1%; padding: 10px; - background-color: rgba(255, 255, 255, 0.8); + border: 1px solid #fff; + box-sizing: border-box; + background-color: rgba(255, 255, 255, 0.8); /* 半透明白色背景 */ } -.header, .designer { +.header { + border: 1px solid black; + padding: 5px; + margin-bottom: 10px; text-align: center; font-size: 10px; font-weight: bold; - padding: 5px; - margin: 10px 0; } .content { display: flex; + border: 1px solid black; margin-bottom: 10px; padding: 5px; position: relative; + box-sizing: border-box; +} + +.detail_content { + display: flex; + border: 1px solid black; + height: 201px; + margin-bottom: 10px; + padding: 5px; + position: relative; + box-sizing: border-box; } .image { @@ -39,6 +60,37 @@ body { height: 201px; border: 1px solid rgba(128, 64, 128, 0.5); text-align: center; + box-sizing: border-box; +} + +.vector_icon { + width: 20px; + height: 20px; + vertical-align: bottom; + margin-left: 5px; +} + +.banner { + margin-bottom: 15px; + border: 2px solid #000; /* 为 banner 添加边框 */ + display: flex; + justify-content: center; /* 居中对齐图片 */ + align-items: center; /* 垂直居中对齐图片 */ +} + +.banner img { + width: 450px; + height: 253px; +} + +.details .ticket_box { + display: inline-block; /* 根据内容的宽度自动调整 */ + background-color: rgba(255, 255, 255, 0.1); /* 半透明白色背景 */ + border-radius: 15px; /* 圆角半径 */ + padding: 20px; /* 内边距 */ + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* 添加阴影以增强视觉效果 */ + text-align: center; /* 内容水平居中 */ + width: auto; /* 根据内容宽度自动调整 */ } .details { @@ -46,38 +98,31 @@ body { padding-left: 10px; display: flex; flex-direction: column; + position: relative; /* 使子元素绝对定位相对于这个父元素 */ font-size: 10px; - position: relative; } .details .title { - font-weight: bold; + font-weight: bold; /* 加粗字体 */ margin-bottom: 5px; } .details .venue_name, .details .guests, -.details .placeholder, -.details .price, -.details .wish, -.details .start-time, -.details .end-time { +.details .placeholder { margin-bottom: 5px; } -.details .sale_flag, -.details .id { - position: absolute; - font-size: 10px; -} - .details .sale_flag { color: red; + position: absolute; top: 5px; right: 5px; } .details .id { + color: black; + position: absolute; top: 20px; right: 5px; } @@ -85,35 +130,56 @@ body { .details .price { color: #fb7299; font-weight: bold; + position: absolute; bottom: 5px; left: 5px; } +.details .status { + position: absolute; + display: flex; + bottom: 5px; +} + .details .wish { color: gray; + position: absolute; bottom: 5px; - left: 60px; + left: 60px; /* 适当调整以避免与price重叠 */ } .details .start-time { - bottom: 20px; + position: absolute; + bottom: 20px; /* 与end-time有一定的高度差异 */ right: 5px; } .details .end-time { + position: absolute; bottom: 5px; right: 5px; } .footer { + border: 1px solid black; + flex: 1; + padding-left: 10px; display: flex; flex-direction: column; justify-content: space-between; - font-size: 10px; position: relative; + font-size: 10px; } -.footer .pages, +.footer .designer { + text-align: center; + font-weight: bold; + font-size: 10px; + padding: 5px; + margin: 10px 0; +} + +.footer .pages, .footer .total_results { position: absolute; font-size: 10px; diff --git a/nonebot_plugin_acgnshow/res/details.html b/nonebot_plugin_acgnshow/res/details.html new file mode 100644 index 0000000..601532d --- /dev/null +++ b/nonebot_plugin_acgnshow/res/details.html @@ -0,0 +1,65 @@ + + + + + + 展览信息 + + + + + +
+
+
展览信息
+ + + + + +
+
+
{{ show.name }}
+
地点:{{ show.venue_name }} {{show.venue_detail}}
+
开始时间:{{ show.start_time }}
+
结束时间:{{ show.end_time }}
+
主办方:{{ show.organizer }}
+
+ {% macro icon(name) %} + + {% endmacro %} + {% if show.is_refund == 0 %} + {{ icon('no') }} 不可退 + {% endif %} + {% if show.id_bind == 1 %} + {{ icon('yes') }} 实名制 + {% endif %} + {% if show.has_eticket == True %} + {{ icon('yes') }} 电子票/兑换票 + {% endif %} +
+
+
+
+
+ {% for ticket in show.ticket_info %} +
{{ ticket.description }} {{ ticket.sale_start }} -- {{ ticket.sale_end }} ¥{{ ticket.price / 100 }} ({{ ticket.status }})
+ {% endfor %} +
+ + + +
+ + + diff --git a/nonebot_plugin_acgnshow/res/svg/no.svg b/nonebot_plugin_acgnshow/res/svg/no.svg new file mode 100644 index 0000000..4dd0b23 --- /dev/null +++ b/nonebot_plugin_acgnshow/res/svg/no.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nonebot_plugin_acgnshow/res/svg/yes.svg b/nonebot_plugin_acgnshow/res/svg/yes.svg new file mode 100644 index 0000000..e5ef583 --- /dev/null +++ b/nonebot_plugin_acgnshow/res/svg/yes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nonebot_plugin_acgnshow/util.py b/nonebot_plugin_acgnshow/util.py index 6b779ca..6d3e681 100644 --- a/nonebot_plugin_acgnshow/util.py +++ b/nonebot_plugin_acgnshow/util.py @@ -1,6 +1,7 @@ import os import random import datetime +import json from pathlib import Path from .config import config @@ -20,3 +21,8 @@ def convert_timestamp(timestamp) -> str: 将时间戳转换为日期格式 """ return datetime.datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") + +def extract_banner_url(value) -> str: + a = json.loads(value) + url = "https:"+a["banner"]["url"] + return url \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 0a35721..5b1af7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "nonebot-plugin-acgnshow" -version = "0.1.5.1" +version = "0.2" description = "Nonebot2插件,从哔哩哔哩会员购获取简易展览数据" readme = "README.md" requires-python = "<4.0,>=3.9" @@ -10,7 +10,7 @@ dependencies = [ "nonebot-plugin-alconna>=0.48.0", "nonebot-plugin-htmlrender>=0.3.2", "jinja2>=3.1.4", - "aiohttp>=3.9", + "aiohttp>=3.9" ] license = { text = "MIT" }