mirror of
https://github.com/woodchen-ink/dnspod-yxip.git
synced 2025-07-18 05:42:08 +08:00
first commit
This commit is contained in:
commit
bb4d8473db
42
.env.example
Normal file
42
.env.example
Normal file
@ -0,0 +1,42 @@
|
||||
# DNSPOD API 配置
|
||||
DNSPOD_ID=your_id_here
|
||||
DNSPOD_TOKEN=your_token_here
|
||||
|
||||
# 域名配置 - 域名1
|
||||
DOMAIN_1=example1.com
|
||||
SUB_DOMAIN_1=@ # 子域名,@ 表示根域名
|
||||
REMARK_1=优选IP # 记录备注
|
||||
TTL_1=600 # TTL值(秒)
|
||||
UPDATE_INTERVAL_1=15 # 更新间隔(分钟)
|
||||
IPV4_ENABLED_1=true # 是否启用IPv4记录
|
||||
IPV6_ENABLED_1=true # 是否启用IPv6记录
|
||||
ENABLED_1=true # 是否启用此域名配置
|
||||
|
||||
# 域名配置 - 域名2(更频繁的更新)
|
||||
DOMAIN_2=example2.com
|
||||
SUB_DOMAIN_2=www
|
||||
REMARK_2=CF优选 # 记录备注
|
||||
TTL_2=300 # 更短的TTL
|
||||
UPDATE_INTERVAL_2=5 # 更频繁的更新
|
||||
IPV4_ENABLED_2=true # 只启用IPv4
|
||||
IPV6_ENABLED_2=false # 不启用IPv6
|
||||
ENABLED_2=true
|
||||
|
||||
# 域名配置 - 域名3(更长的缓存)
|
||||
DOMAIN_3=example3.com
|
||||
SUB_DOMAIN_3=*
|
||||
REMARK_3=CloudFlare优选 # 记录备注
|
||||
TTL_3=1800 # 更长的TTL
|
||||
UPDATE_INTERVAL_3=30 # 更长的更新间隔
|
||||
IPV4_ENABLED_3=true # 启用IPv4
|
||||
IPV6_ENABLED_3=true # 启用IPv6
|
||||
ENABLED_3=true
|
||||
|
||||
# 可以继续添加更多域名配置...
|
||||
# 每个域名可以独立控制:
|
||||
# - 是否启用IPv4 (IPV4_ENABLED_n)
|
||||
# - 是否启用IPv6 (IPV6_ENABLED_n)
|
||||
# - TTL值 (TTL_n)
|
||||
# - 更新间隔 (UPDATE_INTERVAL_n)
|
||||
# - 是否启用 (ENABLED_n)
|
||||
# - 记录备注 (REMARK_n)
|
40
.github/workflows/docker-publish.yml
vendored
Normal file
40
.github/workflows/docker-publish.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
IMAGE_NAME: dnspod-yxip
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: woodchen
|
||||
password: ${{ secrets.ACCESS_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: true
|
||||
tags: woodchen/${{ env.IMAGE_NAME }}:latest
|
||||
platforms: linux/amd64,linux/arm64
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/__pycache__
|
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@ -0,0 +1,17 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制项目文件
|
||||
COPY requirements.txt .
|
||||
COPY config.py .
|
||||
COPY main.py .
|
||||
|
||||
# 安装依赖
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
# 创建配置文件和日志目录
|
||||
RUN mkdir -p logs && touch .env
|
||||
|
||||
# 运行程序
|
||||
CMD ["python", "main.py"]
|
93
config.py
Normal file
93
config.py
Normal file
@ -0,0 +1,93 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
# DNSPOD API配置
|
||||
DNSPOD_ID = os.getenv("DNSPOD_ID")
|
||||
DNSPOD_TOKEN = os.getenv("DNSPOD_TOKEN")
|
||||
|
||||
# API接口配置
|
||||
API_URL = "https://api.vvhan.com/tool/cf_ip"
|
||||
|
||||
# 默认配置
|
||||
DEFAULT_TTL = 600
|
||||
DEFAULT_UPDATE_INTERVAL = 15
|
||||
|
||||
# 支持的线路类型
|
||||
LINE_TYPES = ["默认", "移动", "联通", "电信"]
|
||||
|
||||
# 支持的记录类型
|
||||
RECORD_TYPES = ["A", "AAAA"]
|
||||
|
||||
# 域名配置
|
||||
DOMAINS = []
|
||||
|
||||
# 从环境变量加载域名配置
|
||||
i = 1
|
||||
while True:
|
||||
domain_key = f"DOMAIN_{i}"
|
||||
if not os.getenv(domain_key):
|
||||
break
|
||||
|
||||
# 获取基本配置
|
||||
base_config = {
|
||||
"domain": os.getenv(domain_key),
|
||||
"sub_domain": os.getenv(f"SUB_DOMAIN_{i}", "@"),
|
||||
"line": LINE_TYPES,
|
||||
"ttl": int(os.getenv(f"TTL_{i}", str(DEFAULT_TTL))),
|
||||
"update_interval": int(
|
||||
os.getenv(f"UPDATE_INTERVAL_{i}", str(DEFAULT_UPDATE_INTERVAL))
|
||||
),
|
||||
"enabled": os.getenv(f"ENABLED_{i}", "true").lower() == "true",
|
||||
"remark": os.getenv(f"REMARK_{i}", "YXIP"),
|
||||
}
|
||||
|
||||
# 检查是否启用IPv4
|
||||
ipv4_enabled = os.getenv(f"IPV4_ENABLED_{i}", "true").lower() == "true"
|
||||
# 检查是否启用IPv6
|
||||
ipv6_enabled = os.getenv(f"IPV6_ENABLED_{i}", "true").lower() == "true"
|
||||
|
||||
# 为每个启用的记录类型创建配置
|
||||
if ipv4_enabled:
|
||||
ipv4_config = base_config.copy()
|
||||
ipv4_config["record_type"] = "A"
|
||||
DOMAINS.append(ipv4_config)
|
||||
|
||||
if ipv6_enabled:
|
||||
ipv6_config = base_config.copy()
|
||||
ipv6_config["record_type"] = "AAAA"
|
||||
DOMAINS.append(ipv6_config)
|
||||
|
||||
i += 1
|
||||
|
||||
# 如果没有配置任何域名,使用默认配置
|
||||
if not DOMAINS:
|
||||
base_config = {
|
||||
"domain": os.getenv("DOMAIN", "example.com"),
|
||||
"sub_domain": os.getenv("SUB_DOMAIN", "@"),
|
||||
"line": LINE_TYPES,
|
||||
"ttl": int(os.getenv("TTL", str(DEFAULT_TTL))),
|
||||
"update_interval": int(
|
||||
os.getenv("UPDATE_INTERVAL", str(DEFAULT_UPDATE_INTERVAL))
|
||||
),
|
||||
"enabled": True,
|
||||
}
|
||||
|
||||
# 检查默认的IPv4和IPv6设置
|
||||
ipv4_enabled = os.getenv("IPV4_ENABLED", "true").lower() == "true"
|
||||
ipv6_enabled = os.getenv("IPV6_ENABLED", "true").lower() == "true"
|
||||
|
||||
if ipv4_enabled:
|
||||
ipv4_config = base_config.copy()
|
||||
ipv4_config["record_type"] = "A"
|
||||
DOMAINS.append(ipv4_config)
|
||||
|
||||
if ipv6_enabled:
|
||||
ipv6_config = base_config.copy()
|
||||
ipv6_config["record_type"] = "AAAA"
|
||||
DOMAINS.append(ipv6_config)
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL = "INFO"
|
8
docker-compose.yml
Normal file
8
docker-compose.yml
Normal file
@ -0,0 +1,8 @@
|
||||
services:
|
||||
dnspod-yxip:
|
||||
image: woodchen/dnspod-yxip:latest
|
||||
container_name: dnspod-yxip
|
||||
restart: always
|
||||
volumes:
|
||||
- ./.env:/app/.env:ro # 映射环境变量文件
|
||||
- ./logs:/app/logs # 映射日志目录
|
282
main.py
Normal file
282
main.py
Normal file
@ -0,0 +1,282 @@
|
||||
import json
|
||||
import time
|
||||
import requests
|
||||
import schedule
|
||||
from loguru import logger
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import config
|
||||
|
||||
# 配置日志
|
||||
logger.add("dnspod.log", rotation="10 MB", level=config.LOG_LEVEL)
|
||||
|
||||
|
||||
class DNSPodManager:
|
||||
def __init__(self):
|
||||
self.api_url = "https://dnsapi.cn"
|
||||
self.common_params = {
|
||||
"login_token": f"{config.DNSPOD_ID},{config.DNSPOD_TOKEN}",
|
||||
"format": "json",
|
||||
"lang": "cn",
|
||||
"error_on_empty": "no",
|
||||
}
|
||||
# 记录每个域名每种记录类型的最后更新时间
|
||||
self.last_update = {} # 格式: {domain: {'A': timestamp, 'AAAA': timestamp}}
|
||||
|
||||
def _api_request(self, path: str, data: Dict) -> Dict:
|
||||
"""发送API请求"""
|
||||
try:
|
||||
url = f"{self.api_url}/{path}"
|
||||
response = requests.post(url, data={**self.common_params, **data})
|
||||
result = response.json()
|
||||
|
||||
if int(result.get("status", {}).get("code", -1)) != 1:
|
||||
raise Exception(result.get("status", {}).get("message", "未知错误"))
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"API请求失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def get_optimal_ips(self) -> Dict:
|
||||
"""获取优选IP"""
|
||||
try:
|
||||
response = requests.get(config.API_URL)
|
||||
data = response.json()
|
||||
if data.get("success"):
|
||||
return data["data"]
|
||||
raise Exception("API返回数据格式错误")
|
||||
except Exception as e:
|
||||
logger.error(f"获取优选IP失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def find_best_ip(self, ip_data: Dict, ip_version: str) -> Optional[Tuple[str, int]]:
|
||||
"""查找延迟最低的IP,返回(IP, 延迟)"""
|
||||
best_ip = None
|
||||
min_latency = float("inf")
|
||||
|
||||
# 遍历所有线路
|
||||
for line_key in ["CM", "CU", "CT"]:
|
||||
if line_key in ip_data[ip_version]:
|
||||
ips = ip_data[ip_version][line_key]
|
||||
for ip_info in ips:
|
||||
if ip_info["latency"] < min_latency:
|
||||
min_latency = ip_info["latency"]
|
||||
best_ip = (ip_info["ip"], ip_info["latency"])
|
||||
|
||||
return best_ip
|
||||
|
||||
def find_line_best_ip(
|
||||
self, ip_data: Dict, ip_version: str, line_key: str
|
||||
) -> Optional[Tuple[str, int]]:
|
||||
"""查找指定线路延迟最低的IP"""
|
||||
if line_key not in ip_data[ip_version]:
|
||||
return None
|
||||
|
||||
ips = ip_data[ip_version][line_key]
|
||||
if not ips:
|
||||
return None
|
||||
|
||||
best_ip = min(ips, key=lambda x: x["latency"])
|
||||
return (best_ip["ip"], best_ip["latency"])
|
||||
|
||||
def get_record_list(self, domain: str) -> List:
|
||||
"""获取域名记录列表"""
|
||||
try:
|
||||
result = self._api_request("Record.List", {"domain": domain})
|
||||
return result.get("records", [])
|
||||
except Exception as e:
|
||||
logger.error(f"获取记录列表失败: {str(e)}")
|
||||
return []
|
||||
|
||||
def delete_record(self, domain: str, record_id: str) -> bool:
|
||||
"""删除DNS记录"""
|
||||
try:
|
||||
self._api_request(
|
||||
"Record.Remove", {"domain": domain, "record_id": record_id}
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"删除记录失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def handle_record_conflicts(self, domain: str, sub_domain: str, record_type: str):
|
||||
"""处理记录冲突"""
|
||||
records = self.get_record_list(domain)
|
||||
for record in records:
|
||||
# 如果是要添加A记录,需要删除CNAME记录
|
||||
if (
|
||||
record_type == "A"
|
||||
and record["type"] == "CNAME"
|
||||
and record["name"] == sub_domain
|
||||
):
|
||||
logger.info(f"删除冲突的CNAME记录: {domain} - {sub_domain}")
|
||||
self.delete_record(domain, record["id"])
|
||||
# 如果是要添加CNAME记录,需要删除A记录
|
||||
elif (
|
||||
record_type == "CNAME"
|
||||
and record["type"] == "A"
|
||||
and record["name"] == sub_domain
|
||||
):
|
||||
logger.info(f"删除冲突的A记录: {domain} - {sub_domain}")
|
||||
self.delete_record(domain, record["id"])
|
||||
|
||||
def update_record(
|
||||
self,
|
||||
domain: str,
|
||||
sub_domain: str,
|
||||
record_type: str,
|
||||
line: str,
|
||||
value: str,
|
||||
ttl: int,
|
||||
remark: str = "YXIP",
|
||||
) -> bool:
|
||||
"""更新DNS记录"""
|
||||
try:
|
||||
# 处理记录冲突
|
||||
self.handle_record_conflicts(domain, sub_domain, record_type)
|
||||
|
||||
# 获取域名记录列表
|
||||
records = self.get_record_list(domain)
|
||||
|
||||
# 查找匹配的记录
|
||||
record_id = None
|
||||
for record in records:
|
||||
if (
|
||||
record["name"] == sub_domain
|
||||
and record["line"] == line
|
||||
and record["type"] == record_type
|
||||
):
|
||||
record_id = record["id"]
|
||||
break
|
||||
|
||||
# 更新或创建记录
|
||||
data = {
|
||||
"domain": domain,
|
||||
"sub_domain": sub_domain,
|
||||
"record_type": record_type,
|
||||
"record_line": line,
|
||||
"value": value,
|
||||
"ttl": ttl,
|
||||
"remark": remark,
|
||||
}
|
||||
|
||||
if record_id:
|
||||
data["record_id"] = record_id
|
||||
self._api_request("Record.Modify", data)
|
||||
else:
|
||||
self._api_request("Record.Create", data)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"更新DNS记录失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def update_domain_records(self, domain_config: Dict) -> None:
|
||||
"""更新单个域名的记录"""
|
||||
domain = domain_config["domain"]
|
||||
sub_domain = domain_config["sub_domain"]
|
||||
record_type = domain_config["record_type"]
|
||||
ttl = domain_config["ttl"]
|
||||
remark = domain_config["remark"]
|
||||
|
||||
# 获取优选IP数据
|
||||
ip_data = self.get_optimal_ips()
|
||||
if not ip_data:
|
||||
return
|
||||
|
||||
# 获取对应版本的IP数据
|
||||
ip_version = "v6" if record_type == "AAAA" else "v4"
|
||||
if ip_version not in ip_data:
|
||||
logger.warning(
|
||||
f"未找到{ip_version}版本的IP数据,跳过更新 {domain} 的 {record_type} 记录"
|
||||
)
|
||||
return
|
||||
|
||||
# 检查是否有可用的IP数据
|
||||
has_ip_data = False
|
||||
for line_key in ["CM", "CU", "CT"]:
|
||||
if line_key in ip_data[ip_version] and ip_data[ip_version][line_key]:
|
||||
has_ip_data = True
|
||||
break
|
||||
|
||||
if not has_ip_data:
|
||||
logger.warning(
|
||||
f"没有可用的{ip_version}版本IP数据,跳过更新 {domain} 的 {record_type} 记录"
|
||||
)
|
||||
return
|
||||
|
||||
# 先处理默认线路
|
||||
best_ip = self.find_best_ip(ip_data, ip_version)
|
||||
if best_ip:
|
||||
ip, latency = best_ip
|
||||
logger.info(
|
||||
f"更新{record_type}记录: {domain} - {sub_domain} - 默认 - {ip} (延迟: {latency}ms)"
|
||||
)
|
||||
self.update_record(domain, sub_domain, record_type, "默认", ip, ttl, remark)
|
||||
|
||||
# 更新其他线路的记录
|
||||
for line in domain_config["line"]:
|
||||
if line == "默认":
|
||||
continue
|
||||
|
||||
if line == "移动":
|
||||
line_key = "CM"
|
||||
elif line == "联通":
|
||||
line_key = "CU"
|
||||
elif line == "电信":
|
||||
line_key = "CT"
|
||||
else:
|
||||
continue
|
||||
|
||||
if line_key in ip_data[ip_version]:
|
||||
best_ip = self.find_line_best_ip(ip_data, ip_version, line_key)
|
||||
if best_ip:
|
||||
ip, latency = best_ip
|
||||
logger.info(
|
||||
f"更新{record_type}记录: {domain} - {sub_domain} - {line} - {ip} (延迟: {latency}ms)"
|
||||
)
|
||||
self.update_record(
|
||||
domain, sub_domain, record_type, line, ip, ttl, remark
|
||||
)
|
||||
|
||||
def check_and_update(self):
|
||||
"""检查并更新所有域名"""
|
||||
current_time = time.time()
|
||||
|
||||
for domain_config in config.DOMAINS:
|
||||
if not domain_config["enabled"]:
|
||||
continue
|
||||
|
||||
domain = domain_config["domain"]
|
||||
record_type = domain_config["record_type"]
|
||||
update_interval = domain_config["update_interval"] * 60 # 转换为秒
|
||||
|
||||
# 初始化域名的更新时间记录
|
||||
if domain not in self.last_update:
|
||||
self.last_update[domain] = {}
|
||||
|
||||
# 获取该记录类型的最后更新时间
|
||||
last_update = self.last_update[domain].get(record_type, 0)
|
||||
|
||||
# 检查是否需要更新
|
||||
if current_time - last_update >= update_interval:
|
||||
logger.info(f"开始更新域名: {domain} 的 {record_type} 记录")
|
||||
self.update_domain_records(domain_config)
|
||||
self.last_update[domain][record_type] = current_time
|
||||
|
||||
|
||||
def main():
|
||||
manager = DNSPodManager()
|
||||
|
||||
# 首次运行,更新所有域名
|
||||
manager.check_and_update()
|
||||
|
||||
# 每分钟检查一次是否需要更新
|
||||
schedule.every(1).minutes.do(manager.check_and_update)
|
||||
|
||||
while True:
|
||||
schedule.run_pending()
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
118
readme.md
Normal file
118
readme.md
Normal file
@ -0,0 +1,118 @@
|
||||
# DNSPOD 优选域名
|
||||
|
||||
根据API接口返回的IP地址, 在DNSPOD中创建或者更新域名的解析记录, 支持分线路和多权重。
|
||||
|
||||
支持docker运行, 需要配置DNSPOD的ID和TOKEN, 可以配置多个域名。默认每15分钟自动获取IP地址并更新DNSPOD。
|
||||
|
||||
## 特性
|
||||
|
||||
- 自动获取优选IP地址
|
||||
- 支持IPv4和IPv6
|
||||
- 支持移动、联通、电信三个线路
|
||||
- 自动选择延迟最低的IP
|
||||
- 支持多域名配置
|
||||
- 可配置的更新间隔和TTL
|
||||
- 完整的日志记录
|
||||
- Docker支持
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 使用 Docker Compose
|
||||
|
||||
1. 创建配置文件:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. 编辑 `.env` 文件,填写您的配置:
|
||||
```ini
|
||||
# DNSPOD API 配置
|
||||
DNSPOD_ID=your_id_here
|
||||
DNSPOD_TOKEN=your_token_here
|
||||
|
||||
# 域名配置 - 域名1
|
||||
DOMAIN_1=example1.com
|
||||
SUB_DOMAIN_1=@ # 子域名,@ 表示根域名
|
||||
REMARK_1=优选IP # 记录备注
|
||||
TTL_1=600 # TTL值(秒)
|
||||
UPDATE_INTERVAL_1=15 # 更新间隔(分钟)
|
||||
IPV4_ENABLED_1=true # 是否启用IPv4记录
|
||||
IPV6_ENABLED_1=true # 是否启用IPv6记录
|
||||
ENABLED_1=true # 是否启用此域名配置
|
||||
```
|
||||
|
||||
3. 拉取并运行容器:
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### 手动构建运行
|
||||
|
||||
1. 克隆仓库:
|
||||
```bash
|
||||
git clone https://github.com/your-username/dnspod-yxip.git
|
||||
cd dnspod-yxip
|
||||
```
|
||||
|
||||
2. 创建并编辑配置文件:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# 编辑 .env 文件
|
||||
```
|
||||
|
||||
3. 构建镜像:
|
||||
```bash
|
||||
docker compose build
|
||||
```
|
||||
|
||||
4. 运行容器:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
每个域名配置包含以下参数:
|
||||
- `DOMAIN_n`: 域名
|
||||
- `SUB_DOMAIN_n`: 子域名,@ 表示根域名,* 表示泛解析
|
||||
- `REMARK_n`: 记录备注
|
||||
- `TTL_n`: TTL值(秒)
|
||||
- `UPDATE_INTERVAL_n`: 更新间隔(分钟)
|
||||
- `IPV4_ENABLED_n`: 是否启用IPv4记录
|
||||
- `IPV6_ENABLED_n`: 是否启用IPv6记录
|
||||
- `ENABLED_n`: 是否启用此域名配置
|
||||
|
||||
其中 n 是域名编号(1, 2, 3...),可以配置任意数量的域名。
|
||||
|
||||
## 日志查看
|
||||
|
||||
日志文件保存在 `logs` 目录下:
|
||||
```bash
|
||||
# 查看实时日志
|
||||
docker compose logs -f
|
||||
|
||||
# 查看日志文件
|
||||
cat logs/dnspod.log
|
||||
```
|
||||
|
||||
## 更新
|
||||
|
||||
1. 拉取最新镜像:
|
||||
```bash
|
||||
docker compose pull
|
||||
```
|
||||
|
||||
2. 重新启动容器:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
requests>=2.31.0
|
||||
python-dotenv>=1.0.0
|
||||
schedule>=1.2.1
|
||||
loguru>=0.7.2
|
Loading…
x
Reference in New Issue
Block a user