Files
leonpan-pc/app/core/services/preview_thread.py
2025-11-01 20:14:35 +08:00

345 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import logging
from urllib.parse import urlparse
import requests
from PyQt6.QtCore import QThread, pyqtSignal
from PyQt6.QtGui import QImage, QPixmap
from app.core.api import basicApi, miaoStarsBasicApi
logger = logging.getLogger(__name__)
class TextLoaderThread(QThread):
"""文本文件加载线程"""
textLoaded = pyqtSignal(str)
errorOccurred = pyqtSignal(str)
progressUpdated = pyqtSignal(int) # 进度更新信号
def __init__(self, url, fileId=None):
super().__init__()
self.url = url # 原始URL可能作为fallback
self.fileId = fileId # 文件ID优先使用
def run(self):
"""线程执行函数"""
try:
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__}")
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, fileId=None
): # 50MB缓存
super().__init__()
self.url = url # 原始URL可能作为fallback
self.fileId = fileId # 文件ID优先使用
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:
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)
# 5. 从数据创建QImage比QPixmap更快
image = QImage()
# 检查数据是否为空或类型是否正确
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("无法加载图片数据或图片格式不支持")
# 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)}")