diff --git a/requirements.txt b/requirements.txt
index 072ece5..9e27035 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -17,6 +17,11 @@ pycryptodome
PyExecJS
gradio_client
tortoise-orm
+urllib3==1.26.5
+zhdate
+chinese_calendar
+lunardate
+playwright
requests
pillow
diff --git a/src/clover_image/s.png b/src/clover_image/s.png
new file mode 100644
index 0000000..1bb308d
Binary files /dev/null and b/src/clover_image/s.png differ
diff --git a/src/clover_image/search_image_by_image.py b/src/clover_image/search_image_by_image.py
new file mode 100644
index 0000000..6564666
--- /dev/null
+++ b/src/clover_image/search_image_by_image.py
@@ -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)
\ No newline at end of file
diff --git a/src/clover_report/config.py b/src/clover_report/config.py
new file mode 100644
index 0000000..5e4b92f
--- /dev/null
+++ b/src/clover_report/config.py
@@ -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]
diff --git a/src/clover_report/daily_report/main.css b/src/clover_report/daily_report/main.css
new file mode 100644
index 0000000..13b0d51
--- /dev/null
+++ b/src/clover_report/daily_report/main.css
@@ -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; /* 确保项目符号可见 */
+}
diff --git a/src/clover_report/daily_report/main.html b/src/clover_report/daily_report/main.html
new file mode 100644
index 0000000..13b9a46
--- /dev/null
+++ b/src/clover_report/daily_report/main.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+ Test
+
+
+
+
+
+
+
+
+
+

+
+
+
+
{{data.week}}
+
{{data.date}}
+
{{data.zh_date}}
+
+
+
+
+

摸鱼日历
+ {% for fes in data.data_festival %}
+
距离{{fes[1]}}还剩{{fes[0]}}天
+
+ {% endfor %}
+
+
+

B站热点
+ {% for s in data.data_bili %}
+
热{{s}}
+ {% endfor %}
+
+
+
+

今日新番
+
+ {% for s in data.data_anime %}
+
+

+
{{s[0]}}
+
+ {% endfor %}
+
+
+
+

60S读世界
+
+ {% for s in data.data_six %}
+ {% if data.full_show %}
+ - {{s}}
+ {% else %}
+ - {{s}}
+ {% endif %}
+ {% endfor %}
+
+
+
+

IT资讯
+
+ {% for s in data.data_it %}
+ {% if data.full_show %}
+ - {{s}}
+ {% else %}
+ - {{s}}
+ {% endif %}
+ {% endfor %}
+
+
+
+

今日一言
+
+
“{{data.data_hitokoto}}”
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/clover_report/daily_report/res/font/NotoSansSC-Bold.otf b/src/clover_report/daily_report/res/font/NotoSansSC-Bold.otf
new file mode 100644
index 0000000..172eb67
Binary files /dev/null and b/src/clover_report/daily_report/res/font/NotoSansSC-Bold.otf differ
diff --git a/src/clover_report/daily_report/res/font/NotoSansSC-Regular.otf b/src/clover_report/daily_report/res/font/NotoSansSC-Regular.otf
new file mode 100644
index 0000000..d350ffa
Binary files /dev/null and b/src/clover_report/daily_report/res/font/NotoSansSC-Regular.otf differ
diff --git a/src/clover_report/daily_report/res/font/SSFangTangTi.ttf b/src/clover_report/daily_report/res/font/SSFangTangTi.ttf
new file mode 100644
index 0000000..a8b5c2f
Binary files /dev/null and b/src/clover_report/daily_report/res/font/SSFangTangTi.ttf differ
diff --git a/src/clover_report/daily_report/res/icon/60.png b/src/clover_report/daily_report/res/icon/60.png
new file mode 100644
index 0000000..6d9af90
Binary files /dev/null and b/src/clover_report/daily_report/res/icon/60.png differ
diff --git a/src/clover_report/daily_report/res/icon/bgm.png b/src/clover_report/daily_report/res/icon/bgm.png
new file mode 100644
index 0000000..758a6b6
Binary files /dev/null and b/src/clover_report/daily_report/res/icon/bgm.png differ
diff --git a/src/clover_report/daily_report/res/icon/bilibili.png b/src/clover_report/daily_report/res/icon/bilibili.png
new file mode 100644
index 0000000..9a1e050
Binary files /dev/null and b/src/clover_report/daily_report/res/icon/bilibili.png differ
diff --git a/src/clover_report/daily_report/res/icon/fish.png b/src/clover_report/daily_report/res/icon/fish.png
new file mode 100644
index 0000000..4ca50a3
Binary files /dev/null and b/src/clover_report/daily_report/res/icon/fish.png differ
diff --git a/src/clover_report/daily_report/res/icon/game.png b/src/clover_report/daily_report/res/icon/game.png
new file mode 100644
index 0000000..714f5cb
Binary files /dev/null and b/src/clover_report/daily_report/res/icon/game.png differ
diff --git a/src/clover_report/daily_report/res/icon/hitokoto.png b/src/clover_report/daily_report/res/icon/hitokoto.png
new file mode 100644
index 0000000..4398ffc
Binary files /dev/null and b/src/clover_report/daily_report/res/icon/hitokoto.png differ
diff --git a/src/clover_report/daily_report/res/icon/it.png b/src/clover_report/daily_report/res/icon/it.png
new file mode 100644
index 0000000..5055af6
Binary files /dev/null and b/src/clover_report/daily_report/res/icon/it.png differ
diff --git a/src/clover_report/daily_report/res/image/no_bg.png b/src/clover_report/daily_report/res/image/no_bg.png
new file mode 100644
index 0000000..da0920f
Binary files /dev/null and b/src/clover_report/daily_report/res/image/no_bg.png differ
diff --git a/src/clover_report/data_source.py b/src/clover_report/data_source.py
new file mode 100644
index 0000000..df5f854
--- /dev/null
+++ b/src/clover_report/data_source.py
@@ -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
diff --git a/src/clover_report/date.py b/src/clover_report/date.py
new file mode 100644
index 0000000..ded780f
--- /dev/null
+++ b/src/clover_report/date.py
@@ -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
diff --git a/src/configs/path_config.py b/src/configs/path_config.py
index 62ef4ab..6957751 100644
--- a/src/configs/path_config.py
+++ b/src/configs/path_config.py
@@ -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/'
diff --git a/src/plugins/image.py b/src/plugins/image.py
index e623a38..d3d99a6 100644
--- a/src/plugins/image.py
+++ b/src/plugins/image.py
@@ -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)))