mirror of
https://github.com/Yuvi9587/Kemono-Downloader.git
synced 2025-12-29 16:14:44 +00:00
162 lines
7.0 KiB
Python
162 lines
7.0 KiB
Python
|
|
import os
|
||
|
|
import sys
|
||
|
|
import re
|
||
|
|
import threading
|
||
|
|
import time
|
||
|
|
from PyQt5.QtCore import QThread, pyqtSignal, QProcess
|
||
|
|
import cloudscraper
|
||
|
|
|
||
|
|
from ...core.fap_nation_client import fetch_fap_nation_data
|
||
|
|
from ...services.multipart_downloader import download_file_in_parts
|
||
|
|
|
||
|
|
class FapNationDownloadThread(QThread):
|
||
|
|
"""
|
||
|
|
A dedicated QThread for Fap-Nation that uses a hybrid approach, choosing
|
||
|
|
between yt-dlp for HLS streams and a multipart downloader for direct links.
|
||
|
|
"""
|
||
|
|
progress_signal = pyqtSignal(str)
|
||
|
|
file_progress_signal = pyqtSignal(str, object)
|
||
|
|
finished_signal = pyqtSignal(int, int, bool)
|
||
|
|
overall_progress_signal = pyqtSignal(int, int)
|
||
|
|
|
||
|
|
def __init__(self, url, output_dir, use_post_subfolder, pause_event, cancellation_event, gui_signals, parent=None):
|
||
|
|
super().__init__(parent)
|
||
|
|
self.album_url = url
|
||
|
|
self.output_dir = output_dir
|
||
|
|
self.use_post_subfolder = use_post_subfolder
|
||
|
|
self.is_cancelled = False
|
||
|
|
self.process = None
|
||
|
|
self.current_filename = "Unknown File"
|
||
|
|
self.album_name = "fap-nation_album"
|
||
|
|
self.pause_event = pause_event
|
||
|
|
self.cancellation_event = cancellation_event
|
||
|
|
self.gui_signals = gui_signals
|
||
|
|
self._is_finished = False
|
||
|
|
|
||
|
|
self.process = QProcess(self)
|
||
|
|
self.process.readyReadStandardOutput.connect(self.handle_ytdlp_output)
|
||
|
|
|
||
|
|
def run(self):
|
||
|
|
self.progress_signal.emit("=" * 40)
|
||
|
|
self.progress_signal.emit(f"🚀 Starting Fap-Nation Download for: {self.album_url}")
|
||
|
|
|
||
|
|
self.album_name, files_to_download = fetch_fap_nation_data(self.album_url, self.progress_signal.emit)
|
||
|
|
|
||
|
|
if self.is_cancelled or not files_to_download:
|
||
|
|
self.progress_signal.emit("❌ Failed to extract file information. Aborting.")
|
||
|
|
self.finished_signal.emit(0, 1, self.is_cancelled)
|
||
|
|
return
|
||
|
|
|
||
|
|
self.overall_progress_signal.emit(1, 0)
|
||
|
|
|
||
|
|
save_path = self.output_dir
|
||
|
|
if self.use_post_subfolder:
|
||
|
|
save_path = os.path.join(self.output_dir, self.album_name)
|
||
|
|
self.progress_signal.emit(f" Subfolder per Post is ON. Saving to: '{self.album_name}'")
|
||
|
|
os.makedirs(save_path, exist_ok=True)
|
||
|
|
|
||
|
|
file_data = files_to_download[0]
|
||
|
|
self.current_filename = file_data.get('filename')
|
||
|
|
download_url = file_data.get('url')
|
||
|
|
link_type = file_data.get('type')
|
||
|
|
filepath = os.path.join(save_path, self.current_filename)
|
||
|
|
|
||
|
|
if os.path.exists(filepath):
|
||
|
|
self.progress_signal.emit(f" -> Skip: '{self.current_filename}' already exists.")
|
||
|
|
self.overall_progress_signal.emit(1, 1)
|
||
|
|
self.finished_signal.emit(0, 1, self.is_cancelled)
|
||
|
|
return
|
||
|
|
|
||
|
|
if link_type == 'hls':
|
||
|
|
self.download_with_ytdlp(filepath, download_url)
|
||
|
|
elif link_type == 'direct':
|
||
|
|
self.download_with_multipart(filepath, download_url)
|
||
|
|
else:
|
||
|
|
self.progress_signal.emit(f" ❌ Unknown link type '{link_type}'. Aborting.")
|
||
|
|
self._on_ytdlp_finished(-1)
|
||
|
|
|
||
|
|
def download_with_ytdlp(self, filepath, playlist_url):
|
||
|
|
self.progress_signal.emit(f" Downloading (HLS Stream): '{self.current_filename}' using yt-dlp...")
|
||
|
|
try:
|
||
|
|
if getattr(sys, 'frozen', False):
|
||
|
|
base_path = sys._MEIPASS
|
||
|
|
ytdlp_path = os.path.join(base_path, "yt-dlp.exe")
|
||
|
|
else:
|
||
|
|
ytdlp_path = "yt-dlp.exe"
|
||
|
|
|
||
|
|
if not os.path.exists(ytdlp_path):
|
||
|
|
self.progress_signal.emit(f" ❌ ERROR: yt-dlp.exe not found at '{ytdlp_path}'.")
|
||
|
|
self._on_ytdlp_finished(-1)
|
||
|
|
return
|
||
|
|
|
||
|
|
command = [ytdlp_path, '--no-warnings', '--progress', '--output', filepath, '--merge-output-format', 'mp4', playlist_url]
|
||
|
|
|
||
|
|
self.process.start(command[0], command[1:])
|
||
|
|
self.process.waitForFinished(-1)
|
||
|
|
self._on_ytdlp_finished(self.process.exitCode())
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
self.progress_signal.emit(f" ❌ Failed to start yt-dlp: {e}")
|
||
|
|
self._on_ytdlp_finished(-1)
|
||
|
|
|
||
|
|
def download_with_multipart(self, filepath, direct_url):
|
||
|
|
self.progress_signal.emit(f" Downloading (Direct Link): '{self.current_filename}' using multipart downloader...")
|
||
|
|
try:
|
||
|
|
session = cloudscraper.create_scraper()
|
||
|
|
head_response = session.head(direct_url, allow_redirects=True, timeout=20)
|
||
|
|
head_response.raise_for_status()
|
||
|
|
total_size = int(head_response.headers.get('content-length', 0))
|
||
|
|
|
||
|
|
success, _, _, _ = download_file_in_parts(
|
||
|
|
file_url=direct_url, save_path=filepath, total_size=total_size, num_parts=5,
|
||
|
|
headers=session.headers, api_original_filename=self.current_filename,
|
||
|
|
emitter_for_multipart=self.gui_signals,
|
||
|
|
cookies_for_chunk_session=session.cookies,
|
||
|
|
cancellation_event=self.cancellation_event,
|
||
|
|
skip_event=None, logger_func=self.progress_signal.emit, pause_event=self.pause_event
|
||
|
|
)
|
||
|
|
self._on_ytdlp_finished(0 if success else 1)
|
||
|
|
except Exception as e:
|
||
|
|
self.progress_signal.emit(f" ❌ Multipart download failed: {e}")
|
||
|
|
self._on_ytdlp_finished(1)
|
||
|
|
|
||
|
|
def handle_ytdlp_output(self):
|
||
|
|
if not self.process:
|
||
|
|
return
|
||
|
|
|
||
|
|
output = self.process.readAllStandardOutput().data().decode('utf-8', errors='ignore')
|
||
|
|
for line in reversed(output.strip().splitlines()):
|
||
|
|
line = line.strip()
|
||
|
|
progress_match = re.search(r'\[download\]\s+([\d.]+)%\s+of\s+~?\s*([\d.]+\w+B)', line)
|
||
|
|
if progress_match:
|
||
|
|
percent, size = progress_match.groups()
|
||
|
|
self.file_progress_signal.emit("yt-dlp:", f"{percent}% of {size}")
|
||
|
|
break
|
||
|
|
|
||
|
|
def _on_ytdlp_finished(self, exit_code):
|
||
|
|
if self._is_finished:
|
||
|
|
return
|
||
|
|
self._is_finished = True
|
||
|
|
|
||
|
|
download_count, skip_count = 0, 0
|
||
|
|
|
||
|
|
if self.is_cancelled:
|
||
|
|
self.progress_signal.emit(f" Download of '{self.current_filename}' was cancelled.")
|
||
|
|
skip_count = 1
|
||
|
|
elif exit_code == 0:
|
||
|
|
self.progress_signal.emit(f" ✅ Download process finished successfully for '{self.current_filename}'.")
|
||
|
|
download_count = 1
|
||
|
|
else:
|
||
|
|
self.progress_signal.emit(f" ❌ Download process exited with an error (Code: {exit_code}) for '{self.current_filename}'.")
|
||
|
|
skip_count = 1
|
||
|
|
|
||
|
|
self.overall_progress_signal.emit(1, 1)
|
||
|
|
self.process = None
|
||
|
|
self.finished_signal.emit(download_count, skip_count, self.is_cancelled)
|
||
|
|
|
||
|
|
def cancel(self):
|
||
|
|
self.is_cancelled = True
|
||
|
|
self.cancellation_event.set()
|
||
|
|
if self.process and self.process.state() == QProcess.Running:
|
||
|
|
self.progress_signal.emit(" Cancellation signal received, terminating yt-dlp process.")
|
||
|
|
self.process.kill()
|