feat(daily_report): 新增日报模块及相关资源文件
- 新增日报模块,包括HTML、CSS、字体、图标等资源文件 - 添加以图搜图功能模块 - 修改图片插件以支持日报图片生成 - 更新requirements.txt依赖 - 新增日期处理、配置、数据源等辅助模块
|
|
@ -17,6 +17,11 @@ pycryptodome
|
||||||
PyExecJS
|
PyExecJS
|
||||||
gradio_client
|
gradio_client
|
||||||
tortoise-orm
|
tortoise-orm
|
||||||
|
urllib3==1.26.5
|
||||||
|
zhdate
|
||||||
|
chinese_calendar
|
||||||
|
lunardate
|
||||||
|
playwright
|
||||||
|
|
||||||
requests
|
requests
|
||||||
pillow
|
pillow
|
||||||
|
|
|
||||||
BIN
src/clover_image/s.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
79
src/clover_image/search_image_by_image.py
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
def saucenao_search(image_path=None, image_url=None, api_key='your_api_key', numres=5):
|
||||||
|
"""
|
||||||
|
使用 SauceNAO API 进行以图搜图
|
||||||
|
|
||||||
|
:param image_path: 本地图片的文件路径,若提供图片 URL 则此参数可省略
|
||||||
|
:param image_url: 图片的网络 URL,若提供本地图片路径则此参数可省略
|
||||||
|
:param api_key: SauceNAO API Key
|
||||||
|
:param numres: 要返回的搜索结果数量
|
||||||
|
:return: 搜索结果的 JSON 数据
|
||||||
|
"""
|
||||||
|
base_url = 'https://saucenao.com/search.php'
|
||||||
|
params = {
|
||||||
|
'output_type': 2, # 输出类型为 JSON
|
||||||
|
'numres': numres, # 返回的结果数量
|
||||||
|
'api_key': api_key
|
||||||
|
}
|
||||||
|
|
||||||
|
if image_url:
|
||||||
|
params['url'] = image_url
|
||||||
|
elif image_path:
|
||||||
|
files = {'file': open(image_path, 'rb')}
|
||||||
|
response = requests.post(base_url, params=params, files=files)
|
||||||
|
else:
|
||||||
|
raise ValueError("url错误")
|
||||||
|
|
||||||
|
if image_url:
|
||||||
|
response = requests.get(base_url, params=params)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
print(f"请求失败,状态码: {response.status_code}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_saucenao_result(result):
|
||||||
|
"""
|
||||||
|
解析 SauceNAO 的搜索结果
|
||||||
|
"""
|
||||||
|
if not result or 'results' not in result:
|
||||||
|
print("未找到有效的搜索结果。")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(result)
|
||||||
|
for index, item in enumerate(result['results'], start=1):
|
||||||
|
header = item['header']
|
||||||
|
data = item['data']
|
||||||
|
|
||||||
|
similarity = header.get('similarity', '未知')
|
||||||
|
thumbnail = header.get('thumbnail', '未知')
|
||||||
|
title = data.get('title', '未知')
|
||||||
|
author = data.get('member_name', data.get('author_name', '未知'))
|
||||||
|
ext_urls = data.get('ext_urls', [])
|
||||||
|
pixiv_id = data.get('pixiv_id', '未知')
|
||||||
|
|
||||||
|
print(f"结果 {index}:")
|
||||||
|
print(f" 相似度: {similarity}%")
|
||||||
|
print(f"预览图{thumbnail} ")
|
||||||
|
print(f" 标题: {title}")
|
||||||
|
print(f" 作者: {author}")
|
||||||
|
if ext_urls:
|
||||||
|
print(f" 来源链接: {ext_urls[0]}")
|
||||||
|
else:
|
||||||
|
print(" 来源链接: 未知")
|
||||||
|
print(f"pixiv_id:{pixiv_id}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 若使用本地图片,提供图片路径
|
||||||
|
image_path = 's.png'
|
||||||
|
# 若使用网络图片,提供图片 URL
|
||||||
|
# image_url = 'https://example.com/your_image.jpg'
|
||||||
|
api_key = 'b2f961a88d854dc457a235c55d8486a764e8ff7d'
|
||||||
|
result = saucenao_search(image_path=image_path, api_key=api_key)
|
||||||
|
if result:
|
||||||
|
parse_saucenao_result(result)
|
||||||
75
src/clover_report/config.py
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Hitokoto(BaseModel):
|
||||||
|
id: int
|
||||||
|
"""id"""
|
||||||
|
uuid: str
|
||||||
|
"""uuid"""
|
||||||
|
hitokoto: str
|
||||||
|
"""一言"""
|
||||||
|
type: str
|
||||||
|
"""类型"""
|
||||||
|
from_who: str | None
|
||||||
|
"""作者"""
|
||||||
|
creator: str
|
||||||
|
"""创建者"""
|
||||||
|
creator_uid: int
|
||||||
|
"""创建者id"""
|
||||||
|
reviewer: int
|
||||||
|
"""审核者"""
|
||||||
|
commit_from: str
|
||||||
|
"""提交来源"""
|
||||||
|
created_at: str
|
||||||
|
"""创建日期"""
|
||||||
|
length: int
|
||||||
|
"""长度"""
|
||||||
|
|
||||||
|
|
||||||
|
class SixDataTo(BaseModel):
|
||||||
|
news: list[str]
|
||||||
|
"""新闻"""
|
||||||
|
tip: str
|
||||||
|
"""tip"""
|
||||||
|
updated: int
|
||||||
|
"""更新日期"""
|
||||||
|
url: str
|
||||||
|
"""链接"""
|
||||||
|
cover: str
|
||||||
|
"""图片"""
|
||||||
|
|
||||||
|
|
||||||
|
class SixData(BaseModel):
|
||||||
|
status: int
|
||||||
|
"""状态码"""
|
||||||
|
message: str
|
||||||
|
"""返回内容"""
|
||||||
|
data: SixDataTo
|
||||||
|
"""数据"""
|
||||||
|
|
||||||
|
|
||||||
|
class WeekDay(BaseModel):
|
||||||
|
en: str
|
||||||
|
"""英文"""
|
||||||
|
cn: str
|
||||||
|
"""中文"""
|
||||||
|
ja: str
|
||||||
|
"""日本称呼"""
|
||||||
|
id: int
|
||||||
|
"""ID"""
|
||||||
|
|
||||||
|
|
||||||
|
class AnimeItem(BaseModel):
|
||||||
|
name: str
|
||||||
|
name_cn: str
|
||||||
|
images: dict | None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def image(self) -> str:
|
||||||
|
return self.images["large"] if self.images else ""
|
||||||
|
|
||||||
|
|
||||||
|
class Anime(BaseModel):
|
||||||
|
weekday: WeekDay
|
||||||
|
items: list[AnimeItem]
|
||||||
307
src/clover_report/daily_report/main.css
Normal file
|
|
@ -0,0 +1,307 @@
|
||||||
|
body {
|
||||||
|
position: absolute;
|
||||||
|
left: -8px;
|
||||||
|
top: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Noto Sans SC B";
|
||||||
|
src: url("./res/font/NotoSansSC-Bold.otf") format("opentype");
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Noto Sans SC";
|
||||||
|
src: url("./res/font/NotoSansSC-Regular.otf") format("opentype");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "SSFangTangTi";
|
||||||
|
src: url("./res/font/SSFangTangTi.ttf");
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
/* height: 1885px; */
|
||||||
|
width: 578px;
|
||||||
|
background-color: #45667d;
|
||||||
|
position: relative;
|
||||||
|
padding: 15px;
|
||||||
|
/* box-sizing: border-box; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
/* height: 1860px; */
|
||||||
|
width: 555px;
|
||||||
|
background-color: #edf8ff;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-border {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zx-img {
|
||||||
|
height: 155px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-title {
|
||||||
|
position: relative;
|
||||||
|
width: 322px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zx-title {
|
||||||
|
font-family: "SSFangTangTi";
|
||||||
|
color: #45667d;
|
||||||
|
font-size: 72px;
|
||||||
|
position: absolute;
|
||||||
|
top: -73px;
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zx-tip {
|
||||||
|
color: #45667d;
|
||||||
|
font-family: "Noto Sans SC B";
|
||||||
|
font-size: 33px;
|
||||||
|
position: absolute;
|
||||||
|
top: 73px;
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-date {
|
||||||
|
background-color: #c1e0ef;
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 108px;
|
||||||
|
width: 120px;
|
||||||
|
margin-top: 30px;
|
||||||
|
/* margin-left: 67px; */
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 4px 4px 8px rgb(181, 223, 240);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-date-week {
|
||||||
|
color: #45667d;
|
||||||
|
font-size: 35px;
|
||||||
|
font-family: "Noto Sans SC B";
|
||||||
|
position: absolute;
|
||||||
|
left: 40px;
|
||||||
|
top: -34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-date-date {
|
||||||
|
color: #45667d;
|
||||||
|
font-family: "Noto Sans SC B";
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: #a0d7ec;
|
||||||
|
margin-top: 50px;
|
||||||
|
height: 25px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-date-cn {
|
||||||
|
font-family: "Noto Sans SC B";
|
||||||
|
color: #45667d;
|
||||||
|
font-size: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.one-border {
|
||||||
|
display: flex;
|
||||||
|
height: 320px;
|
||||||
|
margin-top: 50px;
|
||||||
|
font-family: "Noto Sans SC";
|
||||||
|
}
|
||||||
|
|
||||||
|
.moyu-border {
|
||||||
|
font-family: "Noto Sans SC";
|
||||||
|
height: 240px;
|
||||||
|
padding: 15px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: #45667d 3px solid;
|
||||||
|
width: 157px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moyu-title {
|
||||||
|
border: #45667d 2px solid;
|
||||||
|
position: absolute;
|
||||||
|
background-color: #ebf6fd;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #45667d;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: "Noto Sans SC B";
|
||||||
|
top: -19px;
|
||||||
|
left: 37px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-img {
|
||||||
|
height: 15px;
|
||||||
|
width: 15px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moyu-inner {
|
||||||
|
display: flex;
|
||||||
|
line-height: 15px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moyu-day {
|
||||||
|
width: 37px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moyu-day-name {
|
||||||
|
color: #45667d;
|
||||||
|
background-color: #daf8ff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bili-border {
|
||||||
|
font-family: "Noto Sans SC";
|
||||||
|
border: #45667d 3px solid;
|
||||||
|
height: 240px;
|
||||||
|
margin-left: 15px;
|
||||||
|
padding: 15px 10px 15px 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 300px;
|
||||||
|
position: relative;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bili-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 5px;
|
||||||
|
margin-top: 7px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hot-text {
|
||||||
|
background-color: #b5deef;
|
||||||
|
height: 17px;
|
||||||
|
width: 17px;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-border {
|
||||||
|
border: #45667d 3px solid;
|
||||||
|
width: 512px;
|
||||||
|
height: 370px;
|
||||||
|
border-radius: 10px;
|
||||||
|
position: relative;
|
||||||
|
padding: 17px;
|
||||||
|
font-family: "Noto Sans SC";
|
||||||
|
background-color: #edf8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-border-border {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.anime-border {
|
||||||
|
height: 170px;
|
||||||
|
width: 105px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 3px;
|
||||||
|
float: left;
|
||||||
|
margin: 8px 10px;
|
||||||
|
line-height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anime-text {
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 3px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anime-img {
|
||||||
|
height: 130px;
|
||||||
|
width: 105px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.three-border {
|
||||||
|
border: #45667d 3px solid;
|
||||||
|
width: 552px;
|
||||||
|
border-radius: 10px;
|
||||||
|
position: relative;
|
||||||
|
/* float: left; */
|
||||||
|
padding: 17px;
|
||||||
|
font-family: "Noto Sans SC";
|
||||||
|
margin-top: 46px;
|
||||||
|
background-color: #edf8ff;
|
||||||
|
font-size: 15px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.four-border {
|
||||||
|
border: #45667d 3px solid;
|
||||||
|
width: 512px;
|
||||||
|
border-radius: 10px;
|
||||||
|
position: relative;
|
||||||
|
/* float: left; */
|
||||||
|
padding: 17px;
|
||||||
|
font-family: "Noto Sans SC";
|
||||||
|
margin-top: 46px;
|
||||||
|
background-color: #edf8ff;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.five-border {
|
||||||
|
border: #45667d 3px solid;
|
||||||
|
width: 520px;
|
||||||
|
border-radius: 10px;
|
||||||
|
position: relative;
|
||||||
|
/* float: left; */
|
||||||
|
padding: 13px;
|
||||||
|
font-family: "Noto Sans SC";
|
||||||
|
margin-top: 46px;
|
||||||
|
font-size: 19px;
|
||||||
|
color: #45667d;
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: #edf8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.normal-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
/* width: 450px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-position: inside; /* 将项目符号放在内容内部 */
|
||||||
|
/* width: 450px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
li::marker {
|
||||||
|
display: inline-block; /* 确保项目符号可见 */
|
||||||
|
}
|
||||||
89
src/clover_report/daily_report/main.html
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Test</title>
|
||||||
|
<link rel="stylesheet" href="main.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="main">
|
||||||
|
<div class="top-border">
|
||||||
|
<div class="zx-img">
|
||||||
|
<img src="./res/image/no_bg.png" alt="ZhenXun" class="zx-img">
|
||||||
|
</div>
|
||||||
|
<div class="top-title">
|
||||||
|
<p class="zx-title">日报</p>
|
||||||
|
<p class="zx-tip">NEWS</p>
|
||||||
|
</div>
|
||||||
|
<div class="top-date">
|
||||||
|
<p class="top-date-week">{{data.week}}</p>
|
||||||
|
<p class="top-date-date">{{data.date}}</p>
|
||||||
|
<p class="top-date-cn">{{data.zh_date}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="one-border">
|
||||||
|
<div class="moyu-border">
|
||||||
|
<div class="moyu-title"><img src="./res/icon/fish.png" class="title-img">摸鱼日历</div>
|
||||||
|
{% for fes in data.data_festival %}
|
||||||
|
<p class="moyu-inner">距离<span class="moyu-day-name">{{fes[1]}}</span>还剩<span
|
||||||
|
class="moyu-day">{{fes[0]}}</span>天
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="bili-border">
|
||||||
|
<div class="moyu-title" style="left: 128px;"><img src="./res/icon/bilibili.png"
|
||||||
|
class="title-img">B站热点</div>
|
||||||
|
{% for s in data.data_bili %}
|
||||||
|
<p class="bili-text"><span class="hot-text">热</span>{{s}}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="two-border">
|
||||||
|
<div class="moyu-title" style="left: -3px;"><img src="./res/icon/bgm.png" class="title-img">今日新番</div>
|
||||||
|
<div class="two-border-border">
|
||||||
|
{% for s in data.data_anime %}
|
||||||
|
<div class="anime-border">
|
||||||
|
<img src="{{s[1]}}" class="anime-img">
|
||||||
|
<p class="anime-text">{{s[0]}}</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="three-border">
|
||||||
|
<div class="moyu-title" style="left: -3px;"><img src="./res/icon/60.png" class="title-img">60S读世界</div>
|
||||||
|
<ul style="line-height: 30px; line-height: 25px; margin-left: -25px;">
|
||||||
|
{% for s in data.data_six %}
|
||||||
|
{% if data.full_show %}
|
||||||
|
<li class="full-show-text">{{s}}</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="normal-text">{{s}}</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="four-border">
|
||||||
|
<div class="moyu-title" style="left: -3px;"><img src="./res/icon/it.png" class="title-img">IT资讯</div>
|
||||||
|
<ul style="line-height: 30px; line-height: 25px; margin-left: -25px;">
|
||||||
|
{% for s in data.data_it %}
|
||||||
|
{% if data.full_show %}
|
||||||
|
<li class="full-show-text">{{s}}</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="normal-text">{{s}}</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="five-border">
|
||||||
|
<div class="moyu-title" style="left: -3px;"><img src="./res/icon/hitokoto.png" class="title-img">今日一言
|
||||||
|
</div>
|
||||||
|
<p style="margin-top: 5px;margin-bottom: 5px;">“{{data.data_hitokoto}}”</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
BIN
src/clover_report/daily_report/res/font/NotoSansSC-Bold.otf
Normal file
BIN
src/clover_report/daily_report/res/font/NotoSansSC-Regular.otf
Normal file
BIN
src/clover_report/daily_report/res/font/SSFangTangTi.ttf
Normal file
BIN
src/clover_report/daily_report/res/icon/60.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src/clover_report/daily_report/res/icon/bgm.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src/clover_report/daily_report/res/icon/bilibili.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/clover_report/daily_report/res/icon/fish.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
src/clover_report/daily_report/res/icon/game.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
src/clover_report/daily_report/res/icon/hitokoto.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
src/clover_report/daily_report/res/icon/it.png
Normal file
|
After Width: | Height: | Size: 851 B |
BIN
src/clover_report/daily_report/res/image/no_bg.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
205
src/clover_report/data_source.py
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from os import getcwd
|
||||||
|
from pathlib import Path
|
||||||
|
import httpx
|
||||||
|
from httpx import ConnectError, HTTPStatusError, Response, TimeoutException
|
||||||
|
from nonebot.log import logger
|
||||||
|
from nonebot_plugin_htmlrender import template_to_pic
|
||||||
|
import tenacity
|
||||||
|
from tenacity import retry, stop_after_attempt, wait_fixed
|
||||||
|
from zhdate import ZhDate
|
||||||
|
from .config import Anime, Hitokoto, SixData
|
||||||
|
from .date import get_festivals_dates
|
||||||
|
from src.configs.path_config import daily_news_path
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncHttpx:
|
||||||
|
@classmethod
|
||||||
|
@retry(
|
||||||
|
stop=stop_after_attempt(3),
|
||||||
|
wait=wait_fixed(1),
|
||||||
|
retry=(
|
||||||
|
tenacity.retry_if_exception_type(
|
||||||
|
(TimeoutException, ConnectError, HTTPStatusError)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def get(cls, url: str) -> Response:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
response = await client.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response
|
||||||
|
except (TimeoutException, ConnectError, HTTPStatusError) as e:
|
||||||
|
logger.error(f"Request to {url} failed due to: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@retry(
|
||||||
|
stop=stop_after_attempt(3),
|
||||||
|
wait=wait_fixed(1),
|
||||||
|
retry=(
|
||||||
|
tenacity.retry_if_exception_type(
|
||||||
|
(TimeoutException, ConnectError, HTTPStatusError)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def post(
|
||||||
|
cls, url: str, data: dict[str, str], headers: dict[str, str]
|
||||||
|
) -> Response:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
response = await client.post(url, data=data, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response
|
||||||
|
except (TimeoutException, ConnectError, HTTPStatusError) as e:
|
||||||
|
logger.error(f"Request to {url} failed due to: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
async def save_img(data: bytes):
|
||||||
|
|
||||||
|
"""
|
||||||
|
保存日报图片
|
||||||
|
:param data:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
file_path = daily_news_path + f"{datetime.now().date()}.png"
|
||||||
|
with open(file_path, "wb") as file:
|
||||||
|
file.write(data)
|
||||||
|
|
||||||
|
|
||||||
|
class Report:
|
||||||
|
hitokoto_url = "https://v1.hitokoto.cn/?c=a"
|
||||||
|
alapi_url = "https://v2.alapi.cn/api/zaobao"
|
||||||
|
alapi_token = "48x3u7iqryztowlnwbnrwjucebzieu"
|
||||||
|
six_url = "https://60s.viki.moe/?v2=1"
|
||||||
|
game_url = "https://www.4gamers.com.tw/rss/latest-news"
|
||||||
|
bili_url = "https://s.search.bilibili.com/main/hotword"
|
||||||
|
it_url = "https://www.ithome.com/rss/"
|
||||||
|
anime_url = "https://api.bgm.tv/calendar"
|
||||||
|
|
||||||
|
|
||||||
|
week = { # noqa: RUF012
|
||||||
|
0: "一",
|
||||||
|
1: "二",
|
||||||
|
2: "三",
|
||||||
|
3: "四",
|
||||||
|
4: "五",
|
||||||
|
5: "六",
|
||||||
|
6: "日",
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_report_image(cls) -> bytes:
|
||||||
|
"""获取数据"""
|
||||||
|
now = datetime.now()
|
||||||
|
file = Path() / daily_news_path / f"{now.date()}.png"
|
||||||
|
if os.path.exists(file):
|
||||||
|
with file.open("rb") as image_file:
|
||||||
|
return image_file.read()
|
||||||
|
zhdata = ZhDate.from_datetime(now)
|
||||||
|
result = await asyncio.gather(
|
||||||
|
*[
|
||||||
|
cls.get_hitokoto(),
|
||||||
|
cls.get_bili(),
|
||||||
|
cls.get_six(),
|
||||||
|
cls.get_anime(),
|
||||||
|
cls.get_it(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data = {
|
||||||
|
"data_festival": get_festivals_dates(),
|
||||||
|
"data_hitokoto": result[0],
|
||||||
|
"data_bili": result[1],
|
||||||
|
"data_six": result[2],
|
||||||
|
"data_anime": result[3],
|
||||||
|
"data_it": result[4],
|
||||||
|
"week": cls.week[now.weekday()],
|
||||||
|
"date": now.date(),
|
||||||
|
"zh_date": zhdata.chinese().split()[0][5:],
|
||||||
|
"full_show": True,
|
||||||
|
}
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch()
|
||||||
|
|
||||||
|
image_bytes = await template_to_pic(
|
||||||
|
template_path=getcwd() + "/src/clover_report/daily_report",
|
||||||
|
template_name="main.html",
|
||||||
|
templates={"data": data},
|
||||||
|
pages={
|
||||||
|
"viewport": {"width": 578, "height": 1885},
|
||||||
|
"base_url": f"file://{getcwd()}",
|
||||||
|
},
|
||||||
|
wait=2,
|
||||||
|
)
|
||||||
|
await save_img(image_bytes)
|
||||||
|
await browser.close()
|
||||||
|
return image_bytes
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_hitokoto(cls) -> str:
|
||||||
|
"""获取一言"""
|
||||||
|
res = await AsyncHttpx.get(cls.hitokoto_url)
|
||||||
|
data = Hitokoto(**res.json())
|
||||||
|
return data.hitokoto
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_bili(cls) -> list[str]:
|
||||||
|
"""获取哔哩哔哩热搜"""
|
||||||
|
res = await AsyncHttpx.get(cls.bili_url)
|
||||||
|
data = res.json()
|
||||||
|
return [item["keyword"] for item in data["list"]]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_alapi_data(cls) -> list[str]:
|
||||||
|
"""获取alapi数据"""
|
||||||
|
payload = {"token": cls.alapi_token, "format": "json"}
|
||||||
|
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||||
|
res = await AsyncHttpx.post(cls.alapi_url, data=payload, headers=headers)
|
||||||
|
if res.status_code != 200:
|
||||||
|
return ["Error: Unable to fetch data"]
|
||||||
|
data = res.json()
|
||||||
|
news_items = data.get("data", {}).get("news", [])
|
||||||
|
return news_items[:11] if len(news_items) > 11 else news_items
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_six(cls) -> list[str]:
|
||||||
|
"""获取60s数据"""
|
||||||
|
if True:
|
||||||
|
return await cls.get_alapi_data()
|
||||||
|
res = await AsyncHttpx.get(cls.six_url)
|
||||||
|
data = SixData(**res.json())
|
||||||
|
return data.data.news[:11] if len(
|
||||||
|
data.data.news) > 11 else data.data.news
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_it(cls) -> list[str]:
|
||||||
|
"""获取it数据"""
|
||||||
|
res = await AsyncHttpx.get(cls.it_url)
|
||||||
|
root = ET.fromstring(res.text)
|
||||||
|
titles = []
|
||||||
|
for item in root.findall("./channel/item"):
|
||||||
|
title_element = item.find("title")
|
||||||
|
if title_element is not None:
|
||||||
|
titles.append(title_element.text)
|
||||||
|
return titles[:11] if len(titles) > 11 else titles
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_anime(cls) -> list[tuple[str, str]]:
|
||||||
|
"""获取动漫数据"""
|
||||||
|
res = await AsyncHttpx.get(cls.anime_url)
|
||||||
|
data_list = []
|
||||||
|
week = datetime.now().weekday()
|
||||||
|
try:
|
||||||
|
anime = Anime(**res.json()[week])
|
||||||
|
except IndexError:
|
||||||
|
anime = Anime(**res.json()[-1])
|
||||||
|
data_list.extend(
|
||||||
|
(data.name_cn or data.name, data.image) for data in anime.items
|
||||||
|
)
|
||||||
|
return data_list[:8] if len(data_list) > 8 else data_list
|
||||||
95
src/clover_report/date.py
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
import chinese_calendar as calendar
|
||||||
|
import lunardate
|
||||||
|
|
||||||
|
# 定义2025年农历节日的农历日期
|
||||||
|
lunar_festivals = {
|
||||||
|
"春节": (1, 1), # 春节 (农历正月初一)
|
||||||
|
"端午节": (5, 5), # 端午节 (农历五月初五)
|
||||||
|
"中秋节": (8, 15), # 中秋节 (农历八月十五)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 固定日期的节日
|
||||||
|
fixed_festivals_dates = {
|
||||||
|
"劳动节": date(2025, 5, 1), # 劳动节
|
||||||
|
"国庆节": date(2025, 10, 1), # 国庆节
|
||||||
|
"元旦": date(2025, 1, 1), # 元旦
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_next_year_festival_date(
|
||||||
|
festival_name: str, current_festival_date: date
|
||||||
|
) -> date:
|
||||||
|
"""获取下一个该节日的日期"""
|
||||||
|
if festival_name in lunar_festivals:
|
||||||
|
# 对于农历节日,使用lunardate库转换为下一年的公历日期
|
||||||
|
next_year = current_festival_date.year + 1
|
||||||
|
month, day = lunar_festivals[festival_name]
|
||||||
|
next_festival_date = lunardate.LunarDate(next_year, month, day).toSolarDate()
|
||||||
|
else:
|
||||||
|
# 对于固定日期的节日,直接增加一年
|
||||||
|
next_festival_date = current_festival_date.replace(
|
||||||
|
year=current_festival_date.year + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
return next_festival_date
|
||||||
|
|
||||||
|
|
||||||
|
def find_tomb_sweeping_day(year: int) -> date:
|
||||||
|
# 春分通常在3月20日或21日
|
||||||
|
start_date = date(year, 3, 20)
|
||||||
|
|
||||||
|
# 查找春分的确切日期
|
||||||
|
spring_equinox = next(
|
||||||
|
(
|
||||||
|
start_date + timedelta(days=i)
|
||||||
|
for i in range(3)
|
||||||
|
if calendar.get_holiday_detail(start_date + timedelta(days=i))[1] == "春分"
|
||||||
|
),
|
||||||
|
start_date,
|
||||||
|
)
|
||||||
|
return spring_equinox + timedelta(days=15)
|
||||||
|
|
||||||
|
|
||||||
|
def days_until_festival(festival_name: str, today: date, festival_date: date) -> int:
|
||||||
|
if festival_date < today:
|
||||||
|
# 如果节日已经过去,计算下一个该节日的到来时间
|
||||||
|
next_festival_date = get_next_year_festival_date(festival_name, festival_date)
|
||||||
|
delta = next_festival_date - today
|
||||||
|
return delta.days
|
||||||
|
else:
|
||||||
|
delta = festival_date - today
|
||||||
|
return delta.days
|
||||||
|
|
||||||
|
|
||||||
|
# 获取农历节日对应的公历日期
|
||||||
|
def get_lunar_festivals_dates(today: date):
|
||||||
|
year = today.year
|
||||||
|
return {
|
||||||
|
name: lunardate.LunarDate(year, month, day).toSolarDate()
|
||||||
|
for name, (month, day) in lunar_festivals.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_festivals_dates() -> list[tuple[int, str]]:
|
||||||
|
today = date.today()
|
||||||
|
lunar_festivals_dates = get_lunar_festivals_dates(today)
|
||||||
|
# 添加清明节到节日字典中
|
||||||
|
lunar_festivals_dates["清明节"] = find_tomb_sweeping_day(today.year)
|
||||||
|
|
||||||
|
# 合并两个字典
|
||||||
|
festivals_dates = {**lunar_festivals_dates, **fixed_festivals_dates}
|
||||||
|
|
||||||
|
sort_name = ["春节", "端午节", "中秋节", "清明节", "劳动节", "国庆节", "元旦"]
|
||||||
|
|
||||||
|
# 计算到每个节日的天数,并检查是否为法定假日
|
||||||
|
data_list = []
|
||||||
|
for name in sort_name:
|
||||||
|
if name in festivals_dates:
|
||||||
|
days_left = days_until_festival(name, today, festivals_dates[name])
|
||||||
|
data_list.append((days_left, name))
|
||||||
|
else:
|
||||||
|
data_list.append((-1, name))
|
||||||
|
data_list.sort(key=lambda x: x[0])
|
||||||
|
return data_list
|
||||||
|
|
@ -18,6 +18,9 @@ good_bad = path+'/image/good_bad_news/'
|
||||||
#谁说 生成图片路径
|
#谁说 生成图片路径
|
||||||
who_say_path = path+'/image/who_say/'
|
who_say_path = path+'/image/who_say/'
|
||||||
|
|
||||||
|
# 日报
|
||||||
|
daily_news_path = path+'/image/report/'
|
||||||
|
|
||||||
# 字体路径
|
# 字体路径
|
||||||
font_path = path + '/font/'
|
font_path = path + '/font/'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,12 @@ from nonebot.adapters.qq import MessageSegment,MessageEvent
|
||||||
from src.clover_image.get_image import get_image_names
|
from src.clover_image.get_image import get_image_names
|
||||||
from src.clover_image.download_image import download_image
|
from src.clover_image.download_image import download_image
|
||||||
from src.configs.path_config import temp_path
|
from src.configs.path_config import temp_path
|
||||||
|
from src.clover_report.data_source import Report
|
||||||
|
|
||||||
image = on_command("图", rule=to_me(), priority=10, block=True)
|
image = on_command("图", rule=to_me(), priority=10, block=True)
|
||||||
@image.handle()
|
@image.handle()
|
||||||
async def handle_function():
|
async def handle_function():
|
||||||
|
await Report.get_report_image()
|
||||||
local_image_path = get_image_names()
|
local_image_path = get_image_names()
|
||||||
await image.finish(MessageSegment.file_image(Path(local_image_path)))
|
await image.finish(MessageSegment.file_image(Path(local_image_path)))
|
||||||
|
|
||||||
|
|
|
||||||