v4.0.0
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import sys
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from ytsage_gui import YTSageApp # Import the main application class
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
window = YTSageApp() # Instantiate the main application class
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,176 @@
|
||||
from PySide6.QtCore import QThread, Signal, QObject
|
||||
import yt_dlp # Keep yt_dlp import here - only downloader uses it.
|
||||
import time
|
||||
import os
|
||||
import re
|
||||
|
||||
class SignalManager(QObject):
|
||||
update_formats = Signal(list)
|
||||
update_status = Signal(str)
|
||||
update_progress = Signal(float)
|
||||
|
||||
class DownloadThread(QThread):
|
||||
progress_signal = Signal(float)
|
||||
status_signal = Signal(str)
|
||||
finished_signal = Signal()
|
||||
error_signal = Signal(str)
|
||||
|
||||
def __init__(self, url, path, format_id, subtitle_lang=None, is_playlist=False, merge_subs=False, enable_sponsorblock=False, resolution=''):
|
||||
super().__init__()
|
||||
self.url = url
|
||||
self.path = path
|
||||
self.format_id = format_id
|
||||
self.subtitle_lang = subtitle_lang
|
||||
self.is_playlist = is_playlist
|
||||
self.merge_subs = merge_subs
|
||||
self.enable_sponsorblock = enable_sponsorblock
|
||||
self.resolution = resolution
|
||||
self.paused = False
|
||||
self.cancelled = False
|
||||
|
||||
def cleanup_partial_files(self):
|
||||
"""Delete any partial files including .part and unmerged format-specific files"""
|
||||
try:
|
||||
pattern = re.compile(r'\.f\d+\.') # Pattern to match format codes like .f243.
|
||||
for filename in os.listdir(self.path):
|
||||
file_path = os.path.join(self.path, filename)
|
||||
if filename.endswith('.part') or pattern.search(filename):
|
||||
try:
|
||||
if os.path.isfile(file_path):
|
||||
os.remove(file_path)
|
||||
except Exception as e:
|
||||
print(f"Error deleting {filename}: {str(e)}")
|
||||
except Exception as e:
|
||||
self.error_signal.emit(f"Error cleaning partial files: {str(e)}")
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
class DebugLogger:
|
||||
def debug(self, msg):
|
||||
# Add detection of post-processing messages
|
||||
if "Downloading" in msg:
|
||||
self.thread.status_signal.emit("Downloading...")
|
||||
elif "Post-process" in msg or "Sponsorblock" in msg:
|
||||
self.thread.status_signal.emit("Post-processing: Removing sponsor segments...")
|
||||
self.thread.progress_signal.emit(99) # Keep progress bar at 99%
|
||||
elif any(x in msg.lower() for x in ['downloading webpage', 'downloading api', 'extracting', 'downloading m3u8']):
|
||||
self.thread.status_signal.emit("Preparing for download...")
|
||||
self.thread.progress_signal.emit(0)
|
||||
|
||||
def warning(self, msg):
|
||||
self.thread.status_signal.emit(f"Warning: {msg}")
|
||||
|
||||
def error(self, msg):
|
||||
self.thread.status_signal.emit(f"Error: {msg}")
|
||||
|
||||
def __init__(self, thread):
|
||||
self.thread = thread
|
||||
|
||||
def progress_hook(d):
|
||||
if self.cancelled:
|
||||
raise Exception("Download cancelled by user")
|
||||
|
||||
if d['status'] == 'downloading':
|
||||
while self.paused and not self.cancelled:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
try:
|
||||
downloaded_bytes = d.get('downloaded_bytes', 0)
|
||||
total_bytes = d.get('total_bytes', 0) or d.get('total_bytes_estimate', 0)
|
||||
|
||||
if total_bytes:
|
||||
progress = (downloaded_bytes / total_bytes) * 100
|
||||
self.progress_signal.emit(progress)
|
||||
|
||||
speed = d.get('speed', 0)
|
||||
if speed:
|
||||
speed_str = f"{speed/1024/1024:.1f} MB/s"
|
||||
else:
|
||||
speed_str = "N/A"
|
||||
|
||||
eta = d.get('eta', 0)
|
||||
if eta:
|
||||
eta_str = f"{eta//60}:{eta%60:02d}"
|
||||
else:
|
||||
eta_str = "N/A"
|
||||
|
||||
filename = os.path.basename(d.get('filename', ''))
|
||||
|
||||
status = f"Speed: {speed_str} | ETA: {eta_str} | File: {filename}"
|
||||
self.status_signal.emit(status)
|
||||
|
||||
except Exception as e:
|
||||
self.status_signal.emit("Downloading...")
|
||||
|
||||
elif d['status'] == 'finished':
|
||||
if self.enable_sponsorblock:
|
||||
self.progress_signal.emit(99)
|
||||
self.status_signal.emit("Post-processing: Removing sponsor segments...")
|
||||
else:
|
||||
self.progress_signal.emit(100)
|
||||
self.status_signal.emit("Download completed!")
|
||||
|
||||
# Base yt-dlp options with resolution in filename
|
||||
output_template = '%(title)s_%(resolution)s.%(ext)s'
|
||||
if self.is_playlist:
|
||||
output_template = '%(playlist_title)s/%(title)s_%(resolution)s.%(ext)s'
|
||||
|
||||
ydl_opts = {
|
||||
'format': f'{self.format_id}+bestaudio/best',
|
||||
'outtmpl': os.path.join(self.path, output_template),
|
||||
'progress_hooks': [progress_hook],
|
||||
'merge_output_format': 'mkv' if self.merge_subs else 'mp4',
|
||||
'logger': DebugLogger(self),
|
||||
'postprocessors': [{
|
||||
'key': 'FFmpegVideoConvertor',
|
||||
'preferedformat': 'mkv' if self.merge_subs else 'mp4'
|
||||
}]
|
||||
}
|
||||
|
||||
# Add subtitle options if selected
|
||||
if self.subtitle_lang:
|
||||
lang_code = self.subtitle_lang.split(' - ')[0]
|
||||
is_auto = 'Auto-generated' in self.subtitle_lang
|
||||
ydl_opts.update({
|
||||
'writesubtitles': True,
|
||||
'subtitleslangs': [lang_code],
|
||||
'writeautomaticsub': True,
|
||||
'skip_manual_subs': is_auto,
|
||||
'skip_auto_subs': not is_auto,
|
||||
'embedsubtitles': self.merge_subs,
|
||||
})
|
||||
|
||||
# Add SponsorBlock options if enabled
|
||||
if self.enable_sponsorblock:
|
||||
ydl_opts['postprocessors'].extend([{
|
||||
'key': 'SponsorBlock',
|
||||
'categories': ['sponsor'],
|
||||
'api': 'https://sponsor.ajay.app'
|
||||
}, {
|
||||
'key': 'ModifyChapters',
|
||||
'remove_sponsor_segments': ['sponsor'],
|
||||
'sponsorblock_chapter_title': '[SponsorBlock]',
|
||||
'force_keyframes': False
|
||||
}])
|
||||
self.progress_signal.emit(99)
|
||||
self.status_signal.emit("Post-processing: Removing sponsor segments...")
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([self.url])
|
||||
|
||||
self.finished_signal.emit()
|
||||
|
||||
# Clean up subtitle files after successful download
|
||||
if self.merge_subs:
|
||||
for filename in os.listdir(self.path):
|
||||
if filename.lower().endswith(('.vtt', '.srt', '.ass')):
|
||||
try:
|
||||
os.remove(os.path.join(self.path, filename))
|
||||
except Exception as e:
|
||||
self.error_signal.emit(f"Error deleting subtitle file: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
if str(e) == "Download cancelled by user":
|
||||
self.cleanup_partial_files()
|
||||
self.error_signal.emit(str(e))
|
||||
+1800
-1684
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
def check_ffmpeg():
|
||||
try:
|
||||
subprocess.run(['ffmpeg', '-version'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=True)
|
||||
return True
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
return False
|
||||
|
||||
def get_yt_dlp_path():
|
||||
"""Get the appropriate yt-dlp path based on platform and deployment method"""
|
||||
try:
|
||||
if getattr(sys, 'frozen', False):
|
||||
if sys.platform == 'darwin':
|
||||
# For macOS .app bundle
|
||||
if 'Contents/MacOS' in sys.executable:
|
||||
# Inside .app bundle
|
||||
return os.path.join(os.path.dirname(sys.executable), 'yt-dlp')
|
||||
else:
|
||||
# Fallback to user's home directory for macOS
|
||||
base_path = os.path.expanduser('~/Library/Application Support/YTSage')
|
||||
os.makedirs(base_path, exist_ok=True)
|
||||
return os.path.join(base_path, 'yt-dlp')
|
||||
elif sys.platform == 'win32':
|
||||
# For Windows executable
|
||||
app_data = os.getenv('APPDATA')
|
||||
if app_data:
|
||||
base_path = os.path.join(app_data, 'YTSage')
|
||||
else:
|
||||
base_path = os.path.dirname(sys.executable)
|
||||
os.makedirs(base_path, exist_ok=True)
|
||||
return os.path.join(base_path, 'yt-dlp.exe')
|
||||
else:
|
||||
# For Linux AppImage or binary
|
||||
if 'APPIMAGE' in os.environ:
|
||||
# Inside AppImage
|
||||
xdg_data = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
|
||||
base_path = os.path.join(xdg_data, 'YTSage')
|
||||
else:
|
||||
base_path = os.path.dirname(sys.executable)
|
||||
os.makedirs(base_path, exist_ok=True)
|
||||
return os.path.join(base_path, 'yt-dlp')
|
||||
else:
|
||||
# For development/script mode
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'yt-dlp.exe' if sys.platform == 'win32' else 'yt-dlp')
|
||||
except Exception as e:
|
||||
print(f"Error determining yt-dlp path: {e}")
|
||||
# Fallback to current directory
|
||||
return os.path.join(os.getcwd(), 'yt-dlp.exe' if sys.platform == 'win32' else 'yt-dlp')
|
||||
|
||||
def load_saved_path(main_window_instance): # Pass the main window instance
|
||||
config_file = main_window_instance.config_file # Access config_file via instance
|
||||
try:
|
||||
if config_file.exists():
|
||||
with open(config_file, 'r') as f:
|
||||
config = json.load(f)
|
||||
saved_path = config.get('download_path', '')
|
||||
if os.path.exists(saved_path):
|
||||
main_window_instance.last_path = saved_path # Access last_path via instance
|
||||
else:
|
||||
main_window_instance.last_path = str(Path.home() / 'Downloads')
|
||||
else:
|
||||
main_window_instance.last_path = str(Path.home() / 'Downloads')
|
||||
except Exception as e:
|
||||
print(f"Error loading saved settings: {e}")
|
||||
main_window_instance.last_path = str(Path.home() / 'Downloads')
|
||||
|
||||
def save_path(main_window_instance, path): # Pass main window instance
|
||||
config_file = main_window_instance.config_file # Access config_file via instance
|
||||
try:
|
||||
config = {
|
||||
'download_path': path
|
||||
}
|
||||
with open(config_file, 'w') as f:
|
||||
json.dump(config, f)
|
||||
except Exception as e:
|
||||
print(f"Error saving settings: {e}")
|
||||
Reference in New Issue
Block a user