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)}")