2025-10-29 22:20:21 +08:00
|
|
|
|
import os
|
2025-11-01 20:14:35 +08:00
|
|
|
|
import logging
|
2025-10-29 22:20:21 +08:00
|
|
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
|
from PyQt6.QtCore import QThread, pyqtSignal
|
|
|
|
|
|
from PyQt6.QtGui import QImage, QPixmap
|
|
|
|
|
|
|
2025-11-01 20:14:35 +08:00
|
|
|
|
from app.core.api import basicApi, miaoStarsBasicApi
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TextLoaderThread(QThread):
|
|
|
|
|
|
"""文本文件加载线程"""
|
|
|
|
|
|
|
|
|
|
|
|
textLoaded = pyqtSignal(str)
|
|
|
|
|
|
errorOccurred = pyqtSignal(str)
|
|
|
|
|
|
progressUpdated = pyqtSignal(int) # 进度更新信号
|
|
|
|
|
|
|
2025-11-01 20:14:35 +08:00
|
|
|
|
def __init__(self, url, fileId=None):
|
2025-10-29 22:20:21 +08:00
|
|
|
|
super().__init__()
|
2025-11-01 20:14:35 +08:00
|
|
|
|
self.url = url # 原始URL,可能作为fallback
|
|
|
|
|
|
self.fileId = fileId # 文件ID,优先使用
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
|
"""线程执行函数"""
|
|
|
|
|
|
try:
|
2025-11-01 20:14:35 +08:00
|
|
|
|
binary_content = None
|
|
|
|
|
|
content_chunks = [] # 预先初始化content_chunks,避免在任何执行路径下出现未定义错误
|
|
|
|
|
|
|
|
|
|
|
|
# 如果提供了fileId(实际上是URI),优先使用getFileContent直接获取内容
|
|
|
|
|
|
if self.fileId:
|
|
|
|
|
|
self.progressUpdated.emit(0) # 发送初始进度
|
|
|
|
|
|
logger.info(f"使用文件URI {self.fileId} 直接获取文件内容")
|
|
|
|
|
|
response = basicApi.getFileContent(self.fileId)
|
|
|
|
|
|
|
|
|
|
|
|
if response.get('code') == 0 and 'data' in response:
|
|
|
|
|
|
# 检查response['data']的类型,确保只有二进制数据才直接使用
|
|
|
|
|
|
if isinstance(response['data'], bytes):
|
|
|
|
|
|
binary_content = response['data']
|
|
|
|
|
|
logger.info(f"成功获取文件内容,大小: {len(binary_content)} 字节")
|
|
|
|
|
|
self.progressUpdated.emit(100) # 直接发送完成进度
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 如果不是二进制数据(可能是字典等其他类型),记录警告并使用URL作为fallback
|
|
|
|
|
|
logger.warning(f"获取到的文件内容不是二进制数据,类型: {type(response['data']).__name__},将使用URL作为fallback")
|
|
|
|
|
|
binary_content = None
|
|
|
|
|
|
else:
|
|
|
|
|
|
error_msg = response.get('msg', '获取文件内容失败')
|
|
|
|
|
|
logger.warning(f"获取文件内容失败: {error_msg},将使用原始URL作为fallback")
|
|
|
|
|
|
|
|
|
|
|
|
# 如果直接获取内容失败,尝试使用URL
|
|
|
|
|
|
if binary_content is None:
|
|
|
|
|
|
if not self.url:
|
|
|
|
|
|
self.errorOccurred.emit("没有可用的URL来加载文本内容")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 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 = basicApi.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)
|
|
|
|
|
|
|
|
|
|
|
|
# 确保binary_content是bytes类型且不为空
|
|
|
|
|
|
if not binary_content or not isinstance(binary_content, bytes):
|
|
|
|
|
|
if not binary_content:
|
|
|
|
|
|
self.errorOccurred.emit("下载内容为空")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.errorOccurred.emit(f"下载内容类型错误,应为bytes,实际为: {type(binary_content).__name__}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
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__(
|
2025-11-01 20:14:35 +08:00
|
|
|
|
self, url, cache_dir="image_cache", max_cache_size=50 * 1024 * 1024, fileId=None
|
2025-10-29 22:20:21 +08:00
|
|
|
|
): # 50MB缓存
|
|
|
|
|
|
super().__init__()
|
2025-11-01 20:14:35 +08:00
|
|
|
|
self.url = url # 原始URL,可能作为fallback
|
|
|
|
|
|
self.fileId = fileId # 文件ID,优先使用
|
2025-10-29 22:20:21 +08:00
|
|
|
|
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:
|
2025-11-01 20:14:35 +08:00
|
|
|
|
image_data = None
|
|
|
|
|
|
|
|
|
|
|
|
# 如果提供了fileId(实际上是URI),优先使用getFileContent直接获取内容
|
|
|
|
|
|
if self.fileId:
|
|
|
|
|
|
logger.info(f"使用文件URI {self.fileId} 直接获取图片内容")
|
|
|
|
|
|
response = miaoStarsBasicApi.getFileContent(self.fileId)
|
|
|
|
|
|
|
|
|
|
|
|
if response.get('code') == 0 and 'data' in response:
|
|
|
|
|
|
# 检查response['data']的类型,确保只有二进制数据才直接使用
|
|
|
|
|
|
if isinstance(response['data'], bytes):
|
|
|
|
|
|
image_data = response['data']
|
|
|
|
|
|
logger.info(f"成功获取图片内容,大小: {len(image_data)} 字节")
|
|
|
|
|
|
self.progressUpdated.emit(100) # 直接发送完成进度
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 如果不是二进制数据(可能是字典等其他类型),记录警告并使用URL作为fallback
|
|
|
|
|
|
logger.warning(f"获取到的图片内容不是二进制数据,类型: {type(response['data']).__name__},将使用URL作为fallback")
|
|
|
|
|
|
else:
|
|
|
|
|
|
error_msg = response.get('msg', '获取图片内容失败')
|
|
|
|
|
|
logger.warning(f"获取图片内容失败: {error_msg},将使用原始URL作为fallback")
|
|
|
|
|
|
|
|
|
|
|
|
# 如果直接获取内容失败,尝试使用URL
|
|
|
|
|
|
if image_data is None:
|
|
|
|
|
|
if not self.url:
|
|
|
|
|
|
self.errorOccurred.emit("没有可用的URL来加载图片内容")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 确保URL是字符串类型
|
|
|
|
|
|
if isinstance(self.url, dict):
|
|
|
|
|
|
# 处理字典类型的URL,通常包含urls数组
|
|
|
|
|
|
if 'urls' in self.url and isinstance(self.url['urls'], list) and len(self.url['urls']) > 0:
|
|
|
|
|
|
# 获取第一个URL信息
|
|
|
|
|
|
url_info = self.url['urls'][0]
|
|
|
|
|
|
if isinstance(url_info, dict) and 'url' in url_info:
|
|
|
|
|
|
download_url = url_info['url']
|
|
|
|
|
|
logger.info(f"从字典中提取URL: {download_url}")
|
|
|
|
|
|
self.url = download_url
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.error("URL字典格式不正确,缺少有效的URL信息")
|
|
|
|
|
|
self.errorOccurred.emit("URL格式错误: 缺少有效的URL信息")
|
|
|
|
|
|
return
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.error(f"URL字典格式不正确,缺少urls数组或数组为空: {self.url}")
|
|
|
|
|
|
self.errorOccurred.emit("URL格式错误: 缺少有效的URL信息")
|
|
|
|
|
|
return
|
|
|
|
|
|
elif not isinstance(self.url, str):
|
|
|
|
|
|
logger.error(f"URL类型错误,应为字符串或字典,实际为: {type(self.url).__name__}")
|
|
|
|
|
|
self.errorOccurred.emit(f"URL类型错误: 应为字符串")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 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:
|
|
|
|
|
|
# 确保进度不超过100%
|
|
|
|
|
|
progress = min(int((downloaded_size / total_size) * 100), 100)
|
|
|
|
|
|
self.progressUpdated.emit(progress)
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
|
|
|
|
|
# 5. 从数据创建QImage(比QPixmap更快)
|
|
|
|
|
|
image = QImage()
|
2025-11-01 20:14:35 +08:00
|
|
|
|
|
|
|
|
|
|
# 检查数据是否为空或类型是否正确
|
|
|
|
|
|
if not image_data:
|
|
|
|
|
|
raise Exception("图片数据为空")
|
|
|
|
|
|
|
|
|
|
|
|
if not isinstance(image_data, bytes):
|
|
|
|
|
|
logger.error(f"图片数据类型错误,应为bytes,实际为: {type(image_data).__name__}")
|
|
|
|
|
|
raise Exception(f"图片数据类型错误: 应为bytes,实际为{type(image_data).__name__}")
|
|
|
|
|
|
|
|
|
|
|
|
# 尝试加载图片数据
|
|
|
|
|
|
load_success = image.loadFromData(image_data)
|
|
|
|
|
|
|
|
|
|
|
|
if not load_success or image.isNull():
|
|
|
|
|
|
raise Exception("无法加载图片数据或图片格式不支持")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
|
|
|
|
|
# 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)}")
|