fix lot of thing

This commit is contained in:
2025-11-01 20:14:35 +08:00
parent 39dfb62cbf
commit f006729311
16 changed files with 23303 additions and 3996 deletions

View File

@@ -140,22 +140,22 @@ class DeleteFileThread(QThread):
successDelete = pyqtSignal()
errorDelete = pyqtSignal(str)
def __init__(self, fileId: str, fileType: str):
def __init__(self, fileUri: str, fileType: str):
super().__init__()
logger.debug(f"初始化删除文件线程 - ID: {fileId}, 类型: {fileType}")
self.fileId = fileId
logger.debug(f"初始化删除文件线程 - URI: {fileUri}, 类型: {fileType}")
self.fileUri = fileUri
self.fileType = fileType
def run(self):
logger.info(f"开始删除文件 - ID: {self.fileId}, 类型: {self.fileType}")
logger.info(f"开始删除文件 - URI: {self.fileUri}, 类型: {self.fileType}")
try:
response = miaoStarsBasicApi.deleteFile(self.fileId, self.fileType)
response = miaoStarsBasicApi.deleteFile(self.fileUri, self.fileType)
if response["code"] == 0:
logger.info(f"文件删除成功 - ID: {self.fileId}")
logger.info(f"文件删除成功 - URI: {self.fileUri}")
self.successDelete.emit()
else:
logger.error(
f"文件删除失败 - ID: {self.fileId}, 错误: {response.get('msg', '未知错误')}"
f"文件删除失败 - URI: {self.fileUri}, 错误: {response.get('msg', '未知错误')}"
)
self.errorDelete.emit(f"删除失败: {response.get('msg', '未知错误')}")
@@ -237,7 +237,11 @@ class UploadThread(QThread):
self.applicationUrl = "/file/upload"
self.current_path = policyConfig.returnCurrentPath()
self.policy = policyConfig.returnPolicy().get("id")
# 获取存储策略如果为None则使用默认值PqI8
self.policy = policyConfig.returnPolicy().get("id", "PqI8")
if self.policy is None:
self.policy = "PqI8"
logger.info("存储策略ID为None已设置默认值PqI8")
self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
@@ -262,54 +266,99 @@ class UploadThread(QThread):
return mime_type if mime_type else "application/octet-stream"
def _prepareUploadData(self) -> Dict[str, Any]:
"""准备上传数据"""
"""准备上传数据符合Cloudreve V4 API规范"""
try:
modification_time = os.path.getmtime(self.file_path)
size = os.path.getsize(self.file_path)
# 构建符合API要求的URI格式: cloudreve://my{path}/{name}
# 处理路径,确保不会有重复斜杠和前缀
# 清理路径确保不包含cloudreve://my前缀
clean_current_path = self.current_path.replace("cloudreve://my", "")
# 处理路径,确保不会有重复斜杠
path_part = clean_current_path if clean_current_path.startswith('/') else f'/{clean_current_path}'
uri = f"cloudreve://my{path_part}/{self.file_name}"
# 确保路径格式正确,移除重复的前缀
uri = uri.replace("cloudreve://my/cloudreve://my", "cloudreve://my")
# 更健壮地处理重复文件名的情况
# 分割路径并去重
path_parts = uri.split('/')
if len(path_parts) > 1:
# 检查最后一个部分是否是文件名
if path_parts[-1] == self.file_name:
# 检查倒数第二个部分是否也是文件名
if len(path_parts) > 2 and path_parts[-2] == self.file_name:
# 移除重复的文件名部分
path_parts.pop(-2)
uri = '/'.join(path_parts)
logger.info(f"构建上传URI: {uri}")
return {
"path": self.current_path,
"uri": uri,
"size": size,
"name": self.file_name,
"policy_id": self.policy,
"last_modified": int(modification_time * 1000), # 转换为毫秒
"mime_type": self.getMimeType(self.file_path),
}
except (OSError, ValueError) as e:
logger.error(f"准备请求失败: {e}")
raise
def _uploadWithProgress(self, upload_url: str, credential: str, total_size: int):
"""带进度显示的上传方法"""
try:
logger.info("_uploadWithProgress方法开始执行")
logger.info(f"参数检查 - URL: {upload_url}, Credential是否存在: {credential is not None}")
# 处理credential可能为None的情况
if credential is None:
logger.warning("Credential为None尝试不使用Authorization头进行上传")
# 对于本地存储策略可能不需要credential
auth_header = {}
else:
auth_header = {"Authorization": credential}
# 打开文件
logger.info(f"开始打开文件: {self.file_path}")
self._file_obj = open(self.file_path, "rb")
logger.info("文件打开成功")
# 准备上传头信息
upload_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"Accept": "*/*",
"Authorization": credential,
"Accept-Language": "zh-CN,zh;q=0.9",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Content-Type": self.getMimeType(self.file_path), # 使用正确的MIME类型
"Content-Length": str(total_size), # 添加必需的Content-Length头
# 删除硬编码的Origin和Referer使用miaoStarsBasicApi中已配置的请求头
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "cross-site",
**auth_header, # 只在credential不为None时添加Authorization头
}
logger.info(f"上传头信息准备完成: {upload_headers}")
# 发送预检请求
options_result = miaoStarsBasicApi.returnSession().options(
upload_url, headers=upload_headers
)
options_result.raise_for_status()
logger.info("OPTIONS预检请求完成")
# 尝试发送预检请求,但不阻塞主流程
try:
logger.info("尝试发送OPTIONS预检请求")
options_result = miaoStarsBasicApi.returnSession().options(
upload_url, headers=upload_headers, timeout=10
)
options_result.raise_for_status()
logger.info("OPTIONS预检请求完成")
except Exception as e:
logger.warning(f"OPTIONS预检请求失败但继续上传: {str(e)}")
# 创建自定义请求体以支持进度监控和取消
class ProgressFileObject:
def __init__(
self, file_obj, total_size, progress_callback, cancel_check
):
logger.info("创建ProgressFileObject对象")
self.file_obj = file_obj
self.total_size = total_size
self.progress_callback = progress_callback
@@ -319,7 +368,8 @@ class UploadThread(QThread):
def read(self, size=-1):
# 检查是否取消
if self.cancel_check():
return
logger.info("检测到取消上传请求")
raise InterruptedError("Upload cancelled by user")
data = self.file_obj.read(size)
if data:
@@ -330,12 +380,14 @@ class UploadThread(QThread):
else 0
)
self.progress_callback(progress, self.uploaded, self.total_size)
logger.debug(f"上传进度: {progress:.2f}% ({self.uploaded}/{self.total_size} 字节)")
return data
def __len__(self):
return self.total_size
# 创建带进度监控的文件对象
logger.info("创建进度监控文件对象")
progress_file = ProgressFileObject(
self._file_obj,
total_size,
@@ -347,17 +399,60 @@ class UploadThread(QThread):
# 执行实际上传
logger.info(f"开始上传文件,总大小: {total_size} 字节")
upload_result = miaoStarsBasicApi.returnSession().post(
upload_url,
data=progress_file,
headers=upload_headers,
timeout=60,
)
logger.info(f"上传完成,响应状态: {upload_result.status_code}")
upload_result.raise_for_status()
return upload_result
logger.info(f"上传URL: {upload_url}")
logger.info(f"上传头信息(已脱敏): {{\n 'User-Agent': '...', \n 'Content-Type': '{upload_headers.get("Content-Type")}',\n 'Content-Length': '{upload_headers.get("Content-Length")}',\n 'Authorization': '***' if 'Authorization' in upload_headers else 'None'\n }}")
# 修复优化上传逻辑先尝试PUT方法Cloudreve V4本地存储通常使用PUT
try:
# 首先尝试使用PUT方法
logger.info("尝试使用PUT方法上传")
upload_result = miaoStarsBasicApi.returnSession().put(
upload_url,
data=progress_file,
headers=upload_headers,
timeout=120,
stream=True
)
logger.info(f"PUT方法上传完成响应状态: {upload_result.status_code}")
upload_result.raise_for_status()
return upload_result
except InterruptedError:
# 用户取消上传
raise
except Exception as e:
logger.error(f"PUT方法上传失败: {str(e)}")
# 尝试使用POST方法
logger.info("尝试使用POST方法重新上传...")
# 重新打开文件
if self._file_obj:
self._file_obj.close()
self._file_obj = open(self.file_path, "rb")
# 重新创建进度文件对象
progress_file = ProgressFileObject(
self._file_obj,
total_size,
lambda progress, uploaded, total: self.uploadProgress.emit(
progress, uploaded, total
),
lambda: self._is_cancelled,
)
# 使用POST方法上传
upload_result = miaoStarsBasicApi.returnSession().post(
upload_url,
data=progress_file,
headers=upload_headers,
timeout=120,
stream=True,
)
logger.info(f"POST方法上传完成响应状态: {upload_result.status_code}")
upload_result.raise_for_status()
return upload_result
except Exception as e:
logger.error(f"上传过程中发生错误: {e}")
@@ -379,6 +474,24 @@ class UploadThread(QThread):
# 准备上传数据
upload_data = self._prepareUploadData()
# 检查policy_id是否为None如果是则尝试获取默认策略
if upload_data.get('policy_id') is None:
logger.warning("存储策略ID为None尝试获取默认存储策略")
try:
# 获取用户的存储策略列表
policies_response = miaoStarsBasicApi.getPolicy()
if policies_response.get('code') == 0 and policies_response.get('data'):
# 使用第一个可用的策略
default_policy = policies_response['data'][0]
upload_data['policy_id'] = default_policy.get('id')
logger.info(f"已设置默认存储策略: {default_policy.get('name')} (ID: {upload_data['policy_id']})")
else:
logger.warning("无法获取存储策略列表使用硬编码默认策略PqI8")
upload_data['policy_id'] = 'PqI8'
except Exception as e:
logger.error(f"获取存储策略失败: {str(e)}使用硬编码默认策略PqI8")
upload_data['policy_id'] = 'PqI8'
# 检查是否取消
if self._is_cancelled:
@@ -387,41 +500,126 @@ class UploadThread(QThread):
# 执行上传请求获取上传URL
logger.info("请求上传URL")
# 从miaoStarsBasicApi获取完整URL(去掉/api/v4后缀
base_url = miaoStarsBasicApi.basicApi.rstrip('/api/v4') # 使用API类中定义的基础URL
full_url = f"{base_url}{self.applicationUrl}"
# 直接使用basicApi构建完整URL,保留/api/v4后缀
# Cloudreve V4 API要求上传接口路径为/api/v4/file/upload
full_url = f"{miaoStarsBasicApi.basicApi}{self.applicationUrl}"
logger.info(f"构建的上传请求URL: {full_url}")
logger.info(f"上传请求数据: {upload_data}")
response = miaoStarsBasicApi.returnSession().put(
full_url, json=upload_data, headers=self.headers, timeout=30
)
# 记录响应状态码和内容
logger.info(f"上传URL请求响应状态码: {response.status_code}")
response_text = response.text
logger.info(f"上传URL请求响应内容长度: {len(response_text)} 字符")
# 如果响应内容较长只记录前100个字符
if len(response_text) > 100:
logger.info(f"上传URL请求响应内容预览: {response_text[:100]}...")
else:
logger.info(f"上传URL请求响应内容: {response_text}")
response.raise_for_status()
result = response.json()
# 添加错误处理检查响应是否为有效的JSON
try:
result = response.json()
logger.info(f"JSON解析成功响应code: {result.get('code', '未找到code字段')}")
except ValueError as e:
logger.error(f"JSON解析失败: {e}")
logger.error(f"无法解析的响应内容: {response_text}")
# 抛出异常让上层处理
raise ValueError(f"无效的响应格式: {response_text}")
if result.get("code") == 0:
self.uploadApplicationApprovedSignal.emit()
upload_urls = result.get("data").get("uploadURLs")
credential = result.get("data").get("credential")
data = result.get("data", {})
upload_urls = data.get("upload_urls") # 使用正确的字段名符合API规范
credential = data.get("credential")
session_id = data.get("session_id")
storage_policy = data.get("storage_policy", {})
policy_type = storage_policy.get("type", "")
logger.info(f"获取到上传URL数量: {len(upload_urls) if upload_urls else 0}")
logger.debug(f"存储策略类型: {policy_type}, Session ID: {session_id}")
self.uploadUrl = upload_urls[0] + "?chunk=0"
logger.info("获取到上传URL")
# 优先检查是否有session_id因为这是必须的
if not session_id:
logger.error("未获取到session_id无法构建上传URL")
self.uploadFailed.emit("未获取到session_id无法构建上传URL")
return
# 根据存储策略类型和API响应构建正确的上传URL
if upload_urls and len(upload_urls) > 0:
# 对于远程存储策略使用返回的upload_urls并正确设置chunk参数
# 注意根据URL情况决定是否添加?或&
if "?" in upload_urls[0]:
self.uploadUrl = upload_urls[0] + "&chunk=0"
else:
self.uploadUrl = upload_urls[0] + "?chunk=0"
logger.info(f"使用远程存储上传URL: {self.uploadUrl}")
else:
# 对于本地存储策略或没有提供upload_urls的情况使用session_id构建上传URL
# 正确的API路径格式: /file/upload/{sessionId}/{index}
# 修复分块索引应该从0开始符合Cloudreve V4 API规范
self.uploadUrl = f"{miaoStarsBasicApi.basicApi}/file/upload/{session_id}/0"
logger.info(f"使用本地存储分块上传路径: {self.uploadUrl}")
logger.info(f"获取到上传URL: {self.uploadUrl}")
# 获取文件总大小
total_size = os.path.getsize(self.file_path)
# 执行带进度显示的上传
upload_result = self._uploadWithProgress(
self.uploadUrl, credential, total_size
)
logger.info("开始调用_uploadWithProgress方法执行上传")
try:
upload_result = self._uploadWithProgress(
self.uploadUrl, credential, total_size
)
logger.info("_uploadWithProgress方法调用完成")
# 检查是否取消
if self._is_cancelled:
self.uploadCancelled.emit()
# 检查是否取消
if self._is_cancelled:
self.uploadCancelled.emit()
return
# 处理上传结果
logger.info(f"上传响应状态码: {upload_result.status_code}")
# 限制日志长度,避免日志过大
response_text = upload_result.text
if len(response_text) > 100:
logger.info(f"上传响应内容预览: {response_text[:100]}...")
else:
logger.info(f"上传响应内容: {response_text}")
# 确保上传成功
upload_result.raise_for_status()
except Exception as e:
logger.error(f"上传过程中发生异常: {str(e)}")
logger.error(f"异常类型: {type(e).__name__}")
self.uploadFailed.emit(f"上传过程中发生错误: {str(e)}")
return
# 处理上传结果
logger.info(f"上传响应: {upload_result}")
self.uploadFinished.emit()
try:
# 解析上传响应
upload_response = upload_result.json()
logger.info(f"上传响应JSON解析成功: {upload_response}")
# 检查上传是否成功完成
if upload_response.get('code') == 0:
logger.info("文件上传成功完成")
self.uploadFinished.emit()
else:
error_msg = upload_response.get('msg', '上传失败')
logger.error(f"上传失败: {error_msg}")
self.uploadFailed.emit(error_msg)
except ValueError as e:
# 如果响应不是有效的JSON只要状态码成功也视为上传成功
logger.warning(f"上传响应不是有效的JSON: {e}")
logger.info("基于成功状态码,假设上传成功")
self.uploadFinished.emit()
else:
error_msg = result.get("msg", "上传失败")

View File

@@ -1,11 +1,14 @@
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 import miaoStarsBasicApi
from app.core.api import basicApi, miaoStarsBasicApi
logger = logging.getLogger(__name__)
class TextLoaderThread(QThread):
@@ -15,54 +18,88 @@ class TextLoaderThread(QThread):
errorOccurred = pyqtSignal(str)
progressUpdated = pyqtSignal(int) # 进度更新信号
def __init__(self, url):
def __init__(self, url, fileId=None):
super().__init__()
self.url = url
self.url = url # 原始URL可能作为fallback
self.fileId = fileId # 文件ID优先使用
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)
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 = miaoStarsBasicApi.returnSession().get(
self.url,
stream=True,
timeout=(15, 30), # 增加超时时间连接15秒读取30秒
)
# 2. 增加超时时间并添加重试机制
response = basicApi.returnSession().get(
self.url,
stream=True,
timeout=(15, 30), # 增加超时时间连接15秒读取30秒
)
response.raise_for_status()
response.raise_for_status()
# 3. 获取文件大小(如果服务器支持)
total_size = int(response.headers.get("content-length", 0))
downloaded_size = 0
# 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)
# 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)
# 更新进度(如果知道总大小)
if total_size > 0:
progress = int((downloaded_size / total_size) * 100)
self.progressUpdated.emit(progress)
# 5. 合并内容并解码
binary_content = b"".join(content_chunks)
# 5. 合并内容 - 使用正确的二进制合并语法
binary_content = b"" .join(content_chunks)
if not binary_content:
self.errorOccurred.emit("下载内容为空")
# 确保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. 智能编码检测和解码
@@ -113,10 +150,11 @@ class ImageLoaderThread(QThread):
progressUpdated = pyqtSignal(int) # 进度更新信号
def __init__(
self, url, cache_dir="image_cache", max_cache_size=50 * 1024 * 1024
self, url, cache_dir="image_cache", max_cache_size=50 * 1024 * 1024, fileId=None
): # 50MB缓存
super().__init__()
self.url = url
self.url = url # 原始URL可能作为fallback
self.fileId = fileId # 文件ID优先使用
self.cache_dir = cache_dir
self.max_cache_size = max_cache_size
self._setup_cache()
@@ -184,48 +222,108 @@ class ImageLoaderThread(QThread):
def run(self):
"""线程执行函数"""
try:
# 1. 首先检查缓存
cached_pixmap = self._get_cached_image()
if cached_pixmap:
self.imageLoaded.emit(cached_pixmap)
return
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)
# 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()
# 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
# 获取文件大小(如果服务器支持)
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)
# 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)
# 更新进度(如果知道总大小)
if total_size > 0:
# 确保进度不超过100%
progress = min(int((downloaded_size / total_size) * 100), 100)
self.progressUpdated.emit(progress)
# 5. 从数据创建QImage比QPixmap更快
image = QImage()
image.loadFromData(image_data)
if image.isNull():
raise Exception("无法加载图片数据")
# 检查数据是否为空或类型是否正确
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)