fixing progress
This commit is contained in:
248
gui.py
248
gui.py
@@ -11,6 +11,8 @@ import mimetypes
|
||||
import urllib.parse
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import ssl
|
||||
|
||||
def normalize_file_path(path_str: str) -> str:
|
||||
if not path_str:
|
||||
@@ -587,7 +589,7 @@ class DraggableImageLabel(QLabel):
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.drag_start_pos = event.pos()
|
||||
self.drag_start_pos = event.position().toPoint()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
@@ -595,7 +597,7 @@ class DraggableImageLabel(QLabel):
|
||||
return
|
||||
if self.drag_start_pos is None:
|
||||
return
|
||||
if (event.pos() - self.drag_start_pos).manhattanLength() < QApplication.startDragDistance():
|
||||
if (event.position().toPoint() - self.drag_start_pos).manhattanLength() < QApplication.startDragDistance():
|
||||
return
|
||||
|
||||
if self.file_path and os.path.exists(self.file_path):
|
||||
@@ -1012,7 +1014,40 @@ def play_success_sound(audio_path=None, volume=None):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
class CurlUploadThread(QThread):
|
||||
class StreamingMultipartEncoder:
|
||||
def __init__(self, fields, file_key, file_path):
|
||||
self.boundary = f"----WebKitFormBoundary{time.time_ns():x}"
|
||||
self.file_path = file_path
|
||||
self.file_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0
|
||||
self.fields = fields
|
||||
self.file_key = file_key
|
||||
|
||||
body_parts = []
|
||||
for key, val in fields.items():
|
||||
if val is not None and str(val) != "":
|
||||
body_parts.append(f"--{self.boundary}\r\nContent-Disposition: form-data; name=\"{key}\"\r\n\r\n{val}\r\n".encode('utf-8'))
|
||||
|
||||
filename = os.path.basename(file_path)
|
||||
mime_type, _ = mimetypes.guess_type(file_path)
|
||||
if not mime_type:
|
||||
ext = os.path.splitext(file_path)[1].lower()
|
||||
if ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp']:
|
||||
mime_type = f"image/{ext.lstrip('.')}"
|
||||
if mime_type == "image/jpg": mime_type = "image/jpeg"
|
||||
elif ext in ['.mp4', '.webm', '.mkv', '.mov', '.avi']:
|
||||
mime_type = f"video/{ext.lstrip('.')}"
|
||||
else:
|
||||
mime_type = "image/png"
|
||||
|
||||
file_header = f"--{self.boundary}\r\nContent-Disposition: form-data; name=\"{self.file_key}\"; filename=\"{filename}\"\r\nContent-Type: {mime_type}\r\n\r\n".encode('utf-8')
|
||||
file_footer = f"\r\n--{self.boundary}--\r\n".encode('utf-8')
|
||||
|
||||
self.header_bytes = b"".join(body_parts) + file_header
|
||||
self.footer_bytes = file_footer
|
||||
self.total_size = len(self.header_bytes) + self.file_size + len(self.footer_bytes)
|
||||
self.content_type = f"multipart/form-data; boundary={self.boundary}"
|
||||
|
||||
class StreamingUploadThread(QThread):
|
||||
progress = Signal(int, int) # bytes_sent, bytes_total
|
||||
finished = Signal(bool, str) # success, response_or_error_msg
|
||||
|
||||
@@ -1025,93 +1060,146 @@ class CurlUploadThread(QThread):
|
||||
self.tags = tags
|
||||
self.visibility = visibility
|
||||
self.is_oc = is_oc
|
||||
self.process = None
|
||||
self.file_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0
|
||||
self.aborted = False
|
||||
self.sock = None
|
||||
|
||||
def abort(self):
|
||||
self.aborted = True
|
||||
if self.process:
|
||||
if self.sock:
|
||||
try:
|
||||
self.process.kill()
|
||||
self.sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
mime_type, _ = mimetypes.guess_type(self.file_path)
|
||||
if not mime_type:
|
||||
ext = os.path.splitext(self.file_path)[1].lower()
|
||||
if ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp']:
|
||||
mime_type = f"image/{ext.lstrip('.')}"
|
||||
if mime_type == "image/jpg":
|
||||
mime_type = "image/jpeg"
|
||||
elif ext in ['.mp4', '.webm', '.mkv', '.mov', '.avi']:
|
||||
mime_type = f"video/{ext.lstrip('.')}"
|
||||
else:
|
||||
mime_type = "image/png"
|
||||
|
||||
file_param = f"file=@{self.file_path}"
|
||||
if mime_type:
|
||||
file_param += f";type={mime_type}"
|
||||
|
||||
cmd = [
|
||||
"curl", "-#",
|
||||
"-F", file_param
|
||||
]
|
||||
if self.api_key:
|
||||
cmd.extend(["-H", f"X-Api-Key: {self.api_key}"])
|
||||
if self.rating:
|
||||
cmd.extend(["-F", f"rating={self.rating}"])
|
||||
if self.tags:
|
||||
cmd.extend(["-F", f"tags={self.tags}"])
|
||||
if self.visibility != "":
|
||||
cmd.extend(["-F", f"visibility={self.visibility}"])
|
||||
if self.is_oc != "0":
|
||||
cmd.extend(["-F", f"is_oc={self.is_oc}"])
|
||||
|
||||
cmd.append(self.api_url)
|
||||
fields = {
|
||||
"rating": self.rating,
|
||||
"tags": self.tags,
|
||||
"visibility": self.visibility,
|
||||
"is_oc": self.is_oc
|
||||
}
|
||||
|
||||
try:
|
||||
self.process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1
|
||||
)
|
||||
encoder = StreamingMultipartEncoder(fields, "file", self.file_path)
|
||||
parsed_url = urllib.parse.urlparse(self.api_url)
|
||||
|
||||
buffer = ""
|
||||
host = parsed_url.hostname
|
||||
port = parsed_url.port or (443 if parsed_url.scheme == "https" else 80)
|
||||
path = parsed_url.path or "/"
|
||||
if parsed_url.query:
|
||||
path += "?" + parsed_url.query
|
||||
|
||||
raw_sock = socket.create_connection((host, port), timeout=30)
|
||||
if parsed_url.scheme == "https":
|
||||
ctx = ssl.create_default_context()
|
||||
self.sock = ctx.wrap_socket(raw_sock, server_hostname=host)
|
||||
else:
|
||||
self.sock = raw_sock
|
||||
|
||||
req_headers = (
|
||||
f"POST {path} HTTP/1.1\r\n"
|
||||
f"Host: {host}\r\n"
|
||||
f"User-Agent: f0ckm-uploader/2.0\r\n"
|
||||
f"X-Api-Key: {self.api_key}\r\n"
|
||||
f"Content-Type: {encoder.content_type}\r\n"
|
||||
f"Content-Length: {encoder.total_size}\r\n"
|
||||
f"Connection: close\r\n\r\n"
|
||||
).encode('utf-8')
|
||||
|
||||
self.sock.sendall(req_headers)
|
||||
|
||||
total_sent = 0
|
||||
self.sock.sendall(encoder.header_bytes)
|
||||
total_sent += len(encoder.header_bytes)
|
||||
self.progress.emit(total_sent, encoder.total_size)
|
||||
|
||||
chunk_size = 32768
|
||||
with open(self.file_path, "rb") as f:
|
||||
while True:
|
||||
char = self.process.stderr.read(1)
|
||||
if not char and self.process.poll() is not None:
|
||||
if self.aborted:
|
||||
self.finished.emit(False, "ABORTED")
|
||||
return
|
||||
chunk = f.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
if char in ("\r", "\n"):
|
||||
matches = re.findall(r'(\d+(?:\.\d+)?)\s*%', buffer)
|
||||
if matches:
|
||||
try:
|
||||
pct = float(matches[-1])
|
||||
bytes_sent = int(self.file_size * (pct / 100.0))
|
||||
self.progress.emit(bytes_sent, self.file_size)
|
||||
except Exception:
|
||||
pass
|
||||
buffer = ""
|
||||
else:
|
||||
buffer += char
|
||||
|
||||
stdout_data, stderr_data = self.process.communicate()
|
||||
exit_code = self.process.returncode
|
||||
self.sock.sendall(chunk)
|
||||
total_sent += len(chunk)
|
||||
|
||||
pct = min(99, int((total_sent / encoder.total_size) * 100))
|
||||
bytes_sent_calc = int(encoder.total_size * (pct / 100.0))
|
||||
self.progress.emit(bytes_sent_calc, encoder.total_size)
|
||||
time.sleep(0.005)
|
||||
|
||||
self.sock.sendall(encoder.footer_bytes)
|
||||
total_sent += len(encoder.footer_bytes)
|
||||
self.progress.emit(encoder.total_size, encoder.total_size)
|
||||
|
||||
response_data = bytearray()
|
||||
while not self.aborted:
|
||||
data = self.sock.recv(4096)
|
||||
if not data:
|
||||
break
|
||||
response_data.extend(data)
|
||||
|
||||
if self.aborted:
|
||||
self.finished.emit(False, "ABORTED")
|
||||
elif exit_code == 0:
|
||||
self.finished.emit(True, stdout_data)
|
||||
return
|
||||
|
||||
parts = response_data.split(b"\r\n\r\n", 1)
|
||||
headers_str = parts[0].decode('utf-8', errors='replace') if parts else ""
|
||||
raw_body = parts[1].decode('utf-8', errors='replace') if len(parts) > 1 else response_data.decode('utf-8', errors='replace')
|
||||
status_line = headers_str.split("\r\n")[0] if headers_str else ""
|
||||
|
||||
# De-chunk HTTP/1.1 response body if Transfer-Encoding: chunked or chunk-prefixed
|
||||
body = raw_body
|
||||
if "transfer-encoding: chunked" in headers_str.lower() or re.match(r'^[0-9a-fA-F]+\r\n', raw_body.lstrip()):
|
||||
decoded_chunks = []
|
||||
rest = raw_body.lstrip()
|
||||
try:
|
||||
while rest:
|
||||
lines = rest.split("\r\n", 1)
|
||||
if len(lines) < 2:
|
||||
break
|
||||
hex_str = lines[0].strip().split(";")[0]
|
||||
try:
|
||||
chunk_len = int(hex_str, 16)
|
||||
except ValueError:
|
||||
break
|
||||
if chunk_len == 0:
|
||||
break
|
||||
chunk_content = lines[1][:chunk_len]
|
||||
decoded_chunks.append(chunk_content)
|
||||
rest = lines[1][chunk_len:]
|
||||
if rest.startswith("\r\n"):
|
||||
rest = rest[2:]
|
||||
if decoded_chunks:
|
||||
body = "".join(decoded_chunks)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Extra safety fallback: extract JSON substring if there's any hex prefix left
|
||||
if not body.strip().startswith("{") and "{" in body and "}" in body:
|
||||
match = re.search(r'\{.*\}', body, re.DOTALL)
|
||||
if match:
|
||||
body = match.group(0)
|
||||
|
||||
if "200" in status_line or "201" in status_line:
|
||||
self.finished.emit(True, body)
|
||||
else:
|
||||
self.finished.emit(False, stderr_data or f"curl exited with code {exit_code}")
|
||||
self.finished.emit(False, body or status_line)
|
||||
|
||||
except Exception as e:
|
||||
if self.aborted:
|
||||
self.finished.emit(False, "ABORTED")
|
||||
else:
|
||||
self.finished.emit(False, str(e))
|
||||
finally:
|
||||
if self.sock:
|
||||
try:
|
||||
self.sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ==========================================
|
||||
# System Tray Application Manager
|
||||
@@ -1238,6 +1326,12 @@ class SystemTrayApp(QObject):
|
||||
if os.path.exists(save_path) and os.path.getsize(save_path) > 0:
|
||||
self.upload_file_direct(save_path)
|
||||
|
||||
def on_abort_upload(self):
|
||||
if self.current_reply and self.current_reply.isRunning():
|
||||
self.current_reply.abort()
|
||||
elif hasattr(self, "upload_thread") and self.upload_thread:
|
||||
self.upload_thread.abort()
|
||||
|
||||
def upload_file_direct(self, file_path):
|
||||
file_path = normalize_file_path(file_path)
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
@@ -1247,10 +1341,13 @@ class SystemTrayApp(QObject):
|
||||
config = get_env_config()
|
||||
api_url = config.get("api_url", "")
|
||||
api_key = config.get("api_key", "")
|
||||
if not api_url or not api_key:
|
||||
self.show_message("Upload Error", "API URL and API Key must be set in Settings.", QSystemTrayIcon.Critical)
|
||||
return
|
||||
|
||||
rating = config.get("default_rating", "")
|
||||
tags = config.get("default_tags", "")
|
||||
if tags:
|
||||
# Format tags as clean comma-separated string ("tag1, tag2") for f0ckm API tagsRaw.split(',') compatibility
|
||||
tag_list = [t.strip() for t in re.split(r'[,]+', tags) if t.strip()]
|
||||
tags = ", ".join(tag_list)
|
||||
|
||||
@@ -1264,13 +1361,28 @@ class SystemTrayApp(QObject):
|
||||
self.tray.setIcon(create_digit_icon(0))
|
||||
self.tray.setToolTip(f"Uploading {filename}...\n0% (0 B)")
|
||||
|
||||
self.upload_thread = CurlUploadThread(api_url, api_key, file_path, rating, tags, visibility, is_oc)
|
||||
self.upload_thread = StreamingUploadThread(api_url, api_key, file_path, rating, tags, visibility, is_oc)
|
||||
self.upload_thread.progress.connect(self.on_upload_progress)
|
||||
self.upload_thread.finished.connect(lambda ok, resp: self.on_upload_finished(ok, resp, file_path))
|
||||
self.upload_thread.start()
|
||||
|
||||
self.abort_action.setEnabled(True)
|
||||
|
||||
def _on_qt_reply_finished(self, reply, file_path):
|
||||
self.current_reply = None
|
||||
err = reply.error()
|
||||
if err == QNetworkReply.NetworkError.NoError:
|
||||
resp_bytes = reply.readAll().data()
|
||||
resp_str = resp_bytes.decode('utf-8', errors='replace')
|
||||
self.on_upload_finished(True, resp_str, file_path)
|
||||
else:
|
||||
if err == QNetworkReply.NetworkError.OperationCanceledError:
|
||||
self.on_upload_finished(False, "ABORTED", file_path)
|
||||
else:
|
||||
resp_bytes = reply.readAll().data()
|
||||
resp_str = resp_bytes.decode('utf-8', errors='replace') if resp_bytes else reply.errorString()
|
||||
self.on_upload_finished(False, resp_str, file_path)
|
||||
|
||||
def on_upload_progress(self, bytes_sent, bytes_total):
|
||||
if bytes_total > 0:
|
||||
pct = min(100, max(0, int((bytes_sent / bytes_total) * 100)))
|
||||
|
||||
Reference in New Issue
Block a user