This commit is contained in:
jxxghp
2023-06-06 07:15:17 +08:00
commit 4d06f86e62
217 changed files with 13959 additions and 0 deletions

0
app/utils/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

33
app/utils/dom.py Normal file
View File

@ -0,0 +1,33 @@
from typing import Union
class DomUtils:
@staticmethod
def tag_value(tag_item, tag_name: str, attname: str = "", default: Union[str, int] = None):
"""
解析XML标签值
"""
tagNames = tag_item.getElementsByTagName(tag_name)
if tagNames:
if attname:
attvalue = tagNames[0].getAttribute(attname)
if attvalue:
return attvalue
else:
firstChild = tagNames[0].firstChild
if firstChild:
return firstChild.data
return default
@staticmethod
def add_node(doc, parent, name: str, value: str = None):
"""
添加一个DOM节点
"""
node = doc.createElement(name)
parent.appendChild(node)
if value is not None:
text = doc.createTextNode(str(value))
node.appendChild(text)
return node

172
app/utils/http.py Normal file
View File

@ -0,0 +1,172 @@
from typing import Union, Any
import requests
import urllib3
from requests import Session
from urllib3.exceptions import InsecureRequestWarning
urllib3.disable_warnings(InsecureRequestWarning)
class RequestUtils:
_headers: dict = None
_cookies: Union[str, dict] = None
_proxies: dict = None
_timeout: int = 20
_session: Session = None
def __init__(self,
headers: dict = None,
ua: str = None,
cookies: Union[str, dict] = None,
proxies: dict = None,
session: Session = None,
timeout: int = None,
referer: str = None,
content_type: str = None,
accept_type: str = None):
if not content_type:
content_type = "application/x-www-form-urlencoded; charset=UTF-8"
if headers:
self._headers = headers
else:
self._headers = {
"User-Agent": ua,
"Content-Type": content_type,
"Accept": accept_type,
"referer": referer
}
if cookies:
if isinstance(cookies, str):
self._cookies = self.cookie_parse(cookies)
else:
self._cookies = cookies
if proxies:
self._proxies = proxies
if session:
self._session = session
if timeout:
self._timeout = timeout
def post(self, url: str, data: Any = None, json: dict = None):
if json is None:
json = {}
try:
if self._session:
return self._session.post(url,
data=data,
verify=False,
headers=self._headers,
proxies=self._proxies,
timeout=self._timeout,
json=json)
else:
return requests.post(url,
data=data,
verify=False,
headers=self._headers,
proxies=self._proxies,
timeout=self._timeout,
json=json)
except requests.exceptions.RequestException:
return None
def get(self, url: str, params: dict = None):
try:
if self._session:
r = self._session.get(url,
verify=False,
headers=self._headers,
proxies=self._proxies,
timeout=self._timeout,
params=params)
else:
r = requests.get(url,
verify=False,
headers=self._headers,
proxies=self._proxies,
timeout=self._timeout,
params=params)
return str(r.content, 'utf-8')
except requests.exceptions.RequestException:
return None
def get_res(self, url: str, params: dict = None, allow_redirects: bool = True, raise_exception: bool = False):
try:
if self._session:
return self._session.get(url,
params=params,
verify=False,
headers=self._headers,
proxies=self._proxies,
cookies=self._cookies,
timeout=self._timeout,
allow_redirects=allow_redirects)
else:
return requests.get(url,
params=params,
verify=False,
headers=self._headers,
proxies=self._proxies,
cookies=self._cookies,
timeout=self._timeout,
allow_redirects=allow_redirects)
except requests.exceptions.RequestException:
if raise_exception:
raise requests.exceptions.RequestException
return None
def post_res(self, url: str, data: Any = None, params: dict = None, allow_redirects: bool = True,
files: Any = None,
json: dict = None):
try:
if self._session:
return self._session.post(url,
data=data,
params=params,
verify=False,
headers=self._headers,
proxies=self._proxies,
cookies=self._cookies,
timeout=self._timeout,
allow_redirects=allow_redirects,
files=files,
json=json)
else:
return requests.post(url,
data=data,
params=params,
verify=False,
headers=self._headers,
proxies=self._proxies,
cookies=self._cookies,
timeout=self._timeout,
allow_redirects=allow_redirects,
files=files,
json=json)
except requests.exceptions.RequestException:
return None
@staticmethod
def cookie_parse(cookies_str: str, array: bool = False):
"""
解析cookie转化为字典或者数组
:param cookies_str: cookie字符串
:param array: 是否转化为数组
:return: 字典或者数组
"""
if not cookies_str:
return {}
cookie_dict = {}
cookies = cookies_str.split(';')
for cookie in cookies:
cstr = cookie.split('=')
if len(cstr) > 1:
cookie_dict[cstr[0].strip()] = cstr[1].strip()
if array:
cookiesList = []
for cookieName, cookieValue in cookie_dict.items():
cookies = {'name': cookieName, 'value': cookieValue}
cookiesList.append(cookies)
return cookiesList
return cookie_dict

