Files

279 lines
11 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""业务逻辑所需的或有一定通用性的函数"""
# 为了降低耦合度,也避免功能复杂后可能出现的循环导入的问题,这里尽量不导入项目内部的模块
# 如果需要获得配置信息,也应当由外部模块将配置项的值以参数的形式传入
import os
import re
import sys
import time
import shutil
import zipfile
import logging
import subprocess
import platform
from datetime import datetime
from packaging import version
from colorama import Style
from pathlib import Path
import importlib.metadata as meta
# 判断系统是否可以使用tk
USE_GUI = True
try:
from tkinter import filedialog, Tk
except ImportError:
USE_GUI = False
from javsp.web.base import *
from javsp.lib import re_escape, resource_path
from javsp.prompt import prompt
__all__ = ['select_folder', 'get_scan_dir', 'remove_trail_actor_in_title',
'shutdown', 'CLEAR_LINE', 'check_update', 'split_by_punc']
CLEAR_LINE = '\r\x1b[K'
logger = logging.getLogger(__name__)
def select_folder(default_dir=''):
"""使用文件对话框提示用户选择一个文件夹"""
if not USE_GUI:
logger.error("无法打开窗口,请通过命令行的方式输入扫描路径")
exit(1)
window = Tk()
window.withdraw()
window.iconbitmap(resource_path('image/JavSP.ico'))
path = filedialog.askdirectory(initialdir=default_dir)
if path != '':
return os.path.normpath(path)
def get_scan_dir(cfg_scan_dir: Path | None) -> str | None:
"""综合命令参数、配置文件等信息,返回要扫描影片的文件夹"""
# 目前config模块负责处理来自命令行和来自文件的配置,cfg_scan_dir已经是综合了这两处后得到的结果
if cfg_scan_dir:
if cfg_scan_dir.exists():
return str(cfg_scan_dir)
else:
logger.error(f"配置的待整理文件夹无效:'{cfg_scan_dir}'")
else:
if platform.system().lower() == 'windows':
print('请选择要整理的文件夹:', end='')
root = select_folder()
else:
root = prompt('请选择要整理的文件夹路径,必须是绝对路径: ', "要整理的文件夹")
print(root)
return root
def remove_trail_actor_in_title(title:str, actors:list) -> str:
"""寻找并移除标题尾部的女优名"""
if not (actors and title):
return title
# 目前使用分隔符白名单来做检测(担心按Unicode范围匹配误伤太多),考虑尽可能多的分隔符
delimiters = '-xX &·,; &・,;'
actor_ls = [re_escape(i) for i in actors if i]
pattern = f"^(.*?)([{delimiters}]{{1,3}}({'|'.join(actor_ls)}))+$"
# 使用match而不是sub是为了将替换掉的部分写入日志
match = re.match(pattern, title)
if match:
logger.debug(f"移除标题尾部的女优名: '{match.group(1)}' [{match.group(2)}]")
return match.group(1)
else:
return title
def shutdown(timeout=120):
"""关闭计算机"""
try:
for i in reversed(range(timeout)):
print(CLEAR_LINE + f"JavSP整理完成,将在 {i} 秒后关机。按'Ctrl+C'取消", end='')
time.sleep(1)
logger.info('整理完成,自动关机')
#TODO: 当前仅支持Windows平台
os.system('shutdown -s')
except KeyboardInterrupt:
return
def utc2local(utc_str):
"""将UTC时间转换为本地时间"""
# python不支持 ISO-8601 中的Z后缀
now = time.time()
offset = datetime.fromtimestamp(now) - datetime.utcfromtimestamp(now)
utc_str = utc_str.replace('Z', '+00:00')
utc_time = datetime.fromisoformat(utc_str)
local_time = utc_time + offset
return local_time
def get_actual_width(mix_str: str) -> int:
"""给定一个中英混合的字符串,返回实际的显示宽度"""
width = len(mix_str)
for c in mix_str:
if u'\u4e00' <= c <= u'\u9fa5':
width += 1
return width
def align_center(mix_str: str, total_width: int) -> str:
"""给定一个中英混合的字符串,根据其实际显示宽度中心对齐"""
actual_width = get_actual_width(mix_str)
add_space = int((total_width - actual_width) / 2)
aligned_str = ' ' * add_space + mix_str
return aligned_str
# 枚举Unicode各平面内中日韩区块及拉丁字母区块内的所有标点符号
_punc = (
"  ", # spaces
"!\"#%&'()*,-./:;?@[\\]_{}", # (0x0, 0x7f), Basic Latin
"¡§«¶·»¿", # (0x80, 0xff), Latin-1 Supplement
";·", # (0x370, 0x3ff), Greek and Coptic
"‐‑‒–—―‖‗‘’‚‛“”„‟†‡•‣․‥…‧‰‱′″‴‵‶‷‸‹›※‼‽‾‿⁀⁁⁂⁃⁅⁆⁇⁈⁉⁊⁋⁌⁍⁎⁏⁐⁑⁓⁔⁕⁖⁗⁘⁙⁚⁛⁜⁝⁞", # (0x2000, 0x206f), General Punctuation
"、。〃〈〉《》「」『』【】〔〕〖〗〘〙〚〛〜〝〞〟〰〽", # (0x3000, 0x303f), CJK Symbols and Punctuation
"゠・", # (0x30a0, 0x30ff), Katakana
"︐︑︒︓︔︕︖︗︘︙", # (0xfe10, 0xfe1f), Vertical Forms
"︰︱︲︳︴︵︶︷︸︹︺︻︼︽︾︿﹀﹁﹂﹃﹄﹅﹆﹇﹈﹉﹊﹋﹌﹍﹎﹏", # (0xfe30, 0xfe4f), CJK Compatibility Forms
# "﹐﹑﹒﹔﹕﹖﹗﹘﹙﹚﹛﹜﹝﹞﹟﹠﹡﹣﹨﹪﹫", # (0xfe50, 0xfe6f), Small Form Variants
"!"#%&'()*,-./:;?@[\]_{}⦅⦆。「」、・", # (0xff00, 0xffef), Halfwidth and Fullwidth Forms
)
_punc_pattern = re.compile('.*?[' + ''.join(_punc) + ']')
def split_by_punc(s):
"""将一个字符串按照Unicode标准中的标点符号进行分割"""
iters = list(_punc_pattern.finditer(s))
if iters:
ls = [s[i.span()[0]: i.span()[1]] for i in iters]
ls.append(s[iters[-1].span()[1]:])
else:
ls = [s]
return ls
def check_update(allow_check=True, auto_update=True):
"""检查版本更新"""
def print_header(title, info=[]):
title_width = max([get_actual_width(i) for i in title])
if info:
info_width = max([get_actual_width(i) for i in info])
else:
info_width = 0
terminal_width = shutil.get_terminal_size().columns
display_width = min(max(title_width, info_width) + 6, terminal_width)
print('=' * display_width)
for line in title:
print(align_center(line, display_width))
if info:
print('-' * display_width)
for line in info:
print(line)
print('=' * display_width)
print('')
# 使用pyinstaller打包exe时生成hook,运行时由该hook将版本信息注入到sys中
local_version = meta.version('javsp')
if local_version == "":
return
# 检查更新
if allow_check:
api_url = 'https://api.github.com/repos/Yuukiy/JavSP/releases/latest'
release_url = 'https://github.com/Yuukiy/JavSP/releases/latest'
print('正在检查更新...', end='')
try:
data = request_get(api_url, timeout=3).json()
latest_version = data['tag_name']
release_time = utc2local(data['published_at'])
release_date = release_time.isoformat().split('T')[0]
if version.parse(local_version) < version.parse(latest_version):
update_status = 'new_version'
else:
update_status = 'already_latest'
except Exception as e:
logger.debug('检查版本更新时出错: ' + repr(e))
update_status = 'fail_to_check'
else:
update_status = 'disallow'
# 根据检查更新的情况输出软件版本信息和更新信息
print(CLEAR_LINE, end='')
if update_status == 'disallow':
title = f'Jav Scraper Package: {local_version}'
print_header([title])
elif update_status == 'already_latest':
title = f'Jav Scraper Package: {local_version} (已是最新版)'
print_header([title])
elif update_status == 'fail_to_check':
release_url_mirror = 'https://hub.fastgit.xyz/Yuukiy/JavSP/releases/latest'
titles = [f'Jav Scraper Package: {local_version}']
info = ['检查更新失败,请前往以下地址查看最新版本:', ' '+release_url,
'如果你打不开上面的地址,也可以尝试访问镜像站点:', ' '+release_url_mirror]
print_header(titles, info)
elif update_status == 'new_version':
titles = [f'Jav Scraper Package: {local_version}']
titles.append(f'↓ 有新版本可下载: {latest_version} ↓')
titles.append(release_url)
# 提取changelog消息
try:
enable_msg_head = True
lines = data['body'].splitlines()
changelog = [f'更新时间: {release_date}']
for line in lines:
if line.startswith('## '):
enable_msg_head = False
changelog.append(Style.BRIGHT + line[3:] + Style.RESET_ALL)
elif line.startswith('- '):
enable_msg_head = False
changelog.append(line)
elif enable_msg_head:
changelog.append(line)
print_header(titles, changelog)
except:
print_header(titles)
# 尝试自动更新
if auto_update:
try:
logger.info('尝试自动更新到新版本: ' + latest_version + " (按'Ctrl+C'取消)")
download_update(data)
except KeyboardInterrupt:
logger.info('用户取消更新')
except Exception as e:
logger.warning('自动更新失败,请重启程序再试或者手动下载更新')
logger.debug(e, exc_info=True)
finally:
print() # 输出空行,作为新旧程序的分隔
def download_update(rel_info):
"""下载版本更新
Args:
rel_info (json): 调用Github API得到的最新版的release信息
"""
if rel_info.get('assets') and getattr(sys, 'frozen', False):
down_url = rel_info['assets'][0]['browser_download_url']
asset_name = rel_info['assets'][0]['name']
desc = '下载更新' if shutil.get_terminal_size().columns < 120 else '下载更新: '+asset_name
download(down_url, asset_name, desc=desc)
if os.path.exists(asset_name):
# 备份原有的程序
basepath, ext = os.path.splitext(sys.executable)
backup_name = basepath + '_backup' + ext
if os.path.exists(backup_name):
os.remove(backup_name)
os.rename(sys.executable, backup_name)
# 解压下载的zip文件
with zipfile.ZipFile(asset_name, 'r') as zip_ref:
zip_ref.extractall()
logger.info('更新完成,启动新版本程序...')
args = [sys.executable] + sys.argv[1:]
p = subprocess.Popen(args, start_new_session=True)
p.wait()
p.terminate()
sys.exit(0)
if __name__ == "__main__":
setattr(sys, 'javsp_version', 'v0')
check_update()