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 + + + + + +
+
+
+
+ ZhenXun +
+
+

日报

+

NEWS

+
+
+

{{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)))