init
This commit is contained in:
1440
app/core/services/file_thread.py
Normal file
1440
app/core/services/file_thread.py
Normal file
File diff suppressed because it is too large
Load Diff
166
app/core/services/login_thread.py
Normal file
166
app/core/services/login_thread.py
Normal file
@@ -0,0 +1,166 @@
|
||||
import base64
|
||||
|
||||
from loguru import logger
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
||||
from PyQt6.QtGui import (QColor, QPainter, QPainterPath, QPen, QPixmap)
|
||||
|
||||
from ..api import miaoStarsBasicApi
|
||||
from ...core import cfg, qconfig, userConfig
|
||||
|
||||
|
||||
class CaptchaThread(QThread):
|
||||
captchaReady = pyqtSignal(QPixmap)
|
||||
captchaFailed = pyqtSignal(str)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
@staticmethod
|
||||
def _createRoundedPixmap(pixmap, radius=10):
|
||||
"""创建圆角图片"""
|
||||
try:
|
||||
# 获取原始图片尺寸
|
||||
if pixmap.isNull():
|
||||
logger.error("原始图片为空,无法创建圆角图片")
|
||||
return pixmap
|
||||
size = pixmap.size()
|
||||
# 创建透明背景的图片
|
||||
rounded_pixmap = QPixmap(size)
|
||||
rounded_pixmap.fill(Qt.GlobalColor.transparent)
|
||||
# 创建对象
|
||||
painter = QPainter(rounded_pixmap)
|
||||
painter.setRenderHints(
|
||||
QPainter.RenderHint.Antialiasing
|
||||
| QPainter.RenderHint.SmoothPixmapTransform
|
||||
)
|
||||
# 创建圆角矩形路径
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(0, 0, size.width(), size.height(), radius, radius)
|
||||
# 设置裁剪区域
|
||||
painter.setClipPath(path)
|
||||
# 绘制原始图片
|
||||
painter.drawPixmap(0, 0, pixmap)
|
||||
# 绘制边框
|
||||
pen = QPen(QColor(200, 200, 200)) # 浅灰色边框
|
||||
pen.setWidth(1)
|
||||
painter.setPen(pen)
|
||||
painter.drawRoundedRect(
|
||||
0, 0, size.width() - 1, size.height() - 1, radius, radius
|
||||
)
|
||||
|
||||
painter.end()
|
||||
return rounded_pixmap
|
||||
except Exception as e:
|
||||
logger.error(f"创建圆角图片失败:{e}")
|
||||
return pixmap # 如果出错,返回原始图片
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
logger.debug("开始获取验证码")
|
||||
response = miaoStarsBasicApi.getCaptcha()
|
||||
logger.debug(f"验证码API返回响应: {response}")
|
||||
|
||||
if response["code"] == 0:
|
||||
# 确保data字段存在且为字符串
|
||||
if "data" in response and isinstance(response["data"], str):
|
||||
# 分割base64前缀和实际数据
|
||||
try:
|
||||
captchaImageData = response["data"].split(",")[1]
|
||||
logger.debug(f"成功提取base64数据,长度: {len(captchaImageData)}")
|
||||
|
||||
# 解码base64数据
|
||||
captchaImage = base64.b64decode(captchaImageData)
|
||||
logger.debug(f"成功解码base64数据,长度: {len(captchaImage)} bytes")
|
||||
|
||||
# 加载图片
|
||||
pixmap = QPixmap()
|
||||
load_success = pixmap.loadFromData(captchaImage)
|
||||
|
||||
if load_success:
|
||||
logger.debug(f"成功加载图片,尺寸: {pixmap.width()}x{pixmap.height()}")
|
||||
# 创建圆角图片
|
||||
pixmap = self._createRoundedPixmap(pixmap, radius=10)
|
||||
self.captchaReady.emit(pixmap)
|
||||
else:
|
||||
logger.error("图片加载失败")
|
||||
self.captchaFailed.emit("验证码图片加载失败")
|
||||
self.captchaReady.emit(QPixmap(":app/images/loadFailure.png"))
|
||||
except (IndexError, ValueError, TypeError) as e:
|
||||
logger.error(f"验证码数据格式错误: {e}")
|
||||
self.captchaFailed.emit(f"验证码数据格式错误: {str(e)}")
|
||||
self.captchaReady.emit(QPixmap(":app/images/loadFailure.png"))
|
||||
else:
|
||||
logger.error("验证码响应中缺少有效的data字段")
|
||||
self.captchaFailed.emit("验证码数据无效")
|
||||
self.captchaReady.emit(QPixmap(":app/images/loadFailure.png"))
|
||||
else:
|
||||
error_msg = response.get("msg", "获取验证码失败")
|
||||
logger.error(f"获取验证码失败: {error_msg}")
|
||||
self.captchaFailed.emit(error_msg)
|
||||
self.captchaReady.emit(QPixmap(":app/images/loadFailure.png"))
|
||||
except Exception as e:
|
||||
logger.exception(f"获取验证码过程中发生异常: {e}")
|
||||
self.captchaFailed.emit(str(e))
|
||||
self.captchaReady.emit(QPixmap(":app/images/loadFailure.png"))
|
||||
|
||||
|
||||
class LoginThread(QThread):
|
||||
successLogin = pyqtSignal()
|
||||
errorLogin = pyqtSignal(str)
|
||||
|
||||
def __init__(self, email: str, password: str, captchaCode: str):
|
||||
super().__init__()
|
||||
logger.debug(f"初始化许可证服务线程 - 邮箱: {email}")
|
||||
self.email = email
|
||||
self.password = password
|
||||
self.captchaCode = captchaCode
|
||||
|
||||
def run(self):
|
||||
logger.info(f"开始验证用户登录 - 邮箱: {self.email}")
|
||||
try:
|
||||
loginResponse = miaoStarsBasicApi.login(
|
||||
self.email, self.password, self.captchaCode
|
||||
)
|
||||
if loginResponse["code"] == 0:
|
||||
self.successLogin.emit()
|
||||
qconfig.set(cfg.email, self.email)
|
||||
qconfig.set(cfg.activationCode, self.password)
|
||||
userConfig.userData = loginResponse
|
||||
# 从登录响应中提取token并保存
|
||||
# 在basicApi的login方法中已经处理了token的设置,这里保存到userConfig中以便程序启动时恢复
|
||||
token = miaoStarsBasicApi.token
|
||||
if token:
|
||||
userConfig.setToken(token)
|
||||
else:
|
||||
self.errorLogin.emit(loginResponse["msg"])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"登录验证过程中发生异常: {e}")
|
||||
self.errorLogin.emit("系统错误,请稍后重试")
|
||||
|
||||
|
||||
class RegisterThread(QThread):
|
||||
successRegister = pyqtSignal()
|
||||
errorRegister = pyqtSignal(str)
|
||||
|
||||
def __init__(self, email: str, password: str, captchaCode: str):
|
||||
super().__init__()
|
||||
logger.debug(f"初始化许可证服务线程 - 邮箱: {email}")
|
||||
self.email = email
|
||||
self.password = password
|
||||
self.captchaCode = captchaCode
|
||||
|
||||
def run(self):
|
||||
logger.info(f"开始验证用户注册 - 邮箱: {self.email}")
|
||||
try:
|
||||
registerRespond = miaoStarsBasicApi.register(
|
||||
self.email, self.password, self.captchaCode
|
||||
)
|
||||
if registerRespond["code"] == 203:
|
||||
self.successRegister.emit()
|
||||
else:
|
||||
logger.error(f"注册失败: {registerRespond['msg']}")
|
||||
self.errorRegister.emit(registerRespond["msg"])
|
||||
except Exception as e:
|
||||
logger.error(f"登录验证过程中发生异常: {e}")
|
||||
self.errorRegister.emit("系统错误,请稍后重试")
|
||||
246
app/core/services/preview_thread.py
Normal file
246
app/core/services/preview_thread.py
Normal file
@@ -0,0 +1,246 @@
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from PyQt6.QtCore import QThread, pyqtSignal
|
||||
from PyQt6.QtGui import QImage, QPixmap
|
||||
|
||||
from app.core import miaoStarsBasicApi
|
||||
|
||||
|
||||
class TextLoaderThread(QThread):
|
||||
"""文本文件加载线程"""
|
||||
|
||||
textLoaded = pyqtSignal(str)
|
||||
errorOccurred = pyqtSignal(str)
|
||||
progressUpdated = pyqtSignal(int) # 进度更新信号
|
||||
|
||||
def __init__(self, url):
|
||||
super().__init__()
|
||||
self.url = url
|
||||
|
||||
def run(self):
|
||||
"""线程执行函数"""
|
||||
try:
|
||||
# 1. 设置网络请求参数 - 优化连接参数
|
||||
session = requests.Session()
|
||||
adapter = requests.adapters.HTTPAdapter(
|
||||
pool_connections=20,
|
||||
pool_maxsize=20,
|
||||
max_retries=5, # 增加重试次数
|
||||
pool_block=False,
|
||||
)
|
||||
session.mount("http://", adapter)
|
||||
session.mount("https://", adapter)
|
||||
|
||||
# 2. 增加超时时间并添加重试机制
|
||||
response = miaoStarsBasicApi.returnSession().get(
|
||||
self.url,
|
||||
stream=True,
|
||||
timeout=(15, 30), # 增加超时时间:连接15秒,读取30秒
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
# 3. 获取文件大小(如果服务器支持)
|
||||
total_size = int(response.headers.get("content-length", 0))
|
||||
downloaded_size = 0
|
||||
|
||||
# 4. 分块读取并处理 - 使用二进制读取提高速度
|
||||
content_chunks = []
|
||||
for chunk in response.iter_content(chunk_size=16384): # 增大块大小
|
||||
if chunk:
|
||||
content_chunks.append(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
|
||||
# 更新进度(如果知道总大小)
|
||||
if total_size > 0:
|
||||
progress = int((downloaded_size / total_size) * 100)
|
||||
self.progressUpdated.emit(progress)
|
||||
|
||||
# 5. 合并内容并解码
|
||||
binary_content = b"".join(content_chunks)
|
||||
|
||||
if not binary_content:
|
||||
self.errorOccurred.emit("下载内容为空")
|
||||
return
|
||||
|
||||
# 6. 智能编码检测和解码
|
||||
text_content = self._decode_content(binary_content)
|
||||
|
||||
# 7. 发射加载完成的信号
|
||||
self.textLoaded.emit(text_content)
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
self.errorOccurred.emit("请求超时,请检查网络连接或尝试重新加载")
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.errorOccurred.emit("网络连接错误,请检查网络设置")
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.errorOccurred.emit(f"网络请求错误: {str(e)}")
|
||||
except Exception as e:
|
||||
self.errorOccurred.emit(f"文本处理错误: {str(e)}")
|
||||
|
||||
def _decode_content(self, binary_content):
|
||||
"""智能解码二进制内容"""
|
||||
# 优先尝试UTF-8
|
||||
encodings = ["utf-8", "gbk", "gb2312", "latin-1", "iso-8859-1", "cp1252"]
|
||||
|
||||
for encoding in encodings:
|
||||
try:
|
||||
return binary_content.decode(encoding)
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
# 如果所有编码都失败,使用替换错误处理
|
||||
try:
|
||||
return binary_content.decode("utf-8", errors="replace")
|
||||
except:
|
||||
# 最后尝试忽略错误
|
||||
return binary_content.decode("utf-8", errors="ignore")
|
||||
|
||||
def cancel(self):
|
||||
"""取消下载"""
|
||||
if self.isRunning():
|
||||
self.terminate()
|
||||
self.wait(1000) # 等待线程结束
|
||||
|
||||
|
||||
class ImageLoaderThread(QThread):
|
||||
"""优化的图片加载线程"""
|
||||
|
||||
imageLoaded = pyqtSignal(QPixmap)
|
||||
errorOccurred = pyqtSignal(str)
|
||||
progressUpdated = pyqtSignal(int) # 进度更新信号
|
||||
|
||||
def __init__(
|
||||
self, url, cache_dir="image_cache", max_cache_size=50 * 1024 * 1024
|
||||
): # 50MB缓存
|
||||
super().__init__()
|
||||
self.url = url
|
||||
self.cache_dir = cache_dir
|
||||
self.max_cache_size = max_cache_size
|
||||
self._setup_cache()
|
||||
|
||||
def _setup_cache(self):
|
||||
"""设置图片缓存目录"""
|
||||
if not os.path.exists(self.cache_dir):
|
||||
os.makedirs(self.cache_dir)
|
||||
|
||||
def _get_cache_filename(self):
|
||||
"""生成缓存文件名"""
|
||||
parsed_url = urlparse(self.url)
|
||||
filename = os.path.basename(parsed_url.path) or "image"
|
||||
# 添加URL哈希避免重名
|
||||
import hashlib
|
||||
|
||||
url_hash = hashlib.md5(self.url.encode()).hexdigest()[:8]
|
||||
return f"{url_hash}_{filename}"
|
||||
|
||||
def _get_cached_image(self):
|
||||
"""获取缓存图片"""
|
||||
cache_file = os.path.join(self.cache_dir, self._get_cache_filename())
|
||||
if os.path.exists(cache_file):
|
||||
try:
|
||||
pixmap = QPixmap(cache_file)
|
||||
if not pixmap.isNull():
|
||||
return pixmap
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _save_to_cache(self, pixmap):
|
||||
"""保存图片到缓存"""
|
||||
try:
|
||||
cache_file = os.path.join(self.cache_dir, self._get_cache_filename())
|
||||
pixmap.save(cache_file, "JPG", 80) # 压缩质量80%
|
||||
self._cleanup_cache() # 清理过期缓存
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _cleanup_cache(self):
|
||||
"""清理过期的缓存文件"""
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
files = []
|
||||
for f in os.listdir(self.cache_dir):
|
||||
filepath = os.path.join(self.cache_dir, f)
|
||||
if os.path.isfile(filepath):
|
||||
files.append((filepath, os.path.getmtime(filepath)))
|
||||
|
||||
# 按修改时间排序
|
||||
files.sort(key=lambda x: x[1])
|
||||
|
||||
# 计算总大小
|
||||
total_size = sum(os.path.getsize(f[0]) for f in files)
|
||||
|
||||
# 如果超过最大缓存大小,删除最旧的文件
|
||||
while total_size > self.max_cache_size and files:
|
||||
oldest_file = files.pop(0)
|
||||
total_size -= os.path.getsize(oldest_file[0])
|
||||
os.remove(oldest_file[0])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
"""线程执行函数"""
|
||||
try:
|
||||
# 1. 首先检查缓存
|
||||
cached_pixmap = self._get_cached_image()
|
||||
if cached_pixmap:
|
||||
self.imageLoaded.emit(cached_pixmap)
|
||||
return
|
||||
|
||||
# 2. 设置更短的超时时间
|
||||
session = requests.Session()
|
||||
adapter = requests.adapters.HTTPAdapter(
|
||||
pool_connections=10, pool_maxsize=10, max_retries=5 # 重试2次
|
||||
)
|
||||
session.mount("http://", adapter)
|
||||
session.mount("https://", adapter)
|
||||
|
||||
# 3. 流式下载,支持进度显示
|
||||
response = miaoStarsBasicApi.returnSession().get(
|
||||
self.url, stream=True, timeout=(20, 30) # 连接超时5秒,读取超时10秒
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
# 获取文件大小(如果服务器支持)
|
||||
total_size = int(response.headers.get("content-length", 0))
|
||||
downloaded_size = 0
|
||||
|
||||
# 4. 分块读取并处理
|
||||
image_data = b""
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
image_data += chunk
|
||||
downloaded_size += len(chunk)
|
||||
|
||||
# 更新进度(如果知道总大小)
|
||||
if total_size > 0:
|
||||
progress = int((downloaded_size / total_size) * 100)
|
||||
self.progressUpdated.emit(progress)
|
||||
|
||||
# 5. 从数据创建QImage(比QPixmap更快)
|
||||
image = QImage()
|
||||
image.loadFromData(image_data)
|
||||
|
||||
if image.isNull():
|
||||
raise Exception("无法加载图片数据")
|
||||
|
||||
# 6. 转换为QPixmap
|
||||
pixmap = QPixmap.fromImage(image)
|
||||
|
||||
# 7. 保存到缓存
|
||||
self._save_to_cache(pixmap)
|
||||
|
||||
# 发射加载完成的信号
|
||||
self.imageLoaded.emit(pixmap)
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
self.errorOccurred.emit("请求超时,请检查网络连接")
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.errorOccurred.emit("网络连接错误")
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.errorOccurred.emit(f"网络请求错误: {str(e)}")
|
||||
except Exception as e:
|
||||
self.errorOccurred.emit(f"图片处理错误: {str(e)}")
|
||||
287
app/core/services/text_speech.py
Normal file
287
app/core/services/text_speech.py
Normal file
@@ -0,0 +1,287 @@
|
||||
# coding: utf-8
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from loguru import logger
|
||||
from PyQt6.QtCore import QEventLoop, QObject, QThread, pyqtSignal
|
||||
from PyQt6.QtCore import QUrl
|
||||
from PyQt6.QtMultimedia import QAudioOutput, QMediaPlayer
|
||||
|
||||
|
||||
class LocalTextToSpeechThread(QThread):
|
||||
"""本地文本转语音播放线程 - Windows优化版"""
|
||||
|
||||
# 信号定义
|
||||
playback_started = pyqtSignal() # 播放开始
|
||||
playback_finished = pyqtSignal() # 播放完成
|
||||
playback_error = pyqtSignal(str) # 播放错误
|
||||
progress_updated = pyqtSignal(int) # 播放进度更新
|
||||
synthesis_completed = pyqtSignal(str) # 语音合成完成(返回文件路径)
|
||||
|
||||
def __init__(self, text, parent=None):
|
||||
super().__init__(parent)
|
||||
self.text = text
|
||||
self.audio_file_path = None
|
||||
self.media_player = None
|
||||
self.audio_output = None
|
||||
self._stop_requested = False
|
||||
|
||||
def run(self):
|
||||
"""线程执行函数"""
|
||||
try:
|
||||
# 1. 将文本转换为语音文件
|
||||
self.audio_file_path = self._text_to_speech(self.text)
|
||||
if not self.audio_file_path or self._stop_requested:
|
||||
return
|
||||
|
||||
# 发射合成完成信号
|
||||
self.synthesis_completed.emit(self.audio_file_path)
|
||||
|
||||
# 2. 播放语音
|
||||
self._play_audio(self.audio_file_path)
|
||||
|
||||
except Exception as e:
|
||||
self.playback_error.emit(f"语音播放错误: {str(e)}")
|
||||
|
||||
def _text_to_speech(self, text):
|
||||
"""使用本地TTS引擎将文本转换为语音文件"""
|
||||
try:
|
||||
# 检查文本长度
|
||||
if not text or len(text.strip()) == 0:
|
||||
self.playback_error.emit("文本内容为空")
|
||||
return None
|
||||
|
||||
# 限制文本长度,避免合成时间过长
|
||||
max_length = 1000
|
||||
if len(text) > max_length:
|
||||
text = text[:max_length] + "。文本过长,已截断。"
|
||||
self.playback_error.emit(f"文本过长,已截断前{max_length}个字符")
|
||||
|
||||
# 优先使用pyttsx3,效率最高
|
||||
try:
|
||||
import pyttsx3
|
||||
return self._pyttsx3_tts(text)
|
||||
except ImportError:
|
||||
# 备用方案:使用Windows内置TTS
|
||||
return self._windows_tts(text)
|
||||
|
||||
except Exception as e:
|
||||
self.playback_error.emit(f"语音合成失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def _pyttsx3_tts(self, text):
|
||||
"""使用pyttsx3合成语音 - 优化版"""
|
||||
try:
|
||||
import pyttsx3
|
||||
|
||||
# 初始化TTS引擎
|
||||
engine = pyttsx3.init()
|
||||
|
||||
# 设置语音属性 - 提高效率
|
||||
engine.setProperty('rate', 200) # 提高语速
|
||||
engine.setProperty('volume', 0.9) # 提高音量
|
||||
|
||||
# 创建临时文件保存音频
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as temp_file:
|
||||
temp_path = temp_file.name
|
||||
|
||||
# 保存语音到文件
|
||||
engine.save_to_file(text, temp_path)
|
||||
engine.runAndWait()
|
||||
|
||||
# 检查文件是否成功创建
|
||||
if os.path.exists(temp_path) and os.path.getsize(temp_path) > 0:
|
||||
return temp_path
|
||||
else:
|
||||
logger.error("语音文件生成失败")
|
||||
|
||||
except Exception as e:
|
||||
# 如果pyttsx3失败,尝试Windows TTS
|
||||
return self._windows_tts(text)
|
||||
|
||||
def _windows_tts(self, text):
|
||||
"""Windows系统TTS - 优化版"""
|
||||
try:
|
||||
# 方法1: 使用PowerShell命令 - 最可靠
|
||||
return self._powershell_tts(text)
|
||||
except Exception as e:
|
||||
logger.error(f"Windows TTS失败: {str(e)}")
|
||||
|
||||
def _powershell_tts(self, text):
|
||||
"""使用PowerShell合成语音 - 优化版"""
|
||||
try:
|
||||
# 创建临时文件
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as temp_file:
|
||||
temp_path = temp_file.name
|
||||
|
||||
# 转义文本中的特殊字符
|
||||
escaped_text = text.replace('"', '`"').replace("'", "`'")
|
||||
|
||||
# 使用PowerShell的SpeechSynthesizer - 简化命令
|
||||
ps_script = f"""
|
||||
Add-Type -AssemblyName System.Speech
|
||||
$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer
|
||||
$speak.SetOutputToWaveFile("{temp_path}")
|
||||
$speak.Speak("{escaped_text}")
|
||||
$speak.Dispose()
|
||||
"""
|
||||
|
||||
# 使用更高效的方式执行PowerShell
|
||||
process = subprocess.Popen(
|
||||
["powershell", "-Command", ps_script],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
shell=True
|
||||
)
|
||||
|
||||
# 等待进程完成,设置超时
|
||||
try:
|
||||
stdout, stderr = process.communicate(timeout=30)
|
||||
if process.returncode != 0:
|
||||
logger.error(f"PowerShell执行失败: {stderr.decode('gbk', errors='ignore')}")
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
logger.error(f"PowerShell超时")
|
||||
|
||||
if os.path.exists(temp_path) and os.path.getsize(temp_path) > 0:
|
||||
return temp_path
|
||||
else:
|
||||
logger.error("语音文件生成失败")
|
||||
|
||||
except Exception as e:
|
||||
# raise Exception(f"PowerShell TTS失败: {str(e)}")
|
||||
logger.error(f"PowerShell TTS失败{e}")
|
||||
|
||||
def _play_audio(self, file_path):
|
||||
"""播放音频文件 - 优化版"""
|
||||
if self._stop_requested:
|
||||
return
|
||||
|
||||
try:
|
||||
# 创建媒体播放器和音频输出
|
||||
self.media_player = QMediaPlayer()
|
||||
self.audio_output = QAudioOutput()
|
||||
self.media_player.setAudioOutput(self.audio_output)
|
||||
|
||||
# 设置音量
|
||||
self.audio_output.setVolume(1.0)
|
||||
|
||||
# 连接信号
|
||||
self.media_player.playbackStateChanged.connect(self._on_playback_state_changed)
|
||||
self.media_player.positionChanged.connect(self._on_position_changed)
|
||||
self.media_player.durationChanged.connect(self._on_duration_changed)
|
||||
self.media_player.errorOccurred.connect(self._on_player_error)
|
||||
|
||||
# 设置媒体源并开始播放
|
||||
self.media_player.setSource(QUrl.fromLocalFile(file_path))
|
||||
self.media_player.play()
|
||||
|
||||
# 使用事件循环等待播放完成
|
||||
loop = QEventLoop()
|
||||
self.media_player.playbackStateChanged.connect(
|
||||
lambda state: loop.quit() if state == QMediaPlayer.PlaybackState.StoppedState else None
|
||||
)
|
||||
loop.exec()
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"音频播放失败: {str(e)}")
|
||||
finally:
|
||||
# 清理临时文件
|
||||
if file_path and os.path.exists(file_path):
|
||||
try:
|
||||
os.unlink(file_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _on_playback_state_changed(self, state):
|
||||
"""处理播放状态变化"""
|
||||
from PyQt6.QtMultimedia import QMediaPlayer
|
||||
if state == QMediaPlayer.PlaybackState.StoppedState:
|
||||
self.playback_finished.emit()
|
||||
|
||||
def _on_position_changed(self, position):
|
||||
"""处理播放位置变化"""
|
||||
if (self.media_player and
|
||||
self.media_player.duration() > 0):
|
||||
progress = int((position / self.media_player.duration()) * 100)
|
||||
self.progress_updated.emit(progress)
|
||||
|
||||
def _on_duration_changed(self, duration):
|
||||
"""处理时长变化"""
|
||||
if duration > 0:
|
||||
self.playback_started.emit()
|
||||
|
||||
def _on_player_error(self, error, error_string):
|
||||
"""处理播放器错误"""
|
||||
self.playback_error.emit(f"播放器错误: {error_string}")
|
||||
|
||||
def stop_playback(self):
|
||||
"""停止播放"""
|
||||
self._stop_requested = True
|
||||
|
||||
if self.media_player and self.media_player.playbackState() != QMediaPlayer.PlaybackState.StoppedState:
|
||||
self.media_player.stop()
|
||||
|
||||
# 清理临时文件
|
||||
if self.audio_file_path and os.path.exists(self.audio_file_path):
|
||||
try:
|
||||
os.unlink(self.audio_file_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class LocalSpeechController(QObject):
|
||||
"""本地语音播放控制器"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.speech_thread = None
|
||||
|
||||
def play_text(self, text):
|
||||
"""播放文本语音"""
|
||||
# 停止当前播放
|
||||
self.stop_playback()
|
||||
|
||||
# 创建新的语音线程
|
||||
self.speech_thread = LocalTextToSpeechThread(text)
|
||||
|
||||
# 连接信号
|
||||
self.speech_thread.playback_started.connect(self._on_playback_started)
|
||||
self.speech_thread.playback_finished.connect(self._on_playback_finished)
|
||||
self.speech_thread.playback_error.connect(self._on_playback_error)
|
||||
self.speech_thread.progress_updated.connect(self._on_progress_updated)
|
||||
self.speech_thread.synthesis_completed.connect(self._on_synthesis_completed)
|
||||
|
||||
# 开始播放
|
||||
self.speech_thread.start()
|
||||
|
||||
def stop_playback(self):
|
||||
"""停止播放"""
|
||||
if self.speech_thread and self.speech_thread.isRunning():
|
||||
self.speech_thread.stop_playback()
|
||||
self.speech_thread.wait(1000) # 等待线程结束,最多1秒
|
||||
|
||||
def is_playing(self):
|
||||
"""检查是否正在播放"""
|
||||
return self.speech_thread and self.speech_thread.isRunning()
|
||||
|
||||
def _on_playback_started(self):
|
||||
"""处理播放开始"""
|
||||
logger.info("语音播放开始")
|
||||
|
||||
def _on_playback_finished(self):
|
||||
"""处理播放完成"""
|
||||
logger.success("语音播放完成")
|
||||
|
||||
def _on_playback_error(self, error_msg):
|
||||
"""处理播放错误"""
|
||||
logger.warning(f"语音播放错误: {error_msg}")
|
||||
|
||||
def _on_progress_updated(self, progress):
|
||||
...
|
||||
|
||||
def _on_synthesis_completed(self, file_path):
|
||||
"""处理语音合成完成"""
|
||||
logger.info(f"语音合成完成,文件路径: {file_path}")
|
||||
199
app/core/services/user_thread.py
Normal file
199
app/core/services/user_thread.py
Normal file
@@ -0,0 +1,199 @@
|
||||
from loguru import logger
|
||||
from PyQt6.QtCore import QThread, pyqtSignal
|
||||
from PyQt6.QtGui import QPixmap
|
||||
|
||||
from ..api import miaoStarsBasicApi
|
||||
|
||||
|
||||
class UserNickNameUpdateThread(QThread):
|
||||
successUpdate = pyqtSignal()
|
||||
errorUpdate = pyqtSignal(str)
|
||||
|
||||
def __init__(self, nickName: str):
|
||||
super().__init__()
|
||||
logger.debug(f"初始化用户昵称服务线程 - 昵称: {nickName}")
|
||||
self.nickName = nickName
|
||||
|
||||
def run(self):
|
||||
logger.info(f"开始更新用户昵称 - 昵称: {self.nickName}")
|
||||
try:
|
||||
response = miaoStarsBasicApi.updateUserNickname(self.nickName)
|
||||
print(response)
|
||||
if response["code"] == 0:
|
||||
self.successUpdate.emit()
|
||||
else:
|
||||
logger.error("更新失败:", response["msg"])
|
||||
self.errorUpdate.emit(response["msg"])
|
||||
except Exception as e:
|
||||
logger.error(f"更新用户昵称过程中发生异常: {e}")
|
||||
self.errorUpdate.emit("系统错误,请稍后重试")
|
||||
|
||||
|
||||
class UserAvatarUpdateThread(QThread):
|
||||
successUpdate = pyqtSignal()
|
||||
errorUpdate = pyqtSignal(str)
|
||||
|
||||
def __init__(self, avatarPath: str):
|
||||
super().__init__()
|
||||
logger.debug(f"初始化用户头像服务线程 - 头像路径: {avatarPath}")
|
||||
self.avatarPath = avatarPath
|
||||
|
||||
def run(self):
|
||||
logger.info(f"开始更新用户头像 - 头像路径: {self.avatarPath}")
|
||||
try:
|
||||
response = miaoStarsBasicApi.updateUserAvatar(self.avatarPath)
|
||||
if response["code"] == 0:
|
||||
logger.info("头像更新成功")
|
||||
self.successUpdate.emit()
|
||||
else:
|
||||
logger.error(f"更新失败,错误信息: {response['msg']}")
|
||||
self.errorUpdate.emit(f"更新失败: {response['msg']}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新用户头像过程中发生异常: {e}")
|
||||
self.errorUpdate.emit("系统错误,请稍后重试")
|
||||
|
||||
|
||||
class GetUserAvatarThread(QThread):
|
||||
avatarPixmap = pyqtSignal(QPixmap)
|
||||
|
||||
def __init__(self, size: str):
|
||||
super().__init__()
|
||||
logger.debug(f"初始化获取用户头像服务线程 - 头像尺寸: {size}")
|
||||
self.size = size
|
||||
|
||||
def run(self):
|
||||
logger.info(f"开始获取用户头像 - 头像尺寸: {self.size}")
|
||||
try:
|
||||
response = miaoStarsBasicApi.getUserAvatar(self.size)
|
||||
self.avatarPixmap.emit(response)
|
||||
except Exception as e:
|
||||
logger.error(f"获取用户头像过程中发生异常: {e}")
|
||||
self.avatarPixmap.emit(QPixmap(":app/images/logo.png"))
|
||||
|
||||
|
||||
class GetPackThread(QThread):
|
||||
storageDictSignal = pyqtSignal(dict)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def run(self):
|
||||
logger.info("开始请求用户配额包")
|
||||
try:
|
||||
response = miaoStarsBasicApi.getUserPack()
|
||||
self.storageDictSignal.emit(response)
|
||||
except Exception as e:
|
||||
logger.error(f"获取用户配额包过程中发生异常: {e}")
|
||||
self.storageDictSignal.emit(
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"base": 0,
|
||||
"pack": 0,
|
||||
"used": 0,
|
||||
"total": 0,
|
||||
"packs": [],
|
||||
},
|
||||
"msg": "",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class GetPoliciesThread(QThread):
|
||||
"""获取策略列表线程"""
|
||||
|
||||
successGetSignal = pyqtSignal(list)
|
||||
errorSignal = pyqtSignal(str)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
response = miaoStarsBasicApi.getPolicy()
|
||||
if response["code"] == 0:
|
||||
self.successGetSignal.emit(response["data"])
|
||||
else:
|
||||
self.errorSignal.emit(f"API返回错误: {response.get('msg')}")
|
||||
except Exception as e:
|
||||
self.errorSignal.emit(f"获取策略列表失败: {str(e)}")
|
||||
|
||||
|
||||
class ChangePolicyThread(QThread):
|
||||
"""更改策略线程"""
|
||||
|
||||
successChangedSignal = pyqtSignal()
|
||||
errorSignal = pyqtSignal(str)
|
||||
|
||||
def __init__(self, path, policy_id):
|
||||
super().__init__()
|
||||
self.path = path
|
||||
self.policy_id = policy_id
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
response = miaoStarsBasicApi.changePolicy(self.path, self.policy_id)
|
||||
if response["code"] == 0:
|
||||
self.successChangedSignal.emit()
|
||||
else:
|
||||
self.errorSignal.emit(
|
||||
f"更改策略失败: {response.get('msg', '未知错误')}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.errorSignal.emit(f"更改策略请求失败: {str(e)}")
|
||||
|
||||
|
||||
class DeleteTagThread(QThread):
|
||||
"""删除标签线程"""
|
||||
|
||||
successDeleteSignal = pyqtSignal()
|
||||
errorSignal = pyqtSignal(str)
|
||||
|
||||
def __init__(self, tagId):
|
||||
super().__init__()
|
||||
self.tagId = tagId
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
response = miaoStarsBasicApi.deleteTag(self.tagId)
|
||||
if response["code"] == 0:
|
||||
self.successDeleteSignal.emit()
|
||||
logger.info(f"删除标签成功: {self.tagId}")
|
||||
else:
|
||||
logger.error(f"删除标签失败: {response.get('msg')}")
|
||||
self.errorSignal.emit(f"删除标签失败: {response.get('msg')}")
|
||||
|
||||
except Exception as e:
|
||||
self.errorSignal.emit(f"{str(e)}")
|
||||
logger.error(f"删除标签请求失败: {str(e)}")
|
||||
|
||||
|
||||
class AddTagThread(QThread):
|
||||
"""添加标签的线程类"""
|
||||
|
||||
successSignal = pyqtSignal(str, dict) # 标签名称, 响应数据
|
||||
errorSignal = pyqtSignal(str, str) # 标签名称, 错误信息
|
||||
|
||||
def __init__(self, name, expression, parent=None):
|
||||
super().__init__(parent)
|
||||
self.name = name
|
||||
self.expression = expression
|
||||
|
||||
def run(self):
|
||||
"""线程执行的主方法"""
|
||||
try:
|
||||
|
||||
response = miaoStarsBasicApi.addTag(self.name, self.expression)
|
||||
|
||||
if response["code"] == 0:
|
||||
logger.info(f"添加标签成功: {self.name}")
|
||||
self.successSignal.emit(self.name, response)
|
||||
else:
|
||||
logger.error(f"添加标签失败: {self.name} - {response.get('msg')}")
|
||||
self.errorSignal.emit(self.name, response.get("msg"))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"添加标签异常: {self.name} - {str(e)}")
|
||||
self.errorSignal.emit(self.name, str(e))
|
||||
Reference in New Issue
Block a user