feat(daily_report): 新增日报模块及相关资源文件
- 新增日报模块,包括HTML、CSS、字体、图标等资源文件 - 添加以图搜图功能模块 - 修改图片插件以支持日报图片生成 - 更新requirements.txt依赖 - 新增日期处理、配置、数据源等辅助模块
|
|
@ -17,6 +17,11 @@ pycryptodome
|
|||
PyExecJS
|
||||
gradio_client
|
||||
tortoise-orm
|
||||
urllib3==1.26.5
|
||||
zhdate
|
||||
chinese_calendar
|
||||
lunardate
|
||||
playwright
|
||||
|
||||
requests
|
||||
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/'
|
||||
|
||||
# 日报
|
||||
daily_news_path = path+'/image/report/'
|
||||
|
||||
# 字体路径
|
||||
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.download_image import download_image
|
||||
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.handle()
|
||||
async def handle_function():
|
||||
|
||||
await Report.get_report_image()
|
||||
local_image_path = get_image_names()
|
||||
await image.finish(MessageSegment.file_image(Path(local_image_path)))
|
||||
|
||||
|
|
|
|||