11
app/utils/object.py Normal file
View File

@ -0,0 +1,11 @@
from typing import Any
class ObjectUtils:
@staticmethod
def is_obj(obj: Any):
if isinstance(obj, list) or isinstance(obj, dict):
return True
else:
return str(obj).startswith("{") or str(obj).startswith("[")

20
app/utils/singleton.py Normal file
View File

@ -0,0 +1,20 @@
import abc
class Singleton(abc.ABCMeta, type):
"""
类单例模式
"""
_instances: dict = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class AbstractSingleton(abc.ABC, metaclass=Singleton):
"""
抽像类单例模式
"""

514
app/utils/string.py Normal file
View File

@ -0,0 +1,514 @@
import bisect
import datetime
import hashlib
import random
import re
from typing import Union, Tuple, Optional, Any, List, Generator
from urllib import parse
import cn2an
import dateparser
import dateutil.parser
from app.utils.types import MediaType
class StringUtils:
@staticmethod
def num_filesize(text: Union[str, int, float]) -> int:
"""
将文件大小文本转化为字节
"""
if not text:
return 0
if not isinstance(text, str):
text = str(text)
if text.isdigit():
return int(text)
text = text.replace(",", "").replace(" ", "").upper()
size = re.sub(r"[KMGTPI]*B?", "", text, flags=re.IGNORECASE)
try:
size = float(size)
except ValueError:
return 0
if text.find("PB") != -1 or text.find("PIB") != -1:
size *= 1024 ** 5
elif text.find("TB") != -1 or text.find("TIB") != -1:
size *= 1024 ** 4
elif text.find("GB") != -1 or text.find("GIB") != -1:
size *= 1024 ** 3
elif text.find("MB") != -1 or text.find("MIB") != -1:
size *= 1024 ** 2
elif text.find("KB") != -1 or text.find("KIB") != -1:
size *= 1024
return round(size)
@staticmethod
def str_timelong(time_sec: Union[str, int, float]) -> str:
"""
将数字转换为时间描述
"""
if not isinstance(time_sec, int) or not isinstance(time_sec, float):
try:
time_sec = float(time_sec)
except ValueError:
return ""
d = [(0, ''), (60 - 1, ''), (3600 - 1, '小时'), (86400 - 1, '')]
s = [x[0] for x in d]
index = bisect.bisect_left(s, time_sec) - 1
if index == -1:
return str(time_sec)
else:
b, u = d[index]
return str(round(time_sec / (b + 1))) + u
@staticmethod
def is_chinese(word: Union[str, list]) -> bool:
"""
判断是否含有中文
"""
if isinstance(word, list):
word = " ".join(word)
chn = re.compile(r'[\u4e00-\u9fff]')
if chn.search(word):
return True
else:
return False
@staticmethod
def is_japanese(word: str) -> bool:
"""
判断是否含有日文
"""
jap = re.compile(r'[\u3040-\u309F\u30A0-\u30FF]')
if jap.search(word):
return True
else:
return False
@staticmethod
def is_korean(word: str) -> bool:
"""
判断是否包含韩文
"""
kor = re.compile(r'[\uAC00-\uD7FF]')
if kor.search(word):
return True
else:
return False
@staticmethod
def is_all_chinese(word: str) -> bool:
"""
判断是否全是中文
"""
for ch in word:
if ch == ' ':
continue
if '\u4e00' <= ch <= '\u9fff':
continue
else:
return False
return True
@staticmethod
def str_int(text: str) -> int:
"""
web字符串转int
:param text:
:return:
"""
if text:
text = text.strip()
if not text:
return 0
try:
return int(text.replace(',', ''))
except ValueError:
return 0
@staticmethod
def str_float(text: str) -> float:
"""
web字符串转float
:param text:
:return:
"""
if text:
text = text.strip()
if not text:
return 0.0
try:
text = text.replace(',', '')
if text:
return float(text)
except ValueError:
pass
return 0.0
@staticmethod
def clear_special_chars(text: Union[list, str], replace_word: str = "",
allow_space: bool = False) -> Union[list, str]:
"""
忽略特殊字符
"""
# 需要忽略的特殊字符
CONVERT_EMPTY_CHARS = r"[、.。,,·:;!'\"“”()\[\]【】「」\-——\+\|\\_/&#~]"
if not text:
return text
if not isinstance(text, list):
text = re.sub(r"[\u200B-\u200D\uFEFF]",
"",
re.sub(r"%s" % CONVERT_EMPTY_CHARS, replace_word, text),
flags=re.IGNORECASE)
if not allow_space:
return re.sub(r"\s+", "", text)
else:
return re.sub(r"\s+", " ", text).strip()
else:
return [StringUtils.clear_special_chars(x) for x in text]
@staticmethod
def str_filesize(size: Union[str, float, int], pre: int = 2) -> str:
"""
将字节计算为文件大小描述(带单位的格式化后返回)
"""
if size is None:
return ""
size = re.sub(r"\s|B|iB", "", str(size), re.I)
if size.replace(".", "").isdigit():
try:
size = float(size)
d = [(1024 - 1, 'K'), (1024 ** 2 - 1, 'M'), (1024 ** 3 - 1, 'G'), (1024 ** 4 - 1, 'T')]
s = [x[0] for x in d]
index = bisect.bisect_left(s, size) - 1
if index == -1:
return str(size) + "B"
else:
b, u = d[index]
return str(round(size / (b + 1), pre)) + u
except ValueError:
return ""
if re.findall(r"[KMGTP]", size, re.I):
return size
else:
return size + "B"
@staticmethod
def url_equal(url1: str, url2: str) -> bool:
"""
比较两个地址是否为同一个网站
"""
if not url1 or not url2:
return False
if url1.startswith("http"):
url1 = parse.urlparse(url1).netloc
if url2.startswith("http"):
url2 = parse.urlparse(url2).netloc
if url1.replace("www.", "") == url2.replace("www.", ""):
return True
return False
@staticmethod
def get_url_netloc(url: str) -> Tuple[str, str]:
"""
获取URL的协议和域名部分
"""
if not url:
return "", ""
if not url.startswith("http"):
return "http", url
addr = parse.urlparse(url)
return addr.scheme, addr.netloc
@staticmethod
def get_url_domain(url: str) -> str:
"""
获取URL的域名部分只保留最后两级
"""
if not url:
return ""
_, netloc = StringUtils.get_url_netloc(url)
if netloc:
return ".".join(netloc.split(".")[-2:])
return ""
@staticmethod
def get_url_sld(url: str) -> str:
"""
获取URL的二级域名部分不含端口若为IP则返回IP
"""
if not url:
return ""
_, netloc = StringUtils.get_url_netloc(url)
if not netloc:
return ""
netloc = netloc.split(":")[0].split(".")
if len(netloc) >= 2:
return netloc[-2]
return netloc[0]
@staticmethod
def get_base_url(url: str) -> str:
"""
获取URL根地址
"""
if not url:
return ""
scheme, netloc = StringUtils.get_url_netloc(url)
return f"{scheme}://{netloc}"
@staticmethod
def clear_file_name(name: str) -> Optional[str]:
if not name:
return None
return re.sub(r"[*?\\/\"<>~|]", "", name, flags=re.IGNORECASE).replace(":", "")
@staticmethod
def generate_random_str(randomlength: int = 16) -> str:
"""
生成一个指定长度的随机字符串
"""
random_str = ''
base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789'
length = len(base_str) - 1
for i in range(randomlength):
random_str += base_str[random.randint(0, length)]
return random_str
@staticmethod
def get_time(date: Any) -> datetime:
try:
return dateutil.parser.parse(date)
except dateutil.parser.ParserError:
return None
@staticmethod
def unify_datetime_str(datetime_str: str) -> str:
"""
日期时间格式化 统一转成 2020-10-14 07:48:04 这种格式
# 场景1: 带有时区的日期字符串 eg: Sat, 15 Oct 2022 14:02:54 +0800
# 场景2: 中间带T的日期字符串 eg: 2020-10-14T07:48:04
# 场景3: 中间带T的日期字符串 eg: 2020-10-14T07:48:04.208
# 场景4: 日期字符串以GMT结尾 eg: Fri, 14 Oct 2022 07:48:04 GMT
# 场景5: 日期字符串以UTC结尾 eg: Fri, 14 Oct 2022 07:48:04 UTC
# 场景6: 日期字符串以Z结尾 eg: Fri, 14 Oct 2022 07:48:04Z
# 场景7: 日期字符串为相对时间 eg: 1 month, 2 days ago
:param datetime_str:
:return:
"""
# 传入的参数如果是None 或者空字符串 直接返回
if not datetime_str:
return datetime_str
try:
return dateparser.parse(datetime_str).strftime('%Y-%m-%d %H:%M:%S')
except Exception as e:
print(str(e))
return datetime_str
@staticmethod
def format_timestamp(timestamp: str, date_format: str = '%Y-%m-%d %H:%M:%S') -> str:
"""
时间戳转日期
:param timestamp:
:param date_format:
:return:
"""
if isinstance(timestamp, str) and not timestamp.isdigit():
return timestamp
try:
return datetime.datetime.fromtimestamp(int(timestamp)).strftime(date_format)
except Exception as e:
print(str(e))
return timestamp
@staticmethod
def to_bool(text: str, default_val: bool = False) -> bool:
"""
字符串转bool
:param text: 要转换的值
:param default_val: 默认值
:return:
"""
if isinstance(text, str) and not text:
return default_val
if isinstance(text, bool):
return text
if isinstance(text, int) or isinstance(text, float):
return True if text > 0 else False
if isinstance(text, str) and text.lower() in ['y', 'true', '1', 'yes', 'on']:
return True
return False
@staticmethod
def str_from_cookiejar(cj: dict) -> str:
"""
将cookiejar转换为字符串
:param cj:
:return:
"""
return '; '.join(['='.join(item) for item in cj.items()])
@staticmethod
def get_idlist(content: str, dicts: List[dict]):
"""
从字符串中提取id列表
:param content: 字符串
:param dicts: 字典
:return:
"""
if not content:
return []
id_list = []
content_list = content.split()
for dic in dicts:
if dic.get('name') in content_list and dic.get('id') not in id_list:
id_list.append(dic.get('id'))
content = content.replace(dic.get('name'), '')
return id_list, re.sub(r'\s+', ' ', content).strip()
@staticmethod
def md5_hash(data: Any) -> str:
"""
MD5 HASH
"""
if not data:
return ""
return hashlib.md5(str(data).encode()).hexdigest()
@staticmethod
def str_timehours(minutes: int) -> str:
"""
将分钟转换成小时和分钟
:param minutes:
:return:
"""
if not minutes:
return ""
hours = minutes // 60
minutes = minutes % 60
if hours:
return "%s小时%s" % (hours, minutes)
else:
return "%s分钟" % minutes
@staticmethod
def str_amount(amount: object, curr="$") -> str:
"""
格式化显示金额
"""
if not amount:
return "0"
return curr + format(amount, ",")
@staticmethod
def count_words(s: str) -> int:
"""
计算字符串中包含的单词数量,只适用于简单的单行文本
:param s: 要计算的字符串
:return: 字符串中包含的单词数量
"""
# 匹配英文单词
if re.match(r'^[A-Za-z0-9\s]+$', s):
# 如果是英文字符串,则按空格分隔单词,并计算单词数量
num_words = len(s.split())
else:
# 如果不是英文字符串,则计算字符数量
num_words = len(s)
return num_words
@staticmethod
def split_text(text: str, max_length: int) -> Generator:
"""
把文本拆分为固定字节长度的数组,优先按换行拆分,避免单词内拆分
"""
if not text:
yield ''
# 分行
lines = re.split('\n', text)
buf = ''
for line in lines:
if len(line.encode('utf-8')) > max_length:
# 超长行继续拆分
blank = ""
if re.match(r'^[A-Za-z0-9.\s]+', line):
# 英文行按空格拆分
parts = line.split()
blank = " "
else:
# 中文行按字符拆分
parts = line
part = ''
for p in parts:
if len((part + p).encode('utf-8')) > max_length:
# 超长则Yield
yield (buf + part).strip()
buf = ''
part = f"{blank}{p}"
else:
part = f"{part}{blank}{p}"
if part:
# 将最后的部分追加到buf
buf += part
else:
if len((buf + "\n" + line).encode('utf-8')) > max_length:
# buf超长则Yield
yield buf.strip()
buf = line
else:
# 短行直接追加到buf
if buf:
buf = f"{buf}\n{line}"
else:
buf = line
if buf:
# 处理文本末尾剩余部分
yield buf.strip()
@staticmethod
def get_keyword(content: str) \
-> Tuple[Optional[MediaType], Optional[str], Optional[int], Optional[int], Optional[str], Optional[str]]:
"""
从搜索关键字中拆分中年份、季、集、类型
"""
if not content:
return None, None, None, None, None, None
# 去掉查询中的电影或电视剧关键字
mtype = MediaType.TV if re.search(r'^(电视剧|动漫|\s+电视剧|\s+动漫)', content) else None
content = re.sub(r'^(电影|电视剧|动漫|\s+电影|\s+电视剧|\s+动漫)', '', content).strip()
# 稍微切一下剧集吧
season_num = None
episode_num = None
season_re = re.search(r'\s*([0-9一二三四五六七八九十]+)\s*季', content, re.IGNORECASE)
if season_re:
mtype = MediaType.TV
season_num = int(cn2an.cn2an(season_re.group(1), mode='smart'))
episode_re = re.search(r'\s*([0-9一二三四五六七八九十百零]+)\s*集', content, re.IGNORECASE)
if episode_re:
mtype = MediaType.TV
episode_num = int(cn2an.cn2an(episode_re.group(1), mode='smart'))
if episode_num and not season_num:
season_num = 1
year_re = re.search(r'[\s(]+(\d{4})[\s)]*', content)
year = year_re.group(1) if year_re else None
key_word = re.sub(
r'\s*[0-9一二三四五六七八九十]+\s*季|第\s*[0-9一二三四五六七八九十百零]+\s*集|[\s(]+(\d{4})[\s)]*', '',
content, flags=re.IGNORECASE).strip()
key_word = re.sub(r'\s+', ' ', key_word) if key_word else year
return mtype, key_word, season_num, episode_num, year, content
@staticmethod
def str_title(s: str) -> str:
"""
大写首字母兼容None
"""
return s.title() if s else s

