v0.2,不会接受字数大于5的展览查询,添加展览详细信息展示,返回详细错误信息

This commit is contained in:
Asankilp 2024-08-24 22:20:42 +08:00
parent 80a9b4d565
commit d5eeea6c80
11 changed files with 279 additions and 51 deletions

View File

@ -80,3 +80,4 @@ _✨ 从哔哩哔哩会员购获取展览简易信息 ✨_
| 配置项 | 必填 | 默认值 | 说明 | | 配置项 | 必填 | 默认值 | 说明 |
| :---------------: | :--: | :----: | :----------------------------------------------------------: | | :---------------: | :--: | :----: | :----------------------------------------------------------: |
| ACGNSHOW_PAGESIZE | 否 | 8 | 单个图片的条目数,最大为 20条目数过大可能导致 Bot 无法发送 | | ACGNSHOW_PAGESIZE | 否 | 8 | 单个图片的条目数,最大为 20条目数过大可能导致 Bot 无法发送 |
| ACGNSHOW_BGIMAGE_PATH | 否 | 插件内置背景图 | 插件返回图片的背景图目录路径 |

View File

@ -10,14 +10,19 @@ usage = """命令格式:
展览 <地区> [页码] 展览 <地区> [页码]
<地区>展览 [页码] <地区>展览 [页码]
获取指定地区的展览列表
其中地区为省级行政区或地级行政区不包含后缀 其中地区为省级行政区或地级行政区不包含后缀
如北京福建平顶山绍兴香港...或海外/全国 如北京福建平顶山绍兴香港...或海外/全国
展览详情 <ID>
其中ID为展览列表处返回的ID
示例 示例
展览 福建 2 展览 福建 2
福建展览 2 福建展览 2
全国展览 全国展览
海外展览 海外展览
展览详情 86827
数据来源于哔哩哔哩会员购由于API返回结果与实际存在差异数据可能不准确""" 数据来源于哔哩哔哩会员购由于API返回结果与实际存在差异数据可能不准确"""
__author__ = "Asankilp" __author__ = "Asankilp"

View File

@ -7,6 +7,7 @@ from .util import *
CITY_API_ROOT = "https://show.bilibili.com/api/ticket/city/list?channel=3" CITY_API_ROOT = "https://show.bilibili.com/api/ticket/city/list?channel=3"
SHOWS_API_ROOT = "https://show.bilibili.com/api/ticket/project/listV2" SHOWS_API_ROOT = "https://show.bilibili.com/api/ticket/project/listV2"
SHOW_DETAILS_API_ROOT = "https://show.bilibili.com/api/ticket/project/getV2"
HEADERS = { 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" "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() shows_data = await resp.json()
return shows_data 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): def process_show_details_data_to_template(show_details_data: dict):
# showlist = [] data = show_details_data["data"]
# 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
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): def process_shows_data_to_template(shows_data: dict):
showlist = [] showlist = []

View File

@ -7,7 +7,7 @@ from nonebot_plugin_htmlrender import template_to_pic
from nonebot_plugin_alconna import on_alconna from nonebot_plugin_alconna import on_alconna
from nonebot_plugin_alconna.uniseg import UniMessage from nonebot_plugin_alconna.uniseg import UniMessage
from arclet.alconna import Alconna, Args 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 .util import *
from .__init__ import __plugin_meta__ from .__init__ import __plugin_meta__
@ -25,10 +25,43 @@ showcmd.shortcut(
"args": ["{region}", "{page}", "{date}"], "args": ["{region}", "{page}", "{date}"],
}, },
) )
showcmd_details = on_alconna(
Alconna(
"展览详情",
Args["id?", int], # 这里定义了一个必需的 int 参数 "id"
)
)
showcmd_details.shortcut(
r"展览详情\s*(?P<id>\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() @showcmd.handle()
async def find_show( async def find_shows_cmd(
region: Optional[str] = None, region: Optional[str] = None,
page: Optional[int] = None, page: Optional[int] = None,
date: Optional[str] = None, date: Optional[str] = None,
@ -36,6 +69,8 @@ async def find_show(
if not region: if not region:
await UniMessage(__plugin_meta__.usage).send() await UniMessage(__plugin_meta__.usage).send()
return return
if len(region) > 5:
return
if not page: if not page:
page = 1 page = 1
if not date: if not date:
@ -55,9 +90,9 @@ async def find_show(
"bgimage": choose_random_bgimage(), "bgimage": choose_random_bgimage(),
"global_data": shows_data[1], "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: except Exception as e:
await UniMessage("图片生成时产生未知错误").send() await UniMessage(f"图片生成时产生错误:\n{str(e)}").send()
traceback.print_exc() traceback.print_exc()
return return

View File

@ -3,7 +3,8 @@ from pydantic import BaseModel
from nonebot import get_plugin_config from nonebot import get_plugin_config
RES_PATH = Path(__file__).parent / "res" 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" BGIMAGE_PATH = RES_PATH / "bgimage"

View File

@ -7,31 +7,52 @@ body {
font-family: "Source Han Sans", sans-serif; font-family: "Source Han Sans", sans-serif;
} }
.container, .header, .content, .footer, .details, .image { .background {
border: 1px solid black; position: fixed;
box-sizing: border-box; top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
filter: blur(40%);
z-index: -1;
} }
.container { .container {
width: 98%; width: 98%;
margin: 1%; margin: 1%;
padding: 10px; 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; text-align: center;
font-size: 10px; font-size: 10px;
font-weight: bold; font-weight: bold;
padding: 5px;
margin: 10px 0;
} }
.content { .content {
display: flex; display: flex;
border: 1px solid black;
margin-bottom: 10px; margin-bottom: 10px;
padding: 5px; padding: 5px;
position: relative; 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 { .image {
@ -39,6 +60,37 @@ body {
height: 201px; height: 201px;
border: 1px solid rgba(128, 64, 128, 0.5); border: 1px solid rgba(128, 64, 128, 0.5);
text-align: center; 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 { .details {
@ -46,38 +98,31 @@ body {
padding-left: 10px; padding-left: 10px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; /* 使子元素绝对定位相对于这个父元素 */
font-size: 10px; font-size: 10px;
position: relative;
} }
.details .title { .details .title {
font-weight: bold; font-weight: bold; /* 加粗字体 */
margin-bottom: 5px; margin-bottom: 5px;
} }
.details .venue_name, .details .venue_name,
.details .guests, .details .guests,
.details .placeholder, .details .placeholder {
.details .price,
.details .wish,
.details .start-time,
.details .end-time {
margin-bottom: 5px; margin-bottom: 5px;
} }
.details .sale_flag,
.details .id {
position: absolute;
font-size: 10px;
}
.details .sale_flag { .details .sale_flag {
color: red; color: red;
position: absolute;
top: 5px; top: 5px;
right: 5px; right: 5px;
} }
.details .id { .details .id {
color: black;
position: absolute;
top: 20px; top: 20px;
right: 5px; right: 5px;
} }
@ -85,32 +130,53 @@ body {
.details .price { .details .price {
color: #fb7299; color: #fb7299;
font-weight: bold; font-weight: bold;
position: absolute;
bottom: 5px; bottom: 5px;
left: 5px; left: 5px;
} }
.details .status {
position: absolute;
display: flex;
bottom: 5px;
}
.details .wish { .details .wish {
color: gray; color: gray;
position: absolute;
bottom: 5px; bottom: 5px;
left: 60px; left: 60px; /* 适当调整以避免与price重叠 */
} }
.details .start-time { .details .start-time {
bottom: 20px; position: absolute;
bottom: 20px; /* 与end-time有一定的高度差异 */
right: 5px; right: 5px;
} }
.details .end-time { .details .end-time {
position: absolute;
bottom: 5px; bottom: 5px;
right: 5px; right: 5px;
} }
.footer { .footer {
border: 1px solid black;
flex: 1;
padding-left: 10px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
font-size: 10px;
position: relative; position: relative;
font-size: 10px;
}
.footer .designer {
text-align: center;
font-weight: bold;
font-size: 10px;
padding: 5px;
margin: 10px 0;
} }
.footer .pages, .footer .pages,

View File

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>展览信息</title>
<link rel="stylesheet" href="./css/style.css" />
<style>
body {
background-image: url("{{ bgimage }}");
}
</style>
</head>
<body>
<div class="background"></div>
<div class="container">
<div class="header">展览信息</div>
<!-- Banner 图 -->
<div class="banner">
<img src="{{ show.banner_url }}" alt="Banner" style="width: 450px; height: 253px;" />
</div>
<!-- 单个展览内容 -->
<div class="detail_content">
<div class="details">
<div class="title">{{ show.name }}</div>
<div class="venue_name">地点:{{ show.venue_name }} {{show.venue_detail}}</div>
<div class="start-time">开始时间:{{ show.start_time }}</div>
<div class="end-time">结束时间:{{ show.end_time }}</div>
<div class="organizer">主办方:{{ show.organizer }}</div>
<div class="status">
{% macro icon(name) %}
<img src="./svg/{{ name }}.svg" class="vector_icon" width="8px" height="8px">
{% 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 %}
</div>
<div class="placeholder"></div>
</div>
</div>
<div class="details">
{% for ticket in show.ticket_info %}
<div class="ticket_box">{{ ticket.description }} {{ ticket.sale_start }} -- {{ ticket.sale_end }} ¥{{ ticket.price / 100 }} ({{ ticket.status }})</div>
{% endfor %}
</div>
<!-- 页脚 -->
<div class="footer">
<div class="designer">Designed by Asankilp?</div>
<div class="project_name">nonebot-plugin-acgnshow</div>
<div class="notice_text">本页信息仅供参考,具体内容请访问展览官方详情页,并自行检索实际信息</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1724505806097" class="icon" viewBox="0 0 1028 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10250" xmlns:xlink="http://www.w3.org/1999/xlink" width="200.78125" height="200"><path d="M875.086452 153.730058C676.053818-45.302575 353.260522-45.302575 154.128323 153.730058s-199.032634 521.825929 0 720.958129 521.825929 199.032634 720.958129 0 199.032634-521.825929 0-720.958129zM725.836868 725.438604c-9.757478 9.757478-25.488922 9.757478-35.246399 0L514.557604 549.405739 338.624306 725.438604c-9.757478 9.757478-25.488922 9.757478-35.2464 0s-9.757478-25.488922 0-35.2464l176.032865-176.032864-176.032865-175.933299c-9.757478-9.757478-9.757478-25.488922 0-35.246399 9.757478-9.757478 25.488922-9.757478 35.2464 0l176.032864 176.032865 176.032865-176.032865c9.757478-9.757478 25.488922-9.757478 35.246399 0 9.757478 9.757478 9.757478 25.488922 0 35.246399L549.804004 514.15934 725.836868 690.192204c9.657912 9.757478 9.657912 25.488922 0 35.2464z" fill="#F56C6C" p-id="10251"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1724506220274" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13079" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 76.8c-236.8 0-435.2 192-435.2 435.2s192 435.2 435.2 435.2 435.2-192 435.2-435.2S748.8 76.8 512 76.8z m249.6 320L480 704c-12.8 12.8-38.4 12.8-51.2 0L288 556.8c-12.8-12.8-12.8-38.4 0-51.2 12.8-12.8 38.4-12.8 51.2 0l115.2 115.2L704 339.2c12.8-12.8 38.4-12.8 51.2 0 25.6 12.8 25.6 38.4 6.4 57.6z" fill="#68D279" p-id="13080"></path><path d="M710.4 339.2l-256 281.6-115.2-115.2c-12.8-12.8-38.4-12.8-51.2 0-12.8 12.8-12.8 38.4 0 51.2l147.2 140.8c12.8 12.8 38.4 12.8 51.2 0L768 390.4c12.8-12.8 12.8-38.4 0-51.2-19.2-12.8-44.8-12.8-57.6 0z" fill="#FFFFFF" p-id="13081"></path></svg>

After

Width:  |  Height:  |  Size: 913 B

View File

@ -1,6 +1,7 @@
import os import os
import random import random
import datetime import datetime
import json
from pathlib import Path from pathlib import Path
from .config import config 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") 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

View File

@ -1,6 +1,6 @@
[project] [project]
name = "nonebot-plugin-acgnshow" name = "nonebot-plugin-acgnshow"
version = "0.1.5.1" version = "0.2"
description = "Nonebot2插件从哔哩哔哩会员购获取简易展览数据" description = "Nonebot2插件从哔哩哔哩会员购获取简易展览数据"
readme = "README.md" readme = "README.md"
requires-python = "<4.0,>=3.9" requires-python = "<4.0,>=3.9"
@ -10,7 +10,7 @@ dependencies = [
"nonebot-plugin-alconna>=0.48.0", "nonebot-plugin-alconna>=0.48.0",
"nonebot-plugin-htmlrender>=0.3.2", "nonebot-plugin-htmlrender>=0.3.2",
"jinja2>=3.1.4", "jinja2>=3.1.4",
"aiohttp>=3.9", "aiohttp>=3.9"
] ]
license = { text = "MIT" } license = { text = "MIT" }