85
app/utils/system.py Normal file
View File

@ -0,0 +1,85 @@
import os
import platform
import shutil
from pathlib import Path
class SystemUtils:
@staticmethod
def execute(cmd: str) -> str:
"""
执行命令,获得返回结果
"""
try:
with os.popen(cmd) as p:
return p.readline().strip()
except Exception as err:
print(str(err))
return ""
@staticmethod
def is_docker() -> bool:
return Path("/.dockerenv").exists()
@staticmethod
def is_synology() -> bool:
if SystemUtils.is_windows():
return False
return True if "synology" in SystemUtils.execute('uname -a') else False
@staticmethod
def is_windows() -> bool:
return True if os.name == "nt" else False
@staticmethod
def is_macos() -> bool:
return True if platform.system() == 'Darwin' else False
@staticmethod
def copy(src: Path, dest: Path):
"""
复制
"""
try:
shutil.copy2(src, dest)
return 0, ""
except Exception as err:
print(str(err))
return -1, str(err)
@staticmethod
def move(src: Path, dest: Path):
"""
移动
"""
try:
shutil.move(src.with_name(dest.name), dest)
return 0, ""
except Exception as err:
print(str(err))
return -1, str(err)
@staticmethod
def link(src: Path, dest: Path):
"""
硬链接
"""
try:
os.link(src, dest)
return 0, ""
except Exception as err:
print(str(err))
return -1, str(err)
@staticmethod
def softlink(src: Path, dest: Path):
"""
软链接
"""
try:
os.symlink(src, dest)
return 0, ""
except Exception as err:
print(str(err))
return -1, str(err)

39
app/utils/timer.py Normal file
View File

@ -0,0 +1,39 @@
import datetime
import random
from typing import List
class TimerUtils:
@staticmethod
def random_scheduler(num_executions: int = 1,
begin_hour: int = 7,
end_hour: int = 23,
min_interval: int = 20,
max_interval: int = 40) -> List[datetime.datetime]:
"""
按执行次数生成随机定时器
:param num_executions: 执行次数
:param begin_hour: 开始时间
:param end_hour: 结束时间
:param min_interval: 最小间隔分钟
:param max_interval: 最大间隔分钟
"""
trigger: list = []
# 当前时间
now = datetime.datetime.now()
# 创建随机的时间触发器
random_trigger = now.replace(hour=begin_hour, minute=0, second=0, microsecond=0)
for _ in range(num_executions):
# 随机生成下一个任务的时间间隔
interval_minutes = random.randint(min_interval, max_interval)
random_interval = datetime.timedelta(minutes=interval_minutes)
# 更新当前时间为下一个任务的时间触发器
random_trigger += random_interval
# 达到结否时间时退出
if now.hour > end_hour:
break
# 添加到队列
trigger.append(random_trigger)
return trigger

38
app/utils/tokens.py Normal file
View File

@ -0,0 +1,38 @@
import re
class Tokens:
_text: str = ""
_index: int = 0
_tokens: list = []
def __init__(self, text):
self._text = text
self._tokens = []
self.load_text(text)
def load_text(self, text):
splited_text = re.split(r"\.|\s+|\(|\)|\[|]|-|\+|【|】|/||;|&|\||#|_|「|」|~", text)
for sub_text in splited_text:
if sub_text:
self._tokens.append(sub_text)
def cur(self):
if self._index >= len(self._tokens):
return None
else:
token = self._tokens[self._index]
return token
def get_next(self):
token = self.cur()
if token:
self._index = self._index + 1
return token
def peek(self):
index = self._index + 1
if index >= len(self._tokens):
return None
else:
return self._tokens[index]

21
app/utils/types.py Normal file
View File

@ -0,0 +1,21 @@
from enum import Enum
class MediaType(Enum):
MOVIE = '电影'
TV = '电视剧'
UNKNOWN = '未知'
# 可监听事件
class EventType(Enum):
# 插件重载
PluginReload = "plugin.reload"
# 执行命令
CommandExcute = "command.excute"
# 系统配置Key字典
class SystemConfigKey(Enum):
# 用户已安装的插件
UserInstalledPlugins = "UserInstalledPlugins"