diff --git a/Icon/icon.png b/Icon/icon.png
deleted file mode 100644
index 8b32d05..0000000
Binary files a/Icon/icon.png and /dev/null differ
diff --git a/README.md b/README.md
index 0fd828b..6a3e526 100644
--- a/README.md
+++ b/README.md
@@ -27,13 +27,12 @@ Download videos in any quality, extract audio, fetch subtitles, and more.
-| Core Features | Advanced Features | Extra Features |
-|-----------------------------------|-----------------------------------------|------------------------------------|
-| 🎥 Format Table | 🚫 SponsorBlock Integration | 💾 Save Download Path |
-| 🎵 Audio Extraction | 📝 Multi-Subtitle Select & Merge | 🔄 Auto-Updates |
-| ✨ Simple UI | 💾 Save Description | 🛠️ FFmpeg/yt-dlp Detection |
-| 📋 Playlist Support | 🖼️ Save thumbnail | ⚙️ Custom Commands |
-| 🖼️ Playlist Selector | 🚀 Speed Limiter | 🍪 Login with Cookies |
+| Core Features | Advanced Features | Extra Features |
+|--------------|-------------------|----------------|
+| 🎥 Smart Video Quality | 🚫 SponsorBlock Integration | 💾 Save Download Path |
+| 🎵 Audio Extraction | 📝 Subtitle Support & Filtering | 🔄 Auto-Updates |
+| 📊 Real-time Progress | ⚙️ Custom Commands | 🛠️ FFmpeg Tools |
+| 📋 Playlist Support | 🖼️ Save thumbnail | ⚠️ Error Handling |
@@ -41,7 +40,7 @@ Download videos in any quality, extract audio, fetch subtitles, and more.
### Quick Install (Recommended)
```bash
-pip install ytsage
+pip install YTSage
```
```bash
# Run the application
@@ -53,12 +52,10 @@ ytsage
### Pre-built Executables
- 🪟 Windows: `YTSage.exe`
-- 🪟 Windows: `YTSage-ffmpeg.exe` (Includes FFmpeg)
-- 🐧 Linux: `YTSage-x86_64.AppImage`
-- 🍎 macOS: `YTSage-macOS-app.zip`
- 🍎 macOS: `YTSage.dmg`
+- 🐧 Linux: `YTSage.AppImage`
-### Manual Installation from Source
+### Manual Installation
```bash
# Clone repository
git clone https://github.com/oop7/YTSage.git
@@ -91,8 +88,8 @@ python main.py
 |
- | Audio Format |
- Subtitle Options |
+ Audio Format Selection with Save Thumbnail |
+ Subtitle Options merged with Remove Sponsor Segments |
@@ -110,10 +107,9 @@ python main.py
- `Audio Only` for audio extraction
5. **Choose Options:**
- Enable subtitles & select language
- - Enable subtitle merge
+ - Enable subtitle embedding
- Save thumbnail
- Remove sponsor segments
- - Save description
6. **Select Output Directory**
7. **Click "Download"**
@@ -124,9 +120,8 @@ python main.py
1. **Paste Playlist URL**
2. **Click "Analyze"**
-3. **Select videos from the playlist selector (optional, defaults to all)**
-4. **Choose desired format/quality**
-5. **Click "Download"**
+3. **Select Best Quality**
+4. **Click "Download"**
> 💡 The application automatically handles the download queue
@@ -137,18 +132,9 @@ python main.py
- **Quality Selection:** Choose the highest resolution for best quality
- **Subtitle Options:** Filter languages and embed into video
+- **SponsorBlock:** Automatically skip promotional content
- **Custom Commands:** Access advanced yt-dlp features
-- **Save Description:** Save the description of the video
-- **Save Thumbnail:** Save the thumbnail of the video
-- **Remove Sponsor Segments:** Remove sponsor segments from the video
-- **Speed Limiter:** Limit the download speed
-- **Login with Cookies:** Login with cookies
-- **Save Download Path:** Save the download path
-- **Update yt-dlp:** Update yt-dlp
-- **FFmpeg/yt-dlp Detection:** Automatically detect FFmpeg/yt-dlp
-- **Custom Commands:** Access advanced yt-dlp features
-
-
+- **Output Directory:** Ensure sufficient storage space
@@ -162,7 +148,6 @@ Pillow
requests
FFmpeg
packaging
-markdown
```
## 👥 Contributing
@@ -206,9 +191,6 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
| [PySide6](https://wiki.qt.io/Qt_for_Python) | GUI Framework |
| [FFmpeg](https://ffmpeg.org/) | Media Processing |
| [Pillow](https://python-pillow.org/) | Image Processing |
-| [requests](https://requests.readthedocs.io/) | HTTP Requests |
-| [packaging](https://packaging.python.org/) | Packaging |
-| [markdown](https://python-markdown.github.io/) | Markdown Processing |
diff --git a/requirements.txt b/requirements.txt
index f5fd815..b195626 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,5 +2,4 @@ PySide6
yt-dlp
requests
pillow
-packaging
-markdown
\ No newline at end of file
+packaging
\ No newline at end of file
diff --git a/ytsage_downloader.py b/ytsage_downloader.py
index 5b33505..e9b88a7 100644
--- a/ytsage_downloader.py
+++ b/ytsage_downloader.py
@@ -1,10 +1,8 @@
-from PySide6.QtCore import QThread, Signal, QObject, QProcess, QTimer
+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
-import subprocess # For direct CLI command execution
-import shlex # For safely parsing command arguments
from pathlib import Path
class SignalManager(QObject):
@@ -18,32 +16,20 @@ class DownloadThread(QThread):
finished_signal = Signal()
error_signal = Signal(str)
file_exists_signal = Signal(str) # New signal for file existence
- update_details = Signal(str) # New signal for filename, speed, ETA
- def __init__(self, url, path, format_id, subtitle_langs=None, is_playlist=False, merge_subs=False, enable_sponsorblock=False, resolution='', playlist_items=None, save_description=False, cookie_file=None, rate_limit=None):
+ def __init__(self, url, path, format_id, subtitle_lang=None, is_playlist=False, merge_subs=False, enable_sponsorblock=False, resolution='', playlist_items=None):
super().__init__()
self.url = url
self.path = path
self.format_id = format_id
- self.subtitle_langs = subtitle_langs if subtitle_langs else []
+ self.subtitle_lang = subtitle_lang
self.is_playlist = is_playlist
self.merge_subs = merge_subs
self.enable_sponsorblock = enable_sponsorblock
self.resolution = resolution
self.playlist_items = playlist_items
- self.save_description = save_description
- self.cookie_file = cookie_file
- self.rate_limit = rate_limit
self.paused = False
self.cancelled = False
- self.process = None
- self.use_direct_command = True # Flag to use direct CLI command instead of Python API
- self.last_output_time = time.time()
- self.timeout_timer = None
- self.current_filename = None # Initialize filename storage
- self.last_file_path = None # Initialize full file path storage
- self.subtitle_files = [] # Track subtitle files that are created
- self.initial_subtitle_files = set() # Track initial subtitle files before download
def cleanup_partial_files(self):
"""Delete any partial files including .part and unmerged format-specific files"""
@@ -60,99 +46,14 @@ class DownloadThread(QThread):
except Exception as e:
self.error_signal.emit(f"Error cleaning partial files: {str(e)}")
- def cleanup_subtitle_files(self):
- """Delete subtitle files after they have been merged into the video file"""
- if not self.merge_subs:
- return # Only cleanup if merge_subs is enabled
-
- try:
- deleted_count = 0
-
- # Method 1: Delete tracked subtitle files from output messages
- if self.subtitle_files:
- for subtitle_file in self.subtitle_files:
- try:
- if os.path.isfile(subtitle_file):
- os.remove(subtitle_file)
- deleted_count += 1
- print(f"DEBUG: Deleted tracked subtitle file: {os.path.basename(subtitle_file)}")
- except Exception as e:
- print(f"Error deleting subtitle file {subtitle_file}: {str(e)}")
-
- print(f"DEBUG: Deleted {deleted_count} of {len(self.subtitle_files)} tracked subtitle files")
-
- # Method 2: Find newly created subtitle files by comparing with initial set
- try:
- new_subtitle_files = set()
- for root, dirs, files in os.walk(self.path):
- for file in files:
- if file.endswith('.vtt') or file.endswith('.srt'):
- full_path = os.path.join(root, file)
- if full_path not in self.initial_subtitle_files:
- new_subtitle_files.add(full_path)
-
- if new_subtitle_files:
- print(f"DEBUG: Found {len(new_subtitle_files)} new subtitle files to delete")
- for subtitle_file in new_subtitle_files:
- try:
- if os.path.isfile(subtitle_file):
- os.remove(subtitle_file)
- deleted_count += 1
- print(f"DEBUG: Deleted new subtitle file: {os.path.basename(subtitle_file)}")
- except Exception as e:
- print(f"Error deleting new subtitle file {subtitle_file}: {str(e)}")
- except Exception as e:
- print(f"Error in finding new subtitle files: {str(e)}")
-
- # Method 3: As a last resort, use timestamp-based approach for recently created files
- if self.last_file_path and deleted_count == 0:
- target_dir = os.path.dirname(self.last_file_path)
-
- # Look for subtitle files created in last 5 minutes
- now = time.time()
- for filename in os.listdir(target_dir):
- if filename.endswith('.vtt') or filename.endswith('.srt'):
- file_path = os.path.join(target_dir, filename)
-
- # Check if it was created in the last 5 minutes
- file_time = os.path.getctime(file_path)
- if now - file_time < 300: # 5 minutes
- try:
- os.remove(file_path)
- deleted_count += 1
- print(f"DEBUG: Deleted subtitle file by timestamp: {filename}")
- except Exception as e:
- print(f"Error deleting subtitle file {filename}: {str(e)}")
-
- print(f"DEBUG: Total subtitle files deleted: {deleted_count}")
-
- except Exception as e:
- print(f"Error cleaning subtitle files: {str(e)}")
-
def check_file_exists(self):
"""Check if the file already exists before downloading"""
try:
print("DEBUG: Starting file existence check")
- # Use yt-dlp to get the filename without downloading, suppressing warnings
- ydl_opts_check = {
- 'quiet': True,
- 'skip_download': True,
- 'no_warnings': True, # <-- Suppress warnings during check
- 'ignoreerrors': True, # Also ignore other potential errors during this check
- 'outtmpl': {'default': os.path.join(self.path, '%(title)s.%(ext)s')},
- 'format': self.format_id if self.format_id else 'best' # Use selected format or best
- }
- if self.cookie_file:
- ydl_opts_check['cookiefile'] = self.cookie_file
-
- with yt_dlp.YoutubeDL(ydl_opts_check) as ydl:
+ # Use yt-dlp to get the filename without downloading
+ with yt_dlp.YoutubeDL({'quiet': True, 'skip_download': True}) as ydl:
info = ydl.extract_info(self.url, download=False)
- # Handle cases where info extraction fails silently
- if not info:
- print("DEBUG: Failed to extract info during file existence check. Skipping check.")
- return False # Proceed with download attempt
-
# Get the title and sanitize it for filename
title = info.get('title', 'video')
# Don't remove colons and other special characters yet
@@ -205,147 +106,10 @@ class DownloadThread(QThread):
traceback.print_exc()
return None
- def _build_yt_dlp_command(self):
- """Build the yt-dlp command line with all options for direct execution."""
- cmd = ["yt-dlp"]
-
- # Format selection strategy - use format ID if provided or fallback to resolution
- if self.format_id:
- # Strip the -drc suffix if present to fix issues with certain audio formats
- clean_format_id = self.format_id.split('-drc')[0] if '-drc' in self.format_id else self.format_id
-
- # Check if this is an audio-only format
- is_audio_format = False
- try:
- ydl_opts = {
- 'quiet': True,
- 'no_warnings': True,
- 'skip_download': True,
- }
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
- info = ydl.extract_info(self.url, download=False)
- for fmt in info.get('formats', []):
- if fmt.get('format_id') == clean_format_id:
- if fmt.get('vcodec') == 'none' or 'audio only' in fmt.get('format_note', '').lower():
- is_audio_format = True
- print(f"DEBUG: Detected audio-only format for ID: {clean_format_id}")
- break
- except Exception as e:
- print(f"DEBUG: Error checking if format is audio-only: {e}")
-
- # For audio-only formats, don't try to merge with video
- if is_audio_format:
- cmd.extend(["-f", clean_format_id])
- print(f"DEBUG: Using audio-only format selection: {clean_format_id}")
- else:
- cmd.extend(["-f", f"{clean_format_id}+bestaudio/best"])
- print(f"DEBUG: Using video format selection with audio: {clean_format_id}+bestaudio/best")
-
- # Determine output format based on the selected format ID - only for video formats
- if not is_audio_format:
- try:
- format_ext = None
- print(f"DEBUG: Getting format information for format ID: {self.format_id} (using: {clean_format_id})")
- ydl_opts = {
- 'quiet': True,
- 'no_warnings': True,
- 'skip_download': True,
- }
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
- info = ydl.extract_info(self.url, download=False)
- # Look for the clean format ID first
- for fmt in info.get('formats', []):
- if fmt.get('format_id') == clean_format_id:
- format_ext = fmt.get('ext')
- break
- # If not found, try the original ID as fallback
- if not format_ext:
- for fmt in info.get('formats', []):
- if fmt.get('format_id') == self.format_id:
- format_ext = fmt.get('ext')
- break
-
- if format_ext:
- print(f"DEBUG: Detected format extension: {format_ext}")
- # Ensure output matches the selected format - only for video formats
- cmd.extend(["--merge-output-format", format_ext])
- except Exception as e:
- print(f"DEBUG: Error detecting format extension: {e}")
- # If we can't determine the format, don't specify merge-output-format
- pass
- else:
- # If no specific format ID, use resolution-based sorting (-S)
- res_value = self.resolution if self.resolution else "720" # Default to 720p if no resolution specified
- cmd.extend(["-S", f"res:{res_value}"])
-
- # Output template with resolution in filename
- output_template = os.path.join(self.path, '%(title)s_%(resolution)s.%(ext)s')
-
- # Handle playlist directory creation if needed
- if self.is_playlist:
- # Create output template with playlist subfolder
- output_template = os.path.join(self.path, '%(playlist_title)s/%(title)s_%(resolution)s.%(ext)s')
-
- cmd.extend(["-o", output_template])
-
- # Add common options
- cmd.append("--force-overwrites")
-
- # Add playlist items if specified
- if self.is_playlist and self.playlist_items:
- cmd.extend(["--playlist-items", self.playlist_items])
-
- # Add subtitle options if selected
- if self.subtitle_langs:
- # Subtitles work with both audio-only and video formats
- # For audio-only formats, subtitles will be downloaded as separate files
- cmd.append("--write-subs")
-
- # Get language codes from subtitle selections
- lang_codes = []
- for sub_selection in self.subtitle_langs:
- try:
- # Extract just the language code (e.g., 'en' from 'en - Manual')
- lang_code = sub_selection.split(' - ')[0]
- lang_codes.append(lang_code)
- except Exception as e:
- print(f"Warning: Could not parse subtitle selection '{sub_selection}': {e}")
-
- if lang_codes:
- cmd.extend(["--sub-langs", ",".join(lang_codes)])
- cmd.append("--write-auto-subs") # Include auto-generated subtitles
-
- # Add embedding if requested - only applies to video formats
- if self.merge_subs:
- cmd.append("--embed-subs")
-
- # Add SponsorBlock if enabled
- if self.enable_sponsorblock:
- cmd.append("--sponsorblock-remove")
- cmd.append("sponsor")
-
- # Add description saving if enabled
- if self.save_description:
- cmd.append("--write-description")
-
- # Add cookies if specified
- if self.cookie_file:
- cmd.extend(["--cookies", self.cookie_file])
-
- # Add rate limit if specified
- if self.rate_limit:
- cmd.extend(["-r", self.rate_limit])
-
- # Add the URL as the final argument
- cmd.append(self.url)
-
- return cmd
-
def run(self):
try:
print("DEBUG: Starting download thread")
-
- # First check if file already exists using original method
+ # First check if file already exists
existing_file = self.check_file_exists()
if existing_file:
print(f"DEBUG: File exists, emitting signal: {existing_file}")
@@ -353,305 +117,216 @@ class DownloadThread(QThread):
return
print("DEBUG: No existing file found, proceeding with download")
-
- # Get initial list of subtitle files to compare later
- self.initial_subtitle_files = set()
- if self.merge_subs:
+ class DebugLogger:
+ def debug(self, msg):
+ # Print all debug messages to help diagnose issues
+ print(f"YT-DLP DEBUG: {msg}")
+
+ # Check for file exists message - look for both patterns
+ if "already exists" in msg or "has already been downloaded" in msg:
+ print(f"FILE EXISTS DETECTED: {msg}")
+ # Try to extract the filename
+ import re
+ match = re.search(r'File (.*?) already exists', msg)
+ if not match:
+ match = re.search(r'(.*?) has already been downloaded', msg)
+
+ if match:
+ filename = os.path.basename(match.group(1))
+ self.thread.file_exists_signal.emit(filename)
+ raise Exception("FileExistsError")
+
+ # Add detection of post-processing messages
+ if "Downloading" in msg:
+ self.thread.status_signal.emit("⚡ Downloading video...")
+ 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']):
+ self.thread.status_signal.emit("🔍 Fetching video information...")
+ self.thread.progress_signal.emit(0)
+ elif 'extracting' in msg.lower():
+ self.thread.status_signal.emit("📦 Extracting video data...")
+ self.thread.progress_signal.emit(0)
+ elif 'downloading m3u8' in msg.lower():
+ self.thread.status_signal.emit("🎯 Preparing video streams...")
+ self.thread.progress_signal.emit(0)
+
+ def warning(self, msg):
+ print(f"YT-DLP WARNING: {msg}")
+ self.thread.status_signal.emit(f"⚠️ Warning: {msg}")
+ # Also check for file exists in warnings
+ if "already exists" in msg or "has already been downloaded" in msg:
+ print(f"FILE EXISTS DETECTED IN WARNING: {msg}")
+ import re
+ match = re.search(r'File (.*?) already exists', msg)
+ if not match:
+ match = re.search(r'(.*?) has already been downloaded', msg)
+
+ if match:
+ filename = os.path.basename(match.group(1))
+ self.thread.file_exists_signal.emit(filename)
+ raise Exception("FileExistsError")
+
+ def error(self, msg):
+ print(f"YT-DLP ERROR: {msg}")
+ self.thread.status_signal.emit(f"❌ Error: {msg}")
+ # Also check for file exists in errors
+ if "already exists" in msg or "has already been downloaded" in msg:
+ print(f"FILE EXISTS DETECTED IN ERROR: {msg}")
+ import re
+ match = re.search(r'File (.*?) already exists', msg)
+ if not match:
+ match = re.search(r'(.*?) has already been downloaded', msg)
+
+ if match:
+ filename = os.path.basename(match.group(1))
+ self.thread.file_exists_signal.emit(filename)
+ raise Exception("FileExistsError")
+
+ 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!")
+
+ # Get the extension from the format_id
+ with yt_dlp.YoutubeDL({'quiet': True}) as ydl:
try:
- # Scan for existing subtitle files in the directory
- for root, dirs, files in os.walk(self.path):
- for file in files:
- if file.endswith('.vtt') or file.endswith('.srt'):
- self.initial_subtitle_files.add(os.path.join(root, file))
- print(f"DEBUG: Found {len(self.initial_subtitle_files)} existing subtitle files before download")
+ info = ydl.extract_info(self.url, download=False)
+ selected_format = next(
+ f for f in info['formats']
+ if str(f.get('format_id', '')) == self.format_id
+ )
+ output_ext = selected_format.get('ext', 'mp4')
except Exception as e:
- print(f"Warning: Error scanning for initial subtitle files: {e}")
-
- if self.use_direct_command:
- # Use direct CLI command instead of Python API
- self._run_direct_command()
- else:
- # Original method using Python API - code left for reference
- self._run_python_api()
-
- except Exception as e:
- # Catch errors during setup
- self.error_signal.emit(f"Critical error in download thread: {str(e)}")
- import traceback
- traceback.print_exc()
-
- def _run_direct_command(self):
- """Run yt-dlp as a direct command line process instead of using Python API."""
- try:
- cmd = self._build_yt_dlp_command()
- cmd_str = " ".join(shlex.quote(str(arg)) for arg in cmd)
- print(f"DEBUG: Executing command: {cmd_str}")
-
- self.status_signal.emit("🚀 Starting download...")
- self.progress_signal.emit(0)
-
- # Start the process
- # Add creationflags=subprocess.CREATE_NO_WINDOW to hide console on Windows
- creation_flags = 0
- if os.name == 'nt': # Only use flag on Windows
- creation_flags = subprocess.CREATE_NO_WINDOW
-
- self.process = subprocess.Popen(
- cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- text=True,
- bufsize=1, # Line buffered
- universal_newlines=True,
- creationflags=creation_flags # Add this flag
- )
-
- # Process output line by line to update progress
- for line in iter(self.process.stdout.readline, ''):
- if self.cancelled:
- self.process.terminate()
- self.cleanup_partial_files()
- self.status_signal.emit("Download cancelled")
+ self.error_signal.emit(f"Failed to get video information: {str(e)}")
return
+
+ # 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': 'mp4',
+ 'logger': DebugLogger(self),
+ 'postprocessors': [{
+ 'key': 'FFmpegVideoConvertor',
+ 'preferedformat': 'mp4'
+ }],
+ 'force_overwrites': True
+ }
+
+ # 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,
+ })
- # Wait if paused
- while self.paused and not self.cancelled:
- time.sleep(0.1)
-
- # Parse the line for download progress and status updates
- self._parse_output_line(line)
-
- # Wait for process to complete
- return_code = self.process.wait()
-
- if return_code == 0:
- self.progress_signal.emit(100)
- self.status_signal.emit("✅ Download completed!")
-
- # Clean up subtitle files if they were merged, with a small delay
- # to ensure the embedding process has completed
if self.merge_subs:
- # Add a significant delay to ensure ffmpeg has released all file handles
- # and any post-processing is complete
- self.status_signal.emit("✅ Download completed! Cleaning up...")
- time.sleep(3) # Increased delay to 3 seconds
- self.cleanup_subtitle_files()
-
- self.finished_signal.emit()
- else:
- # Check if it was cancelled
- if self.cancelled:
- self.status_signal.emit("Download cancelled")
- else:
- self.error_signal.emit(f"Download failed with return code {return_code}")
+ ydl_opts['postprocessors'].extend([
+ {
+ 'key': 'FFmpegSubtitlesConvertor',
+ 'format': 'srt',
+ },
+ {
+ 'key': 'FFmpegEmbedSubtitle',
+ 'already_have_subtitle': False,
+ }
+ ])
+
+ # 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
+ }])
+
+ # Add playlist items if specified
+ if self.playlist_items:
+ ydl_opts['playlist_items'] = self.playlist_items
+
+ try:
+ # Download the video
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
+ ydl.download([self.url])
+
+ if not self.cancelled:
+ 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("Download cancelled")
+ else:
+ self.error_signal.emit(f"Download failed: {str(e)}")
+
except Exception as e:
- self.error_signal.emit(f"Error in direct command: {str(e)}")
- self.cleanup_partial_files()
-
- def _parse_output_line(self, line):
- """Parse yt-dlp command output to update progress and status."""
- line = line.strip()
- # print(f"yt-dlp: {line}") # Log all output - OPTIONALLY UNCOMMENT FOR VERBOSE DEBUG
-
- # Extract filename when the destination line appears
- # Use a slightly more robust regex looking for the start of the line
- dest_match = re.search(r'^\[download\] Destination:\s*(.*)', line)
- if dest_match:
- try:
- filepath = dest_match.group(1).strip()
- self.current_filename = os.path.basename(filepath)
- self.last_file_path = filepath # Store the full path for later cleanup
- print(f"DEBUG: Extracted filename: {self.current_filename}") # DEBUG
-
- # Check if this is an audio-only download by looking in the previous lines
- is_audio_download = False
-
- # Look for audio format indicators in the current line or preceding output
- # yt-dlp typically mentions format like "Downloading format 251 - audio only"
- if ' - audio only' in line:
- is_audio_download = True
- # Check if the format ID is mentioned earlier in the line
- format_match = re.search(r'Downloading format (\d+)', line)
- if format_match:
- format_id = format_match.group(1)
- print(f"DEBUG: Detected format ID: {format_id}")
- # Format IDs for audio typically have different patterns
- # (like 140, 251 for audio vs 137, 248 for video)
- # This is just a heuristic since format IDs can vary
-
- # Determine file type based on extension and context
- ext = os.path.splitext(self.current_filename)[1].lower()
-
- # Check if this is explicitly an audio stream download
- if is_audio_download or 'Downloading audio' in line:
- self.status_signal.emit(f"⏬ Downloading audio...")
- # Video file extensions with likely video content
- elif ext in ['.mp4', '.webm', '.mkv', '.avi', '.mov', '.flv']:
- self.status_signal.emit(f"⏬ Downloading video...")
- # Audio file extensions
- elif ext in ['.mp3', '.m4a', '.aac', '.wav', '.ogg', '.opus', '.flac']:
- self.status_signal.emit(f"⏬ Downloading audio...")
- # Subtitle file extensions
- elif ext in ['.vtt', '.srt', '.ass', '.ssa']:
- self.status_signal.emit(f"⏬ Downloading subtitle...")
- # Default case
- else:
- self.status_signal.emit(f"⏬ Downloading...")
- except Exception as e:
- print(f"Error extracting filename from line '{line}': {e}")
- self.status_signal.emit("⚡ Downloading...") # Fallback status
- return # Don't process this line further for speed/ETA
-
- # Check for specific download types in the output
- if "Downloading video" in line:
- self.status_signal.emit(f"⏬ Downloading video...")
- return
-
- elif "Downloading audio" in line:
- self.status_signal.emit(f"⏬ Downloading audio...")
- return
-
- # Detect subtitle file creation
- # Look for lines like "[info] Writing video subtitles to: filename.xx.vtt"
- subtitle_match = re.search(r'(?:Writing|Downloading) (?:video )?subtitles.*?(?:to|:)\s*(.*\.(?:vtt|srt))', line, re.IGNORECASE)
- if subtitle_match:
- subtitle_file = subtitle_match.group(1).strip()
- # Show subtitle download message
- self.status_signal.emit(f"⏬ Downloading subtitle...")
- # Store the subtitle file path for later deletion if merging is enabled
- if self.merge_subs:
- if not os.path.isabs(subtitle_file):
- # If it's a relative path, make it absolute based on current path
- subtitle_file = os.path.join(self.path, subtitle_file)
- self.subtitle_files.append(subtitle_file)
- print(f"DEBUG: Tracking subtitle file for later cleanup: {subtitle_file}")
- return
-
- # Send status updates based on output line content
- if 'Downloading webpage' in line or 'Extracting URL' in line:
- self.status_signal.emit("🔍 Fetching video information...")
- self.progress_signal.emit(0)
- elif 'Downloading API JSON' in line:
- self.status_signal.emit("📋 Processing playlist data...")
- self.progress_signal.emit(0)
- elif 'Downloading m3u8 information' in line:
- self.status_signal.emit("🎯 Preparing video streams...")
- self.progress_signal.emit(0)
- elif '[download] Downloading video ' in line:
- self.status_signal.emit("⏬ Downloading video...")
- elif '[download] Downloading audio ' in line:
- self.status_signal.emit("⏬ Downloading audio...")
- elif 'Downloading format' in line:
- # Try to detect if it's audio or video format
- if ' - audio only' in line:
- self.status_signal.emit("⏬ Downloading audio...")
- elif ' - video only' in line:
- self.status_signal.emit("⏬ Downloading video...")
- else:
- # Don't emit generic message - format is unclear
- pass
-
- # Look for download percentage
- percent_match = re.search(r'(\d+\.\d+)%', line)
- if percent_match:
- try:
- percent = float(percent_match.group(1))
- self.progress_signal.emit(percent)
- except (ValueError, IndexError):
- pass
-
- # Check for download speed and ETA
- if '[download]' in line and '%' in line:
- # Try to extract more detailed status info
- try:
- # Look for speed
- speed_match = re.search(r'at\s+(\d+\.\d+[KMG]iB/s)', line)
- speed_str = speed_match.group(1) if speed_match else "N/A"
-
- # Look for ETA
- eta_match = re.search(r'ETA\s+(\d+:\d+)', line)
- eta_str = eta_match.group(1) if eta_match else "N/A"
-
- # Simplify status message to only show the speed and ETA
- status = f"Speed: {speed_str} | ETA: {eta_str}"
- self.update_details.emit(status)
- except Exception as e:
- # If parsing fails, just show basic status (maybe log the error)
- print(f"Error parsing download details line: {line} -> {e}")
- pass # Keep basic status emission below if needed, or emit generic details
-
- # Check for post-processing
- if '[Merger]' in line or 'Merging formats' in line:
- self.status_signal.emit("✨ Post-processing: Merging formats...")
- self.progress_signal.emit(95)
- elif 'SponsorBlock' in line:
- self.status_signal.emit("✨ Post-processing: Removing sponsor segments...")
- self.progress_signal.emit(97)
- elif 'Deleting original file' in line:
- self.progress_signal.emit(98)
- elif 'has already been downloaded' in line:
- # File already exists - extract filename
- match = re.search(r'(.*?) has already been downloaded', line)
- if match:
- filename = os.path.basename(match.group(1))
- # Determine file type based on extension for existing file message
- ext = os.path.splitext(filename)[1].lower()
-
- if ext in ['.mp4', '.webm', '.mkv', '.avi', '.mov', '.flv']:
- self.status_signal.emit(f"⚠️ Video file already exists")
- elif ext in ['.mp3', '.m4a', '.aac', '.wav', '.ogg', '.opus', '.flac']:
- self.status_signal.emit(f"⚠️ Audio file already exists")
- elif ext in ['.vtt', '.srt', '.ass', '.ssa']:
- self.status_signal.emit(f"⚠️ Subtitle file already exists")
- else:
- self.status_signal.emit(f"⚠️ File already exists")
-
- self.file_exists_signal.emit(filename)
- else:
- print(f"Could not extract filename from 'already downloaded' line: {line}")
- self.status_signal.emit("⚠️ File already exists") # Fallback status
- elif 'Finished downloading' in line:
- self.progress_signal.emit(100)
-
- # Show completion message based on file type
- if self.current_filename:
- ext = os.path.splitext(self.current_filename)[1].lower()
-
- # Video file extensions
- if ext in ['.mp4', '.webm', '.mkv', '.avi', '.mov', '.flv']:
- self.status_signal.emit(f"✅ Video download completed!")
- # Audio file extensions
- elif ext in ['.mp3', '.m4a', '.aac', '.wav', '.ogg', '.opus', '.flac']:
- self.status_signal.emit(f"✅ Audio download completed!")
- # Subtitle file extensions
- elif ext in ['.vtt', '.srt', '.ass', '.ssa']:
- self.status_signal.emit(f"✅ Subtitle download completed!")
- # Default case
- else:
- self.status_signal.emit("✅ Download completed!")
- else:
- self.status_signal.emit("✅ Download completed!")
-
- self.update_details.emit("") # Clear details label on completion
-
- def _run_python_api(self):
- """Original download method using Python API - kept for reference."""
- # The existing run method code using yt_dlp.YoutubeDL starts here
- # This method is no longer used by default
-
- def pause(self):
- self.paused = True
-
- def resume(self):
- self.paused = False
-
- def cancel(self):
- self.cancelled = True
- # Terminate the subprocess if it's running
- if self.process:
- try:
- self.process.terminate()
- except Exception:
- pass
\ No newline at end of file
+ self.error_signal.emit(f"Critical error: {str(e)}")
\ No newline at end of file
diff --git a/ytsage_gui_dialogs.py b/ytsage_gui_dialogs.py
index 592b7cc..5c1a69c 100644
--- a/ytsage_gui_dialogs.py
+++ b/ytsage_gui_dialogs.py
@@ -4,8 +4,7 @@ import webbrowser
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLineEdit, QPushButton, QTableWidget,
QTableWidgetItem, QProgressBar, QLabel, QFileDialog,
- QHeaderView, QStyle, QStyleFactory, QComboBox, QTextEdit, QDialog, QPlainTextEdit, QCheckBox, QButtonGroup, QListWidget,
- QListWidgetItem, QDialogButtonBox, QScrollArea, QGroupBox)
+ QHeaderView, QStyle, QStyleFactory, QComboBox, QTextEdit, QDialog, QPlainTextEdit, QCheckBox, QButtonGroup)
from PySide6.QtCore import Qt, Signal, QObject, QThread, QProcess
from PySide6.QtGui import QIcon, QPalette, QColor, QPixmap
import requests
@@ -76,9 +75,9 @@ class CustomCommandDialog(QDialog):
self.command_input.setPlaceholderText("Enter yt-dlp arguments...")
self.command_input.setStyleSheet("""
QPlainTextEdit {
- background-color: #1d1e22;
+ background-color: #363636;
color: #ffffff;
- border: 2px solid #1d1e22;
+ border: 2px solid #3d3d3d;
border-radius: 4px;
padding: 8px;
font-family: Consolas, monospace;
@@ -97,17 +96,17 @@ class CustomCommandDialog(QDialog):
QCheckBox::indicator {
width: 18px;
height: 18px;
- border-radius: 9px;
+ border-radius: 9px; /* Make indicator round */
}
QCheckBox::indicator:unchecked {
border: 2px solid #666666;
- background: #1d1e22;
- border-radius: 9px;
+ background: #2b2b2b;
+ border-radius: 9px; /* Make indicator round */
}
QCheckBox::indicator:checked {
- border: 2px solid #c90000;
- background: #c90000;
- border-radius: 9px;
+ border: 2px solid #ff0000;
+ background: #ff0000;
+ border-radius: 9px; /* Make indicator round */
}
""")
layout.insertWidget(layout.indexOf(self.command_input), self.sponsorblock_checkbox)
@@ -130,9 +129,9 @@ class CustomCommandDialog(QDialog):
self.log_output.setReadOnly(True)
self.log_output.setStyleSheet("""
QTextEdit {
- background-color: #1d1e22;
+ background-color: #2b2b2b;
color: #ffffff;
- border: 2px solid #1d1e22;
+ border: 2px solid #3d3d3d;
border-radius: 4px;
padding: 8px;
font-family: Consolas, monospace;
@@ -143,18 +142,18 @@ class CustomCommandDialog(QDialog):
self.setStyleSheet("""
QDialog {
- background-color: #15181b;
+ background-color: #2b2b2b;
}
QPushButton {
padding: 8px 15px;
- background-color: #c90000;
+ background-color: #ff0000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
}
QPushButton:hover {
- background-color: #a50000;
+ background-color: #cc0000;
}
""")
@@ -399,130 +398,6 @@ class FFmpegCheckDialog(QDialog):
self.close_btn.setEnabled(True)
-class VersionCheckThread(QThread):
- finished = Signal(str, str, str) # current_version, latest_version, error_message
-
- def run(self):
- current_version = ""
- latest_version = ""
- error_message = ""
-
- try:
- # Get the yt-dlp executable path
- if getattr(sys, 'frozen', False):
- if sys.platform == 'win32':
- yt_dlp_path = os.path.join(os.path.dirname(sys.executable), 'yt-dlp.exe')
- else:
- yt_dlp_path = os.path.join(os.path.dirname(sys.executable), 'yt-dlp')
- else:
- yt_dlp_path = 'yt-dlp'
-
- # Get current version
- try:
- result = subprocess.run([yt_dlp_path, '--version'],
- capture_output=True,
- text=True,
- startupinfo=None if sys.platform != 'win32' else subprocess.STARTUPINFO(dwFlags=subprocess.STARTF_USESHOWWINDOW, wShowWindow=subprocess.SW_HIDE), # Hide console window on Windows
- creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0) # Hide console window on Windows
- if result.returncode == 0:
- current_version = result.stdout.strip()
- else: # Try fallback if command failed
- import yt_dlp
- current_version = yt_dlp.version.__version__
- except Exception:
- # Fallback to importing yt_dlp package directly if subprocess fails
- try:
- import yt_dlp
- current_version = yt_dlp.version.__version__
- except ImportError:
- error_message = "yt-dlp not found or accessible."
- self.finished.emit(current_version, latest_version, error_message)
- return
-
-
- # Get latest version from PyPI
- response = requests.get("https://pypi.org/pypi/yt-dlp/json", timeout=10) # Add timeout
- response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
- latest_version = response.json()["info"]["version"]
-
- # Clean up version strings
- current_version = current_version.replace('_', '.')
- latest_version = latest_version.replace('_', '.')
-
- except requests.RequestException as e:
- error_message = f"Network error checking PyPI: {e}"
- except Exception as e:
- error_message = f"Error checking version: {e}"
-
- self.finished.emit(current_version, latest_version, error_message)
-
-
-class UpdateThread(QThread):
- update_status = Signal(str) # For status messages
- update_finished = Signal(bool, str) # success (bool), message/error (str)
-
- def run(self):
- error_message = ""
- success = False
- try:
- self.update_status.emit("Starting update process...")
-
- # Determine paths (similar logic as before)
- python_path = sys.executable # Default to current interpreter
- yt_dlp_dir = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.getcwd()
-
- if getattr(sys, 'frozen', False) and sys.platform == 'win32':
- alt_python_path = os.path.join(os.path.dirname(sys.executable), 'python.exe')
- if os.path.exists(alt_python_path):
- python_path = alt_python_path
-
- # Create and configure QProcess
- process = QProcess()
- process.setWorkingDirectory(yt_dlp_dir)
- process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels) # Combine stdout/stderr
-
- # Prepare command arguments
- pip_args = ['install', '--upgrade', '--no-cache-dir', 'yt-dlp']
- if sys.platform == 'win32':
- command = python_path
- args = ['-m', 'pip'] + pip_args
- else:
- # Assume pip is in PATH or use python -m pip for robustness
- command = python_path
- args = ['-m', 'pip'] + pip_args
- # Alternative if pip is guaranteed in PATH: command = 'pip', args = pip_args
-
- # Start the process
- self.update_status.emit(f"Running: {command} {' '.join(args)}")
- process.start(command, args)
-
- # Wait for finish (use QProcess event loop, not blocking waitForFinished)
- if not process.waitForStarted(5000): # Wait 5s for process to start
- raise RuntimeError("Update process failed to start.")
-
- if not process.waitForFinished(-1): # Wait indefinitely for finish
- raise RuntimeError("Update process failed to finish.")
-
- exit_code = process.exitCode()
- output = process.readAll().data().decode(errors='ignore') # Read combined output
-
- if exit_code == 0:
- self.update_status.emit("Update completed successfully!")
- success = True
- error_message = "Update successful. Please restart the application."
- else:
- self.update_status.emit(f"Update failed (Exit Code: {exit_code})")
- error_message = f"Update failed.\nExit Code: {exit_code}\nOutput:\n{output}"
- success = False
-
- except Exception as e:
- error_message = f"Update failed with exception: {e}"
- self.update_status.emit(error_message)
- success = False
-
- self.update_finished.emit(success, error_message)
-
-
class YTDLPUpdateDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
@@ -557,7 +432,7 @@ class YTDLPUpdateDialog(QDialog):
# Style
self.setStyleSheet("""
QDialog {
- background-color: #15181b;
+ background-color: #2b2b2b;
}
QLabel {
color: #ffffff;
@@ -566,7 +441,7 @@ class YTDLPUpdateDialog(QDialog):
}
QPushButton {
padding: 8px 15px;
- background-color: #c90000;
+ background-color: #ff0000;
border: none;
border-radius: 4px;
color: white;
@@ -577,723 +452,222 @@ class YTDLPUpdateDialog(QDialog):
background-color: #666666;
}
QPushButton:hover {
- background-color: #a50000;
+ background-color: #cc0000;
}
QProgressBar {
- border: 2px solid #1d1e22;
+ border: 2px solid #3d3d3d;
border-radius: 4px;
text-align: center;
color: white;
- background-color: #1d1e22;
+ background-color: #363636;
height: 25px;
}
QProgressBar::chunk {
- background-color: #c90000;
+ background-color: #ff0000;
border-radius: 2px;
}
""")
- # Start version check in background
+ # Start version check
self.check_version()
def check_version(self):
- self.status_label.setText("Checking for updates...")
- self.update_btn.setEnabled(False)
- self.version_check_thread = VersionCheckThread()
- self.version_check_thread.finished.connect(self.on_version_check_finished)
- self.version_check_thread.start()
-
- def on_version_check_finished(self, current_version, latest_version, error_message):
- if error_message:
- self.status_label.setText(error_message)
- self.update_btn.setEnabled(False)
- return
-
- if not current_version or not latest_version:
- self.status_label.setText("Could not determine versions.")
- self.update_btn.setEnabled(False)
- return
-
try:
- # Compare versions
- current_ver = version.parse(current_version)
- latest_ver = version.parse(latest_version)
+ # Get current version using yt-dlp command
+ import subprocess
+ import sys
+ import os
- if current_ver < latest_ver:
- self.status_label.setText(f"Update available!\nCurrent version: {current_version}\nLatest version: {latest_version}")
- self.update_btn.setEnabled(True)
+ # Get the yt-dlp executable path
+ if getattr(sys, 'frozen', False):
+ if sys.platform == 'win32':
+ yt_dlp_path = os.path.join(os.path.dirname(sys.executable), 'yt-dlp.exe')
+ else:
+ yt_dlp_path = os.path.join(os.path.dirname(sys.executable), 'yt-dlp')
else:
- self.status_label.setText(f"yt-dlp is up to date (version {current_version})")
- self.update_btn.setEnabled(False)
- except version.InvalidVersion:
- # If version parsing fails, do a simple string comparison
- if current_version != latest_version:
- self.status_label.setText(f"Update available! (Comparison failed)\nCurrent: {current_version}\nLatest: {latest_version}")
- self.update_btn.setEnabled(True)
- else:
- self.status_label.setText(f"yt-dlp is up to date (version {current_version})")
- self.update_btn.setEnabled(False)
- except Exception as e: # Catch any other unexpected errors during comparison
- self.status_label.setText(f"Error comparing versions: {e}")
- self.update_btn.setEnabled(False)
-
+ yt_dlp_path = 'yt-dlp'
+
+ # Get current version
+ try:
+ result = subprocess.run([yt_dlp_path, '--version'],
+ capture_output=True,
+ text=True)
+ current_version = result.stdout.strip()
+ except Exception:
+ import yt_dlp
+ current_version = yt_dlp.version.__version__
+
+ # Get latest version from PyPI
+ import requests
+ response = requests.get("https://pypi.org/pypi/yt-dlp/json")
+ latest_version = response.json()["info"]["version"]
+
+ # Clean up version strings
+ current_version = current_version.replace('_', '.')
+ latest_version = latest_version.replace('_', '.')
+
+ # Compare versions
+ from packaging import version
+ try:
+ current_ver = version.parse(current_version)
+ latest_ver = version.parse(latest_version)
+
+ if current_ver < latest_ver:
+ self.status_label.setText(f"Update available!\nCurrent version: {current_version}\nLatest version: {latest_version}")
+ self.update_btn.setEnabled(True)
+ else:
+ self.status_label.setText(f"yt-dlp is up to date (version {current_version})")
+ self.update_btn.setEnabled(False)
+ except version.InvalidVersion:
+ # If version parsing fails, do a simple string comparison
+ if current_version != latest_version:
+ self.status_label.setText(f"Update available!\nCurrent version: {current_version}\nLatest version: {latest_version}")
+ self.update_btn.setEnabled(True)
+ else:
+ self.status_label.setText(f"yt-dlp is up to date (version {current_version})")
+ self.update_btn.setEnabled(False)
+
+ except Exception as e:
+ self.status_label.setText(f"Error checking version: {str(e)}")
+ self.update_btn.setEnabled(False)
+
def perform_update(self):
- self.update_btn.setEnabled(False)
- self.close_btn.setEnabled(False)
- self.status_label.setText("Initializing update...")
- self.progress_bar.setRange(0, 0) # Indeterminate progress
- self.progress_bar.show()
-
- # Create and start the update thread
- self.update_thread = UpdateThread()
- self.update_thread.update_status.connect(self.on_update_status) # Connect status signal
- self.update_thread.update_finished.connect(self.on_update_finished) # Connect finished signal
- self.update_thread.start()
-
- def on_update_status(self, message):
- """Slot to receive status messages from UpdateThread."""
- self.status_label.setText(message)
-
- def on_update_finished(self, success, message):
- """Slot called when the UpdateThread finishes."""
- self.progress_bar.setRange(0, 100) # Set determinate range
- self.progress_bar.setValue(100) # Mark as complete
- self.progress_bar.hide() # Optionally hide progress bar again
- self.status_label.setText(message)
- self.close_btn.setEnabled(True)
-
- if success:
- # Optionally re-check version automatically after successful update
- self.check_version()
- else:
- # Re-enable update button only if failed?
- # self.update_btn.setEnabled(True) # Decide if appropriate
- pass # Keep update button disabled on failure for now
-
- def closeEvent(self, event):
- """Ensure threads are terminated if the dialog is closed prematurely."""
- if hasattr(self, 'version_check_thread') and self.version_check_thread.isRunning():
- self.version_check_thread.quit() # Ask thread to stop
- self.version_check_thread.wait() # Wait for it to finish
- if hasattr(self, 'update_thread') and self.update_thread.isRunning():
- self.update_thread.quit()
- self.update_thread.wait()
- super().closeEvent(event)
+ try:
+ self.update_btn.setEnabled(False)
+ self.close_btn.setEnabled(False)
+ self.status_label.setText("Updating yt-dlp...")
+ self.progress_bar.setRange(0, 0)
+ self.progress_bar.show()
+
+ # Get the yt-dlp path
+ if getattr(sys, 'frozen', False):
+ if sys.platform == 'win32':
+ yt_dlp_path = os.path.join(os.path.dirname(sys.executable), 'yt-dlp.exe')
+ else:
+ yt_dlp_path = os.path.join(os.path.dirname(sys.executable), 'yt-dlp')
+ else:
+ yt_dlp_path = 'yt-dlp'
+
+ # Create a QProcess for updating
+ process = QProcess()
+ process.setWorkingDirectory(os.path.dirname(yt_dlp_path))
+
+ if sys.platform == 'win32':
+ # For Windows, use pip to update
+ python_path = os.path.join(os.path.dirname(sys.executable), 'python.exe')
+ if not os.path.exists(python_path):
+ python_path = sys.executable
+
+ process.start(python_path, ['-m', 'pip', 'install', '--upgrade', '--no-cache-dir', 'yt-dlp'])
+ else:
+ # For Unix systems
+ process.start('pip', ['install', '--upgrade', '--no-cache-dir', 'yt-dlp'])
+
+ process.waitForFinished()
+
+ if process.exitCode() == 0:
+ self.status_label.setText("Update completed successfully!\nPlease restart the application.")
+ self.check_version() # Recheck version after update
+ else:
+ error = process.readAllStandardError().data().decode()
+ self.status_label.setText(f"Update failed: {error}")
+
+ self.progress_bar.setRange(0, 100)
+ self.progress_bar.setValue(100)
+ self.close_btn.setEnabled(True)
+
+ except Exception as e:
+ self.status_label.setText(f"Update failed: {str(e)}")
+ self.close_btn.setEnabled(True)
+ self.progress_bar.hide()
class AboutDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
- self.parent = parent # Store parent to access version etc.
- self.setWindowTitle("About YTSage")
- self.setMinimumWidth(450)
-
- layout = QVBoxLayout(self)
- layout.setSpacing(15)
- layout.setContentsMargins(20, 20, 20, 20)
+ self.setWindowTitle('About YTSage')
+ self.setMinimumWidth(500)
- # Title and Version
- title_label = QLabel("YTSage
")
+ layout = QVBoxLayout(self)
+
+ # App title and version
+ title_label = QLabel("YTSage")
+ title_label.setStyleSheet("""
+ font-size: 24px;
+ font-weight: bold;
+ color: #ff0000;
+ padding: 10px;
+ """)
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title_label)
- version_label = QLabel(f"Version: {getattr(self.parent, 'version', 'N/A')}")
+ version_label = QLabel(f"Version {parent.version}")
version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
- version_label.setStyleSheet("color: #cccccc;")
layout.addWidget(version_label)
# Description
- description_label = QLabel("A simple GUI frontend for the powerful yt-dlp video downloader.")
- description_label.setWordWrap(True)
- description_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
- description_label.setStyleSheet("color: #ffffff; padding-top: 10px;")
- layout.addWidget(description_label)
+ description = QLabel(
+ "A modern YouTube downloader with a clean interface.\n"
+ "Download videos in any quality, extract audio, fetch subtitles,\n"
+ "and view video metadata. Built with yt-dlp for reliable performance."
+ )
+ description.setWordWrap(True)
+ description.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ layout.addWidget(description)
- # Separator
- separator = QWidget()
- separator.setFixedHeight(1)
- separator.setStyleSheet("background-color: #1d1e22;")
- layout.addWidget(separator)
+ # Features list
+ features_label = QLabel(
+ "Key Features:\n"
+ "• Smart video quality selection\n"
+ "• Audio extraction\n"
+ "• Subtitle support\n"
+ "• Playlist downloads\n"
+ "• Real-time progress tracking\n"
+ "• Custom command support\n"
+ "• SponsorBlock Integration\n"
+ "• Save Thumbnail\n"
+ "• One-click updates"
+ )
+ features_label.setStyleSheet("padding: 10px;")
+ layout.addWidget(features_label)
- # Information Section
- info_layout = QVBoxLayout()
- info_layout.setSpacing(8)
+ # Credits
+ credits_label = QLabel(
+ "Powered by:\n"
+ "• yt-dlp\n"
+ "• PySide6\n"
+ "• FFmpeg"
+ )
+ credits_label.setStyleSheet("padding: 10px;")
+ layout.addWidget(credits_label)
- # Author
- author_label = QLabel("Created by: oop7")
- author_label.setOpenExternalLinks(True)
- info_layout.addWidget(author_label)
+ # GitHub link
+ github_btn = QPushButton("Visit GitHub Repository")
+ github_btn.clicked.connect(lambda: webbrowser.open('https://github.com/oop7/YTSage'))
+ layout.addWidget(github_btn)
- # GitHub Repo
- repo_label = QLabel("GitHub: github.com/oop7/YTSage")
- repo_label.setOpenExternalLinks(True)
- info_layout.addWidget(repo_label)
+ # Close button
+ close_btn = QPushButton("Close")
+ close_btn.clicked.connect(self.close)
+ layout.addWidget(close_btn)
- # yt-dlp path
- yt_dlp_path = get_yt_dlp_path()
- yt_dlp_path_text = yt_dlp_path if yt_dlp_path else 'yt-dlp not found in PATH'
- yt_dlp_label = QLabel(f"yt-dlp Path: {yt_dlp_path_text}")
- yt_dlp_label.setWordWrap(True)
- info_layout.addWidget(yt_dlp_label)
-
- # FFmpeg Status
- ffmpeg_found = check_ffmpeg()
- ffmpeg_status_text = "Detected" if ffmpeg_found else "Not Detected"
- ffmpeg_label = QLabel(f"FFmpeg Status: {ffmpeg_status_text}")
- info_layout.addWidget(ffmpeg_label)
-
- layout.addLayout(info_layout)
-
- # Close Button
- button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
- button_box.accepted.connect(self.accept)
- # Center the button box
- button_layout = QHBoxLayout()
- button_layout.addStretch()
- button_layout.addWidget(button_box)
- button_layout.addStretch()
- layout.addLayout(button_layout)
-
- # Apply overall styling
+ # Style the dialog
self.setStyleSheet("""
- QDialog { background-color: #15181b; color: #ffffff; }
- QLabel { color: #cccccc; }
+ QDialog {
+ background-color: #2b2b2b;
+ }
+ QLabel {
+ color: #ffffff;
+ font-size: 12px;
+ }
QPushButton {
- padding: 8px 25px;
- background-color: #c90000;
+ padding: 8px 15px;
+ background-color: #ff0000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
}
- QPushButton:hover { background-color: #a50000; }
- """)
-
-# --- New Subtitle Selection Dialog ---
-class SubtitleSelectionDialog(QDialog):
- def __init__(self, available_manual, available_auto, previously_selected, parent=None):
- super().__init__(parent)
- self.setWindowTitle("Select Subtitles")
- self.setMinimumWidth(400)
- self.setMinimumHeight(300)
-
- self.available_manual = available_manual
- self.available_auto = available_auto
- self.previously_selected = set(previously_selected) # Use a set for quick lookups
- self.selected_subtitles = list(previously_selected) # Initialize with previous selection
-
- layout = QVBoxLayout(self)
- layout.setSpacing(10)
-
- # Filter input
- self.filter_input = QLineEdit()
- self.filter_input.setPlaceholderText("Filter languages (e.g., en, es)...")
- self.filter_input.textChanged.connect(self.filter_list)
- self.filter_input.setStyleSheet("""
- QLineEdit {
- background-color: #363636;
- border: 2px solid #3d3d3d;
- border-radius: 4px;
- padding: 5px;
- min-height: 30px;
- color: white;
- }
- QLineEdit:focus {
- border-color: #ff0000;
- }
- """)
- layout.addWidget(self.filter_input)
-
- # Scroll Area for the list
- scroll_area = QScrollArea()
- scroll_area.setWidgetResizable(True)
- scroll_area.setStyleSheet("QScrollArea { border: none; }") # Remove border around scroll area
- layout.addWidget(scroll_area)
-
- # Container widget for list items (needed for scroll area)
- self.list_container = QWidget()
- self.list_layout = QVBoxLayout(self.list_container)
- self.list_layout.setContentsMargins(0, 0, 0, 0)
- self.list_layout.setSpacing(2) # Compact spacing
- self.list_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # Align items to top
- scroll_area.setWidget(self.list_container)
-
- # Populate the list initially
- self.populate_list()
-
- # OK and Cancel buttons
- button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- button_box.accepted.connect(self.accept)
- button_box.rejected.connect(self.reject)
-
- # Style the buttons
- for button in button_box.buttons():
- button.setStyleSheet("""
- QPushButton {
- background-color: #363636;
- border: 2px solid #3d3d3d;
- border-radius: 4px;
- padding: 5px 15px; /* Adjust padding */
- min-height: 30px; /* Ensure consistent height */
- color: white;
- }
- QPushButton:hover {
- background-color: #444444;
- }
- QPushButton:pressed {
- background-color: #555555;
- }
- """)
- # Style the OK button specifically if needed
- if button_box.buttonRole(button) == QDialogButtonBox.ButtonRole.AcceptRole:
- button.setStyleSheet(button.styleSheet() + "QPushButton { background-color: #ff0000; border-color: #cc0000; } QPushButton:hover { background-color: #cc0000; }")
-
-
- layout.addWidget(button_box)
-
- def populate_list(self, filter_text=""):
- # Clear existing checkboxes from layout
- while self.list_layout.count():
- item = self.list_layout.takeAt(0)
- widget = item.widget()
- if widget is not None:
- widget.deleteLater()
-
- filter_text = filter_text.lower()
- combined_subs = {}
-
- # Add manual subs
- for lang_code, sub_info in self.available_manual.items():
- if not filter_text or filter_text in lang_code.lower():
- combined_subs[lang_code] = f"{lang_code} - Manual"
-
- # Add auto subs (only if no manual exists and matches filter)
- for lang_code, sub_info in self.available_auto.items():
- if lang_code not in combined_subs: # Don't overwrite manual
- if not filter_text or filter_text in lang_code.lower():
- combined_subs[lang_code] = f"{lang_code} - Auto-generated"
-
- if not combined_subs:
- no_subs_label = QLabel("No subtitles available" + (f" matching '{filter_text}'" if filter_text else ""))
- no_subs_label.setStyleSheet("color: #aaaaaa; padding: 10px;")
- self.list_layout.addWidget(no_subs_label)
- return
-
- # Sort by language code
- sorted_lang_codes = sorted(combined_subs.keys())
-
- for lang_code in sorted_lang_codes:
- item_text = combined_subs[lang_code]
- checkbox = QCheckBox(item_text)
- checkbox.setProperty("subtitle_id", item_text) # Store the identifier
- checkbox.setChecked(item_text in self.previously_selected) # Check if previously selected
- checkbox.stateChanged.connect(self.update_selection)
- checkbox.setStyleSheet("""
- QCheckBox {
- color: #ffffff;
- padding: 5px;
- }
- QCheckBox::indicator {
- width: 18px;
- height: 18px;
- border-radius: 4px; /* Square checkboxes */
- }
- QCheckBox::indicator:unchecked {
- border: 2px solid #666666;
- background: #2b2b2b;
- }
- QCheckBox::indicator:checked {
- border: 2px solid #ff0000;
- background: #ff0000;
- }
- """)
- self.list_layout.addWidget(checkbox)
-
- self.list_layout.addStretch() # Pushes items up if list is short
-
- def filter_list(self):
- self.populate_list(self.filter_input.text())
-
- def update_selection(self, state):
- sender = self.sender()
- subtitle_id = sender.property("subtitle_id")
- if state == Qt.CheckState.Checked.value:
- if subtitle_id not in self.previously_selected:
- self.previously_selected.add(subtitle_id)
- else:
- if subtitle_id in self.previously_selected:
- self.previously_selected.remove(subtitle_id)
-
- def get_selected_subtitles(self):
- # Return the final set as a list
- return list(self.previously_selected)
-
- def accept(self):
- # Update the final list before closing
- self.selected_subtitles = self.get_selected_subtitles()
- super().accept()
-
-# --- End Subtitle Selection Dialog ---
-
-
-# --- Playlist Video Selection Dialog ---
-
-class PlaylistSelectionDialog(QDialog):
- def __init__(self, playlist_entries, previously_selected_string, parent=None):
- super().__init__(parent)
- self.setWindowTitle("Select Playlist Videos")
- self.setMinimumWidth(500)
- self.setMinimumHeight(400) # Allow more vertical space
-
- self.playlist_entries = playlist_entries
- self.checkboxes = []
-
- # Main layout
- main_layout = QVBoxLayout(self)
-
- # Top buttons (Select/Deselect All)
- button_layout = QHBoxLayout()
- select_all_btn = QPushButton("Select All")
- deselect_all_btn = QPushButton("Deselect All")
- select_all_btn.clicked.connect(self._select_all)
- deselect_all_btn.clicked.connect(self._deselect_all)
- # Style the buttons to match the subtitle dialog
- select_all_btn.setStyleSheet("""
- QPushButton {
- background-color: #363636;
- border: 2px solid #3d3d3d;
- border-radius: 4px;
- padding: 5px 15px;
- min-height: 30px;
- color: white;
- }
QPushButton:hover {
- background-color: #444444;
+ background-color: #cc0000;
}
- QPushButton:pressed {
- background-color: #555555;
- }
- """)
- deselect_all_btn.setStyleSheet(select_all_btn.styleSheet())
- button_layout.addWidget(select_all_btn)
- button_layout.addWidget(deselect_all_btn)
- button_layout.addStretch()
- main_layout.addLayout(button_layout)
-
- # Scrollable area for checkboxes
- scroll_area = QScrollArea()
- scroll_area.setWidgetResizable(True)
- scroll_area.setStyleSheet("QScrollArea { border: none; }") # Remove border around scroll area
- scroll_widget = QWidget()
- self.list_layout = QVBoxLayout(scroll_widget) # Layout for checkboxes
- self.list_layout.setContentsMargins(0, 0, 0, 0)
- self.list_layout.setSpacing(2) # Compact spacing
- self.list_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # Align items to top
- scroll_area.setWidget(scroll_widget)
- main_layout.addWidget(scroll_area)
-
- # Populate checkboxes
- self._populate_list(previously_selected_string)
-
- # Dialog buttons (OK/Cancel)
- button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- button_box.accepted.connect(self.accept)
- button_box.rejected.connect(self.reject)
-
- # Style the buttons to match subtitle dialog
- for button in button_box.buttons():
- button.setStyleSheet("""
- QPushButton {
- background-color: #363636;
- border: 2px solid #3d3d3d;
- border-radius: 4px;
- padding: 5px 15px;
- min-height: 30px;
- color: white;
- }
- QPushButton:hover {
- background-color: #444444;
- }
- QPushButton:pressed {
- background-color: #555555;
- }
- """)
- # Style the OK button specifically if needed
- if button_box.buttonRole(button) == QDialogButtonBox.ButtonRole.AcceptRole:
- button.setStyleSheet(button.styleSheet() + "QPushButton { background-color: #ff0000; border-color: #cc0000; } QPushButton:hover { background-color: #cc0000; }")
-
- main_layout.addWidget(button_box)
-
- # Apply styling to match subtitle dialog
- self.setStyleSheet("""
- QDialog { background-color: #15181b; }
- QCheckBox {
- color: #ffffff;
- padding: 5px;
- }
- QCheckBox::indicator {
- width: 18px;
- height: 18px;
- border-radius: 4px;
- }
- QCheckBox::indicator:unchecked {
- border: 2px solid #666666;
- background: #2b2b2b;
- }
- QCheckBox::indicator:checked {
- border: 2px solid #ff0000;
- background: #ff0000;
- }
- QWidget { background-color: #15181b; }
- """)
-
- def _parse_selection_string(self, selection_string):
- """Parses a yt-dlp playlist selection string (e.g., '1-3,5,7-9') into a set of 1-based indices."""
- selected_indices = set()
- if not selection_string:
- # If no previous selection, assume all are selected initially
- return set(range(1, len(self.playlist_entries) + 1))
-
- parts = selection_string.split(',')
- for part in parts:
- part = part.strip()
- if '-' in part:
- try:
- start, end = map(int, part.split('-'))
- if start <= end:
- selected_indices.update(range(start, end + 1))
- except ValueError:
- pass # Ignore invalid ranges
- else:
- try:
- selected_indices.add(int(part))
- except ValueError:
- pass # Ignore invalid numbers
- return selected_indices
-
- def _populate_list(self, previously_selected_string):
- """Populates the scroll area with checkboxes for each video."""
- selected_indices = self._parse_selection_string(previously_selected_string)
-
- # Clear existing checkboxes if any (e.g., if repopulating)
- while self.list_layout.count():
- child = self.list_layout.takeAt(0)
- if child.widget():
- child.widget().deleteLater()
- self.checkboxes.clear()
-
- for index, entry in enumerate(self.playlist_entries):
- if not entry: continue # Skip None entries if yt-dlp returns them
-
- video_index = index + 1 # yt-dlp uses 1-based indexing
- title = entry.get('title', f'Video {video_index}')
- # Shorten title if too long
- display_title = (title[:70] + '...') if len(title) > 73 else title
-
- checkbox = QCheckBox(f"{video_index}. {display_title}")
- checkbox.setChecked(video_index in selected_indices)
- checkbox.setProperty("video_index", video_index) # Store index
- checkbox.setStyleSheet("""
- QCheckBox {
- color: #ffffff;
- padding: 5px;
- }
- QCheckBox::indicator {
- width: 18px;
- height: 18px;
- border-radius: 4px;
- }
- QCheckBox::indicator:unchecked {
- border: 2px solid #666666;
- background: #2b2b2b;
- }
- QCheckBox::indicator:checked {
- border: 2px solid #ff0000;
- background: #ff0000;
- }
- """)
- self.list_layout.addWidget(checkbox)
- self.checkboxes.append(checkbox)
- self.list_layout.addStretch() # Push checkboxes to the top
-
- def _select_all(self):
- for checkbox in self.checkboxes:
- checkbox.setChecked(True)
-
- def _deselect_all(self):
- for checkbox in self.checkboxes:
- checkbox.setChecked(False)
-
- def _condense_indices(self, indices):
- """Condenses a list of 1-based indices into a yt-dlp selection string."""
- if not indices:
- return ""
- indices = sorted(list(set(indices)))
- if not indices: # Check again after sorting/set conversion
- return ""
-
- ranges = []
- start = indices[0]
- end = indices[0]
- for i in range(1, len(indices)):
- if indices[i] == end + 1:
- end = indices[i]
- else:
- if start == end:
- ranges.append(str(start))
- else:
- ranges.append(f"{start}-{end}")
- start = indices[i]
- end = indices[i]
- # Add the last range
- if start == end:
- ranges.append(str(start))
- else:
- ranges.append(f"{start}-{end}")
- return ",".join(ranges)
-
- def get_selected_items_string(self):
- """Returns the selection string based on checked boxes."""
- selected_indices = [
- cb.property("video_index") for cb in self.checkboxes if cb.isChecked()
- ]
-
- # Check if all items are selected
- if len(selected_indices) == len(self.playlist_entries):
- return None # yt-dlp default is all items, so return None or empty string
-
- return self._condense_indices(selected_indices)
-
- # Optional: Override accept to ensure the string is generated, although not strictly necessary
- # def accept(self):
- # self._selected_string = self.get_selected_items_string()
- # super().accept()
-
-# --- End Playlist Video Selection Dialog ---
-
-class CookieLoginDialog(QDialog):
- def __init__(self, parent=None):
- super().__init__(parent)
- self.setWindowTitle('Login with Cookies')
- self.setMinimumSize(400, 150)
-
- layout = QVBoxLayout(self)
-
- help_text = QLabel(
- "Select the Netscape-format cookies file for logging in.\n"
- "This allows downloading of private videos and premium quality audio."
- )
- help_text.setWordWrap(True)
- help_text.setStyleSheet("color: #999999; padding: 10px;")
- layout.addWidget(help_text)
-
- # File path input and browse button
- path_layout = QHBoxLayout()
- self.cookie_path_input = QLineEdit()
- self.cookie_path_input.setPlaceholderText("Path to cookies file (Netscape format)")
- path_layout.addWidget(self.cookie_path_input)
-
- self.browse_button = QPushButton("Browse")
- self.browse_button.clicked.connect(self.browse_cookie_file)
- path_layout.addWidget(self.browse_button)
-
- layout.addLayout(path_layout)
-
- # Dialog buttons
- button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
- button_box.accepted.connect(self.accept)
- button_box.rejected.connect(self.reject)
- layout.addWidget(button_box)
-
- def browse_cookie_file(self):
- # Open file dialog to select cookie file
- file_dialog = QFileDialog(self)
- file_dialog.setFileMode(QFileDialog.ExistingFile)
- file_dialog.setNameFilter("Cookies files (*.txt *.lwp)") # Assuming common cookie file extensions
- if file_dialog.exec():
- selected_files = file_dialog.selectedFiles()
- if selected_files:
- self.cookie_path_input.setText(selected_files[0])
-
- def get_cookie_file_path(self):
- # Return the selected cookie file path
- return self.cookie_path_input.text()
-
-# === Renamed Dialog: Download Settings ===
-class DownloadSettingsDialog(QDialog): # Renamed class
- def __init__(self, current_path, current_limit, current_unit_index, parent=None): # Added limit params
- super().__init__(parent)
- self.setWindowTitle("Download Settings") # Renamed window
- self.setMinimumWidth(450)
- self.current_path = current_path
- self.current_limit = current_limit if current_limit is not None else "" # Handle None
- self.current_unit_index = current_unit_index
-
- layout = QVBoxLayout(self)
-
- # --- Download Path Section ---
- path_group_box = QGroupBox("Download Path")
- path_layout = QVBoxLayout()
-
- self.path_display = QLabel(self.current_path)
- self.path_display.setWordWrap(True)
- self.path_display.setStyleSheet("QLabel { color: #cccccc; padding: 5px; border: 1px solid #3d3d3d; border-radius: 4px; background-color: #363636; }")
- path_layout.addWidget(self.path_display)
-
- browse_button = QPushButton("Browse...")
- browse_button.clicked.connect(self.browse_new_path)
- path_layout.addWidget(browse_button)
-
- path_group_box.setLayout(path_layout)
- layout.addWidget(path_group_box)
- # --- End Path Section ---
-
- # --- Speed Limit Section ---
- speed_group_box = QGroupBox("Speed Limit")
- speed_layout = QHBoxLayout()
-
- self.speed_limit_input = QLineEdit(str(self.current_limit)) # Set initial value
- self.speed_limit_input.setPlaceholderText("None")
- speed_layout.addWidget(self.speed_limit_input)
-
- self.speed_limit_unit = QComboBox()
- self.speed_limit_unit.addItems(["KB/s", "MB/s"])
- self.speed_limit_unit.setCurrentIndex(self.current_unit_index) # Set initial unit
- speed_layout.addWidget(self.speed_limit_unit)
-
- speed_group_box.setLayout(speed_layout)
- layout.addWidget(speed_group_box)
- # --- End Speed Limit Section ---
-
- # Dialog buttons (OK/Cancel)
- button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- button_box.accepted.connect(self.accept)
- button_box.rejected.connect(self.reject)
- layout.addWidget(button_box)
-
- def browse_new_path(self):
- new_path = QFileDialog.getExistingDirectory(self, "Select Download Directory", self.current_path)
- if new_path:
- self.current_path = new_path
- self.path_display.setText(self.current_path)
-
- def get_selected_path(self):
- """Returns the confirmed path after the dialog is accepted."""
- return self.current_path
-
- def get_selected_speed_limit(self):
- """Returns the entered speed limit value (as string or None)."""
- limit_str = self.speed_limit_input.text().strip()
- if not limit_str:
- return None
- # Optional: Add validation to ensure it's a number
- try:
- float(limit_str) # Check if convertible to float
- return limit_str
- except ValueError:
- # Handle error? Or just return None? Returning None for simplicity.
- print("Invalid speed limit input in dialog")
- return None # Or raise an error / show message
-
- def get_selected_unit_index(self):
- """Returns the index of the selected speed limit unit."""
- return self.speed_limit_unit.currentIndex()
\ No newline at end of file
+ """)
\ No newline at end of file
diff --git a/ytsage_gui_format_table.py b/ytsage_gui_format_table.py
index 5f05fb6..d5b538d 100644
--- a/ytsage_gui_format_table.py
+++ b/ytsage_gui_format_table.py
@@ -19,9 +19,6 @@ class FormatTableMixin:
self.format_table.setColumnCount(8)
self.format_table.setHorizontalHeaderLabels(['Select', 'Quality', 'Extension', 'Resolution', 'File Size', 'Codec', 'Audio', 'Notes'])
- # Enable alternating row colors
- self.format_table.setAlternatingRowColors(True)
-
# Set specific column widths and resize modes
self.format_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed) # Select
self.format_table.setColumnWidth(0, 50) # Select column width
@@ -54,32 +51,25 @@ class FormatTableMixin:
self.format_table.setStyleSheet("""
QTableWidget {
- background-color: #1b2021;
- border: 2px solid #1b2021;
+ background-color: #363636;
+ border: 2px solid #3d3d3d;
border-radius: 4px;
- gridline-color: #1b2021;
+ gridline-color: #3d3d3d;
}
QTableWidget::item {
padding: 5px;
- border-bottom: 1px solid #1b2021;
+ border-bottom: 1px solid #3d3d3d;
}
QTableWidget::item:selected {
background-color: transparent;
}
QHeaderView::section {
- background-color: #15181b;
+ background-color: #2b2b2b;
padding: 5px;
- border: 1px solid #1b2021;
+ border: 1px solid #3d3d3d;
font-weight: bold;
color: white;
}
- /* Style alternating rows with more contrast */
- QTableWidget::item:alternate {
- background-color: #212529;
- }
- QTableWidget::item {
- background-color: #16191b;
- }
QCheckBox::indicator {
width: 16px;
height: 16px;
@@ -87,11 +77,11 @@ class FormatTableMixin:
}
QCheckBox::indicator:unchecked {
border: 2px solid #666666;
- background: #15181b;
+ background: #2b2b2b;
}
QCheckBox::indicator:checked {
- border: 2px solid #c90000;
- background: #c90000;
+ border: 2px solid #ff0000;
+ background: #ff0000;
}
QWidget {
background-color: transparent;
@@ -155,66 +145,21 @@ class FormatTableMixin:
def _update_format_table(self, formats):
self.format_table.setRowCount(0)
self.format_checkboxes.clear()
-
- is_playlist_mode = hasattr(self, 'is_playlist') and self.is_playlist
-
- # Configure columns based on mode
- if is_playlist_mode:
- self.format_table.setColumnCount(5)
- self.format_table.setHorizontalHeaderLabels(['Select', 'Quality', 'Resolution', 'Notes', 'Audio'])
-
- # Configure column visibility and resizing for playlist mode
- self.format_table.setColumnHidden(5, True)
- self.format_table.setColumnHidden(6, True)
- self.format_table.setColumnHidden(7, True)
-
- # Set specific resize modes for playlist columns
- self.format_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
- self.format_table.setColumnWidth(0, 50)
- self.format_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
- self.format_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
- self.format_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
- self.format_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.Stretch)
-
- else:
- self.format_table.setColumnCount(8)
- self.format_table.setHorizontalHeaderLabels(['Select', 'Quality', 'Extension', 'Resolution', 'File Size', 'Codec', 'Audio', 'Notes'])
- # Ensure all columns are visible
- for i in range(2, 8):
- self.format_table.setColumnHidden(i, False)
-
- # Reapply resize modes for non-playlist mode if needed (optional, might be okay without)
- self.format_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
- self.format_table.setColumnWidth(0, 50)
- self.format_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
- self.format_table.setColumnWidth(1, 100)
- self.format_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
- self.format_table.setColumnWidth(2, 80)
- self.format_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed)
- self.format_table.setColumnWidth(3, 100)
- self.format_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed)
- self.format_table.setColumnWidth(4, 100)
- self.format_table.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeMode.Fixed)
- self.format_table.setColumnWidth(5, 150)
- self.format_table.horizontalHeader().setSectionResizeMode(6, QHeaderView.ResizeMode.Fixed)
- self.format_table.setColumnWidth(6, 120)
- self.format_table.horizontalHeader().setSectionResizeMode(7, QHeaderView.ResizeMode.Stretch)
-
-
- # Find best quality format for recommendations (only needed for non-playlist mode notes)
- best_video_size = 0
- if not is_playlist_mode:
- best_video_size = max((f.get('filesize', 0) for f in formats if f.get('vcodec') != 'none'), default=0)
+
+ # Find best quality format for recommendations
+ best_video_size = max((f.get('filesize', 0) for f in formats if f.get('vcodec') != 'none'), default=0)
for f in formats:
row = self.format_table.rowCount()
self.format_table.insertRow(row)
- # Column 0: Select Checkbox (Always shown)
+ # Add checkbox
checkbox = QCheckBox()
checkbox.format_id = str(f.get('format_id', ''))
checkbox.clicked.connect(lambda checked, cb=checkbox: self.handle_checkbox_click(cb))
self.format_checkboxes.append(checkbox)
+
+ # Create a widget to center the checkbox
checkbox_widget = QWidget()
checkbox_widget.setStyleSheet("background-color: transparent;")
checkbox_layout = QHBoxLayout(checkbox_widget)
@@ -224,7 +169,7 @@ class FormatTableMixin:
checkbox_layout.setSpacing(0)
self.format_table.setCellWidget(row, 0, checkbox_widget)
- # Column 1: Quality (Always shown)
+ # Quality (replacing Format ID)
quality_text = self.get_quality_label(f)
quality_item = QTableWidgetItem(quality_text)
# Set color based on quality
@@ -238,73 +183,46 @@ class FormatTableMixin:
quality_item.setForeground(QColor('#ff5555')) # Red for low quality
self.format_table.setItem(row, 1, quality_item)
- # --- Populate columns common to both modes (Moved outside the 'if not is_playlist_mode' block) ---
-
- # Column 2: Resolution (Always shown)
+ # Extension
+ self.format_table.setItem(row, 2, QTableWidgetItem(f.get('ext', '').upper()))
+
+ # Resolution
resolution = f.get('resolution', 'N/A')
if f.get('vcodec') == 'none':
resolution = 'Audio only'
- self.format_table.setItem(row, 2, QTableWidgetItem(resolution))
-
- # Column 3: Notes for playlist mode, Extension for normal mode
- if is_playlist_mode:
- # Get notes for playlist mode
- notes = self.get_format_notes(f, 0) # We don't need best_video_size for simple notes
- notes_item = QTableWidgetItem(notes)
- if "✨ Recommended" in notes:
- notes_item.setForeground(QColor('#00ff00')) # Green for recommended
- elif "💾 Storage friendly" in notes:
- notes_item.setForeground(QColor('#00ccff')) # Blue for storage friendly
- elif "📱 Mobile friendly" in notes:
- notes_item.setForeground(QColor('#ff9900')) # Orange for mobile
- self.format_table.setItem(row, 3, notes_item)
+ self.format_table.setItem(row, 3, QTableWidgetItem(resolution))
+
+ # File Size
+ filesize = f"{f.get('filesize', 0) / 1024 / 1024:.2f} MB"
+ self.format_table.setItem(row, 4, QTableWidgetItem(filesize))
+
+ # Codec
+ if f.get('vcodec') == 'none':
+ codec = f.get('acodec', 'N/A')
else:
- # Extension for normal mode (column 2)
- self.format_table.setItem(row, 2, QTableWidgetItem(f.get('ext', '').upper()))
-
- # Column 4 in playlist mode, Column 6 in normal mode: Audio Status
- needs_audio = f.get('acodec') == 'none' and f.get('vcodec') != 'none' # Only mark video-only as needing merge
- audio_status = "Will merge audio" if needs_audio else ("✓ Has Audio" if f.get('vcodec') != 'none' else "Audio Only")
+ codec = f"{f.get('vcodec', 'N/A')}"
+ if f.get('acodec') != 'none':
+ codec += f" / {f.get('acodec', 'N/A')}"
+ self.format_table.setItem(row, 5, QTableWidgetItem(codec))
+
+ # Audio Status
+ needs_audio = f.get('acodec') == 'none'
+ audio_status = "Will merge audio" if needs_audio else "✓ Has Audio"
audio_item = QTableWidgetItem(audio_status)
if needs_audio:
audio_item.setForeground(QColor('#ffa500'))
- elif audio_status == "Audio Only":
- audio_item.setForeground(QColor('#cccccc')) # Neutral color for audio only
- else: # Has Audio (Video+Audio)
- audio_item.setForeground(QColor('#00cc00')) # Green for included audio
- # Set item for correct column based on mode
- audio_column_index = 4 if is_playlist_mode else 6
- self.format_table.setItem(row, audio_column_index, audio_item)
+ self.format_table.setItem(row, 6, audio_item)
-
- # --- Populate columns only shown in non-playlist mode ---
- if not is_playlist_mode:
- # Column 3: Resolution
- self.format_table.setItem(row, 3, QTableWidgetItem(resolution))
-
- # Column 4: File Size
- filesize = f"{f.get('filesize', 0) / 1024 / 1024:.2f} MB"
- self.format_table.setItem(row, 4, QTableWidgetItem(filesize))
-
- # Column 5: Codec
- if f.get('vcodec') == 'none':
- codec = f.get('acodec', 'N/A')
- else:
- codec = f"{f.get('vcodec', 'N/A')}"
- if f.get('acodec') != 'none':
- codec += f" / {f.get('acodec', 'N/A')}"
- self.format_table.setItem(row, 5, QTableWidgetItem(codec))
-
- # Column 7: Notes
- notes = self.get_format_notes(f, best_video_size)
- notes_item = QTableWidgetItem(notes)
- if "✨ Recommended" in notes:
- notes_item.setForeground(QColor('#00ff00')) # Green for recommended
- elif "💾 Storage friendly" in notes:
- notes_item.setForeground(QColor('#00ccff')) # Blue for storage friendly
- elif "📱 Mobile friendly" in notes:
- notes_item.setForeground(QColor('#ff9900')) # Orange for mobile
- self.format_table.setItem(row, 7, notes_item)
+ # Add Notes column
+ notes = self.get_format_notes(f, best_video_size)
+ notes_item = QTableWidgetItem(notes)
+ if "✨ Recommended" in notes:
+ notes_item.setForeground(QColor('#00ff00')) # Green for recommended
+ elif "💾 Storage friendly" in notes:
+ notes_item.setForeground(QColor('#00ccff')) # Blue for storage friendly
+ elif "📱 Mobile friendly" in notes:
+ notes_item.setForeground(QColor('#ff9900')) # Orange for mobile
+ self.format_table.setItem(row, 7, notes_item)
def handle_checkbox_click(self, clicked_checkbox):
for checkbox in self.format_checkboxes:
diff --git a/ytsage_gui_main.py b/ytsage_gui_main.py
index 6b8001b..898b096 100644
--- a/ytsage_gui_main.py
+++ b/ytsage_gui_main.py
@@ -4,9 +4,8 @@ import webbrowser
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLineEdit, QPushButton, QTableWidget,
QTableWidgetItem, QProgressBar, QLabel, QFileDialog,
- QHeaderView, QStyle, QStyleFactory, QComboBox, QTextEdit, QDialog, QPlainTextEdit, QCheckBox, QButtonGroup, QMessageBox, QListWidget,
- QListWidgetItem, QDialogButtonBox, QScrollArea)
-from PySide6.QtCore import Qt, Signal, QObject, QThread, QMetaObject, Q_ARG, QProcess, Slot
+ QHeaderView, QStyle, QStyleFactory, QComboBox, QTextEdit, QDialog, QPlainTextEdit, QCheckBox, QButtonGroup, QMessageBox)
+from PySide6.QtCore import Qt, Signal, QObject, QThread, QMetaObject, Q_ARG, QProcess
from PySide6.QtGui import QIcon, QPalette, QColor, QPixmap
import requests
from io import BytesIO
@@ -18,14 +17,10 @@ from packaging import version
import subprocess
import re
import yt_dlp
-import markdown
from ytsage_downloader import DownloadThread, SignalManager # Import downloader related classes
from ytsage_utils import check_ffmpeg, get_yt_dlp_path, load_saved_path, save_path # Import utility functions
-from ytsage_gui_dialogs import (LogWindow, CustomCommandDialog, FFmpegCheckDialog,
- YTDLPUpdateDialog, AboutDialog, SubtitleSelectionDialog,
- PlaylistSelectionDialog, CookieLoginDialog,
- DownloadSettingsDialog) # <-- Use renamed dialog
+from ytsage_gui_dialogs import LogWindow, CustomCommandDialog, FFmpegCheckDialog, YTDLPUpdateDialog, AboutDialog # Import dialogs
from ytsage_gui_format_table import FormatTableMixin # Import FormatTableMixin
from ytsage_gui_video_info import VideoInfoMixin # Import VideoInfoMixin
@@ -36,17 +31,11 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
if not check_ffmpeg():
self.show_ffmpeg_dialog()
- self.version = "4.5.0"
+ self.version = "4.2.1"
self.check_for_updates()
self.config_file = Path.home() / '.ytsage_config.json'
load_saved_path(self)
- # Load custom icon
- icon_path = os.path.join(os.path.dirname(__file__), 'Icon', 'icon.png')
- if os.path.exists(icon_path):
- self.setWindowIcon(QIcon(icon_path))
- else:
- print(f"Warning: Icon file not found at {icon_path}. Using default icon.")
- self.setWindowIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowDown)) # Fallback
+ self.setWindowIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowDown))
self.signals = SignalManager()
self.download_paused = False
self.current_download = None
@@ -59,67 +48,60 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
self.is_playlist = False
self.playlist_info = None
self.video_info = None
- self.playlist_entries = [] # Initialize playlist entries
- self.selected_playlist_items = None # Initialize selection string
- self.save_description = False # Initialize description state
self.subtitle_filter = ""
self.thumbnail_image = None
self.video_url = ""
- self.selected_subtitles = [] # Initialize selected subtitles list
- self.cookie_file_path = None # Initialize cookie file path
- self.speed_limit_value = None # Store speed limit value
- self.speed_limit_unit_index = 0 # Store speed limit unit index (0: KB/s, 1: MB/s)
self.init_ui()
self.setStyleSheet("""
QMainWindow {
- background-color: #15181b;
+ background-color: #2b2b2b;
}
QWidget {
- background-color: #15181b;
+ background-color: #2b2b2b;
color: #ffffff;
}
QLineEdit {
padding: 8px;
- border: 2px solid #1b2021;
+ border: 2px solid #3d3d3d;
border-radius: 4px;
- background-color: #1b2021;
+ background-color: #363636;
color: #ffffff;
}
QPushButton {
padding: 8px 15px;
- background-color: #c90000;
+ background-color: #ff0000; /* YouTube red */
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
}
QPushButton:hover {
- background-color: #a50000;
+ background-color: #cc0000; /* Darker red on hover */
}
QPushButton:pressed {
- background-color: #800000;
+ background-color: #990000; /* Even darker red when pressed */
}
QTableWidget {
- border: 2px solid #1b2021;
+ border: 2px solid #3d3d3d;
border-radius: 4px;
- background-color: #1b2021;
- gridline-color: #1b2021;
+ background-color: #363636;
+ gridline-color: #3d3d3d;
}
QHeaderView::section {
- background-color: #15181b;
+ background-color: #2b2b2b;
padding: 5px;
- border: 1px solid #1b2021;
+ border: 1px solid #3d3d3d;
color: #ffffff;
}
QProgressBar {
- border: 2px solid #1b2021;
+ border: 2px solid #3d3d3d;
border-radius: 4px;
text-align: center;
color: white;
}
QProgressBar::chunk {
- background-color: #c90000;
+ background-color: #ff0000; /* YouTube red */
border-radius: 2px;
}
QLabel {
@@ -127,23 +109,23 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
}
/* Style for filter buttons */
QPushButton.filter-btn {
- background-color: #1b2021;
+ background-color: #363636;
padding: 5px 10px;
margin: 0 5px;
}
QPushButton.filter-btn:checked {
- background-color: #c90000;
+ background-color: #ff0000;
}
QPushButton.filter-btn:hover {
background-color: #444444;
}
QPushButton.filter-btn:checked:hover {
- background-color: #a50000;
+ background-color: #cc0000;
}
/* Modern Scrollbar Styling */
QScrollBar:vertical {
border: none;
- background: #15181b;
+ background: #2b2b2b;
width: 14px;
margin: 15px 0 15px 0;
border-radius: 7px;
@@ -158,7 +140,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
}
QScrollBar::sub-line:vertical {
border: none;
- background: #15181b;
+ background: #2b2b2b;
height: 15px;
border-top-left-radius: 7px;
border-top-right-radius: 7px;
@@ -167,7 +149,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
}
QScrollBar::add-line:vertical {
border: none;
- background: #15181b;
+ background: #2b2b2b;
height: 15px;
border-bottom-left-radius: 7px;
border-bottom-right-radius: 7px;
@@ -189,7 +171,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
/* Horizontal Scrollbar */
QScrollBar:horizontal {
border: none;
- background: #15181b;
+ background: #2b2b2b;
height: 14px;
margin: 0 15px 0 15px;
border-radius: 7px;
@@ -204,7 +186,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
}
QScrollBar::sub-line:horizontal {
border: none;
- background: #15181b;
+ background: #2b2b2b;
width: 15px;
border-top-left-radius: 7px;
border-bottom-left-radius: 7px;
@@ -213,7 +195,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
}
QScrollBar::add-line:horizontal {
border: none;
- background: #15181b;
+ background: #2b2b2b;
width: 15px;
border-top-right-radius: 7px;
border-bottom-right-radius: 7px;
@@ -235,17 +217,6 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
""")
self.signals.update_progress.connect(self.update_progress_bar)
- # After adding format buttons
- self.video_button.clicked.connect(self.filter_formats) # Connect video button
- self.audio_button.clicked.connect(self.filter_formats) # Connect audio button
-
- # Add connections to handle video/audio mode-specific controls
- self.video_button.clicked.connect(self.handle_mode_change)
- self.audio_button.clicked.connect(self.handle_mode_change)
-
- # Initialize UI state based on current mode
- self.handle_mode_change()
-
def load_saved_path(self): # Using function from ytsage_utils now - no longer needed in class
pass # Handled in class init now via ytsage_utils.load_saved_path(self)
@@ -253,8 +224,8 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
save_path(self, path) # Call the utility function
def init_ui(self):
- self.setWindowTitle('YTSage v4.5.0')
- self.setMinimumSize(900, 750)
+ self.setWindowTitle('YTSage v4.2.1')
+ self.setMinimumSize(900, 650)
# Main widget and layout
main_widget = QWidget()
@@ -266,62 +237,39 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
# URL input section
url_layout = QHBoxLayout()
self.url_input = QLineEdit()
- self.url_input.setPlaceholderText("Enter YouTube video or playlist URL")
- self.url_input.returnPressed.connect(self.analyze_url) # Analyze on Enter key
+ self.url_input.setPlaceholderText('Enter YouTube URL...')
- self.analyze_button = QPushButton("Analyze")
- self.analyze_button.clicked.connect(self.analyze_url)
+ # Add Paste URL button
+ self.paste_btn = QPushButton('Paste URL')
+ self.paste_btn.clicked.connect(self.paste_url)
- self.paste_button = QPushButton("Paste URL")
- self.paste_button.clicked.connect(self.paste_url)
-
- url_layout.addWidget(self.url_input, 1)
- url_layout.addWidget(self.paste_button)
- url_layout.addWidget(self.analyze_button)
+ self.analyze_btn = QPushButton('Analyze')
+ self.analyze_btn.clicked.connect(self.analyze_url)
+ url_layout.addWidget(self.url_input)
+ url_layout.addWidget(self.paste_btn)
+ url_layout.addWidget(self.analyze_btn)
layout.addLayout(url_layout)
- # Video info container
+ # Video info container with smaller fixed height
video_info_container = QWidget()
+ video_info_container.setFixedHeight(220)
video_info_layout = QVBoxLayout(video_info_container)
video_info_layout.setSpacing(5)
video_info_layout.setContentsMargins(0, 0, 0, 0)
- # Add media info layout (Thumbnail | Video Details)
+ # Add media info layout
media_info_layout = self.setup_video_info_section()
video_info_layout.addLayout(media_info_layout)
+ # Add playlist info with minimal height
+ self.playlist_info_label = self.setup_playlist_info_section()
+ self.playlist_info_label.setMaximumHeight(30)
+ video_info_layout.addWidget(self.playlist_info_label)
+
# Add video info container to main layout
layout.addWidget(video_info_container)
- # --- Add Playlist Info Section Directly to Main Layout ---
- # Add playlist info label (initially hidden)
- self.playlist_info_label = self.setup_playlist_info_section()
- layout.addWidget(self.playlist_info_label)
-
- # Add playlist selection BUTTON (initially hidden) - REPLACED QLineEdit
- self.playlist_select_btn = QPushButton("Select Videos...")
- self.playlist_select_btn.clicked.connect(self.open_playlist_selection_dialog)
- self.playlist_select_btn.setVisible(False)
- self.playlist_select_btn.setStyleSheet("""
- QPushButton {
- padding: 6px 12px;
- background-color: #1d1e22;
- border: 1px solid #c90000;
- border-radius: 4px;
- color: white;
- font-weight: normal;
- text-align: left;
- padding-left: 10px;
- }
- QPushButton:hover {
- background-color: #2a2d36;
- border-color: #a50000;
- }
- """)
- layout.addWidget(self.playlist_select_btn)
- # --- End Playlist Info Section ---
-
# Format controls section with minimal spacing
layout.addSpacing(5)
@@ -344,20 +292,20 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
self.video_button.setStyleSheet("""
QPushButton {
padding: 8px 15px;
- background-color: #1d1e22;
+ background-color: #363636;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
}
QPushButton:checked {
- background-color: #c90000;
+ background-color: #ff0000;
}
QPushButton:hover {
- background-color: #2a2d36;
+ background-color: #444444;
}
QPushButton:checked:hover {
- background-color: #a50000;
+ background-color: #cc0000;
}
""")
self.format_buttons.addButton(self.video_button)
@@ -369,55 +317,27 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
self.audio_button.setStyleSheet("""
QPushButton {
padding: 8px 15px;
- background-color: #1d1e22;
+ background-color: #363636;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
}
QPushButton:checked {
- background-color: #c90000;
+ background-color: #ff0000;
}
QPushButton:hover {
- background-color: #2a2d36;
+ background-color: #444444;
}
QPushButton:checked:hover {
- background-color: #a50000;
+ background-color: #cc0000;
}
""")
self.format_buttons.addButton(self.audio_button)
self.format_layout.addWidget(self.audio_button)
- # Add Merge Subtitles checkbox (Moved here)
- self.merge_subs_checkbox = QCheckBox("Merge Subtitles")
- self.merge_subs_checkbox.setStyleSheet("""
- QCheckBox {
- color: #ffffff;
- padding: 5px;
- margin-left: 20px; /* Consistent margin */
- }
- QCheckBox::indicator {
- width: 18px;
- height: 18px;
- border-radius: 9px;
- }
- QCheckBox::indicator:unchecked {
- border: 2px solid #666666;
- background: #1d1e22;
- border-radius: 9px;
- }
- QCheckBox::indicator:checked {
- border: 2px solid #c90000;
- background: #c90000;
- border-radius: 9px;
- }
- /* Add disabled state styling if needed */
- QCheckBox:disabled { color: #888888; }
- QCheckBox::indicator:disabled { border-color: #555555; background: #444444; }
- """)
- # Initially disable it, will be enabled if subtitles are selected later
- self.merge_subs_checkbox.setEnabled(False)
- self.format_layout.addWidget(self.merge_subs_checkbox)
+ # Connect format buttons
+ self.format_buttons.buttonClicked.connect(self.handle_format_selection)
# Add SponsorBlock checkbox
self.sponsorblock_checkbox = QCheckBox("Remove Sponsor Segments")
@@ -425,7 +345,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
QCheckBox {
color: #ffffff;
padding: 5px;
- margin-left: 20px; /* Consistent margin */
+ margin-left: 20px;
}
QCheckBox::indicator {
width: 18px;
@@ -434,55 +354,45 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
}
QCheckBox::indicator:unchecked {
border: 2px solid #666666;
- background: #1d1e22;
+ background: #2b2b2b;
border-radius: 9px;
}
QCheckBox::indicator:checked {
- border: 2px solid #c90000;
- background: #c90000;
+ border: 2px solid #ff0000;
+ background: #ff0000;
border-radius: 9px;
}
""")
self.format_layout.addWidget(self.sponsorblock_checkbox)
- # Add Save Thumbnail Checkbox (Moved here)
+ # Add Save Thumbnail checkbox with same style as SponsorBlock
self.save_thumbnail_checkbox = QCheckBox("Save Thumbnail")
- self.save_thumbnail_checkbox.setChecked(False)
- self.save_thumbnail_checkbox.stateChanged.connect(self.toggle_save_thumbnail)
self.save_thumbnail_checkbox.setStyleSheet("""
QCheckBox {
color: #ffffff;
padding: 5px;
margin-left: 20px;
}
- QCheckBox::indicator { width: 18px; height: 18px; border-radius: 9px; }
- QCheckBox::indicator:unchecked { border: 2px solid #666666; background: #1d1e22; border-radius: 9px; }
- QCheckBox::indicator:checked { border: 2px solid #c90000; background: #c90000; border-radius: 9px; }
- QCheckBox:disabled { color: #888888; }
- QCheckBox::indicator:disabled { border-color: #555555; background: #444444; }
+ QCheckBox::indicator {
+ width: 18px;
+ height: 18px;
+ border-radius: 9px;
+ }
+ QCheckBox::indicator:unchecked {
+ border: 2px solid #666666;
+ background: #2b2b2b;
+ border-radius: 9px;
+ }
+ QCheckBox::indicator:checked {
+ border: 2px solid #ff0000;
+ background: #ff0000;
+ border-radius: 9px;
+ }
""")
+ self.save_thumbnail_checkbox.clicked.connect(self.toggle_save_thumbnail)
self.format_layout.addWidget(self.save_thumbnail_checkbox)
- # Add Save Description Checkbox (Moved here)
- self.save_description_checkbox = QCheckBox("Save Description")
- self.save_description_checkbox.setChecked(False)
- self.save_description_checkbox.stateChanged.connect(self.toggle_save_description)
- self.save_description_checkbox.setStyleSheet("""
- QCheckBox {
- color: #ffffff;
- padding: 5px;
- margin-left: 20px;
- }
- QCheckBox::indicator { width: 18px; height: 18px; border-radius: 9px; }
- QCheckBox::indicator:unchecked { border: 2px solid #666666; background: #1d1e22; border-radius: 9px; }
- QCheckBox::indicator:checked { border: 2px solid #c90000; background: #c90000; border-radius: 9px; }
- QCheckBox:disabled { color: #888888; }
- QCheckBox::indicator:disabled { border-color: #555555; background: #444444; }
- """)
- self.format_layout.addWidget(self.save_description_checkbox)
-
self.format_layout.addStretch()
-
layout.addLayout(self.format_layout)
# Format table with stretch
@@ -496,21 +406,17 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
self.custom_cmd_btn = QPushButton('Custom Command')
self.custom_cmd_btn.clicked.connect(self.show_custom_command)
- # New button for cookie login
- self.cookie_login_button = QPushButton("Login with Cookies")
- self.cookie_login_button.clicked.connect(self.show_cookie_login_dialog) # Connect to a new method
-
self.update_ytdlp_btn = QPushButton('Update yt-dlp')
self.update_ytdlp_btn.clicked.connect(self.update_ytdlp)
self.about_btn = QPushButton('About')
self.about_btn.clicked.connect(self.show_about_dialog)
- # --- Rename Path Button to Settings Button ---
- self.settings_button = QPushButton("Download Settings") # Renamed button
- self.settings_button.clicked.connect(self.show_download_settings_dialog) # Renamed method
- self.settings_button.setToolTip(f"Current Path: {self.last_path}\nSpeed Limit: None") # Update initial tooltip
- # --- End Settings Button ---
+ self.path_input = QLineEdit(self.last_path)
+ self.path_input.setPlaceholderText('Download path...')
+
+ self.browse_btn = QPushButton('Browse')
+ self.browse_btn.clicked.connect(self.browse_path)
self.download_btn = QPushButton('Download')
self.download_btn.clicked.connect(self.start_download)
@@ -526,10 +432,10 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
# Add all buttons to layout in the correct order
download_layout.addWidget(self.custom_cmd_btn)
- download_layout.addWidget(self.cookie_login_button)
download_layout.addWidget(self.update_ytdlp_btn)
download_layout.addWidget(self.about_btn)
- download_layout.addWidget(self.settings_button)
+ download_layout.addWidget(self.path_input)
+ download_layout.addWidget(self.browse_btn)
download_layout.addWidget(self.download_btn)
download_layout.addWidget(self.pause_btn)
download_layout.addWidget(self.cancel_btn)
@@ -585,6 +491,10 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
self.signals.update_status.connect(self.status_label.setText)
self.signals.update_progress.connect(self.update_progress_bar)
+ # After adding format buttons
+ self.video_button.clicked.connect(self.filter_formats) # Connect video button
+ self.audio_button.clicked.connect(self.filter_formats) # Connect audio button
+
def analyze_url(self):
url = self.url_input.text().strip()
if not url:
@@ -597,17 +507,17 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
def _analyze_url_thread(self, url):
try:
- self.signals.update_status.emit("Analyzing (15%)... Extracting basic info")
+ self.signals.update_status.emit("Analyzing (20%)... Extracting basic info")
# Clean up the URL to handle both playlist and video URLs
if 'list=' in url and 'watch?v=' in url:
playlist_id = url.split('list=')[1].split('&')[0]
url = f'https://www.youtube.com/playlist?list={playlist_id}'
- # Initial extraction with basic options - suppress warnings here too
+ # Initial extraction with basic options
ydl_opts = {
'quiet': False,
- 'no_warnings': True, # <-- Suppress warnings for initial check
+ 'no_warnings': False,
'extract_flat': True,
'force_generic_extractor': False,
'ignoreerrors': True,
@@ -615,10 +525,6 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
'verbose': True
}
- # Add cookies argument if cookie file path is set
- if self.cookie_file_path:
- ydl_opts['cookiefile'] = self.cookie_file_path
-
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
try:
basic_info = ydl.extract_info(url, download=False)
@@ -628,10 +534,9 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
print(f"First extraction failed: {str(e)}")
raise Exception("Could not extract video information, please check your link")
- self.signals.update_status.emit("Analyzing (30%)... Extracting detailed info")
- # Configure options for detailed extraction (keep other options)
- # Add no_warnings here as well, as this is where detailed info is fetched
- ydl_opts_detail = {
+ self.signals.update_status.emit("Analyzing (40%)... Extracting detailed info")
+ # Configure options for detailed extraction
+ ydl_opts.update({
'extract_flat': False,
'format': None,
'writesubtitles': True,
@@ -640,122 +545,75 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
'playliststart': 1,
'playlistend': 1,
'youtube_include_dash_manifest': True,
- 'youtube_include_hls_manifest': True,
- 'no_warnings': True # <-- Add flag here for detailed extraction
- }
+ 'youtube_include_hls_manifest': True
+ })
- # Add cookies argument if cookie file path is set
- if self.cookie_file_path:
- ydl_opts_detail['cookiefile'] = self.cookie_file_path
-
- # Use a separate options dict for the detailed extraction
- with yt_dlp.YoutubeDL(ydl_opts_detail) as ydl_detail:
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
try:
- self.signals.update_status.emit("Analyzing (45%)... Processing video data")
+ self.signals.update_status.emit("Analyzing (60%)... Processing video data")
if basic_info.get('_type') == 'playlist':
self.is_playlist = True
self.playlist_info = basic_info
- self.selected_playlist_items = None # Reset selection for new playlist
- self.playlist_entries = [entry for entry in basic_info.get('entries', []) if entry] # Store entries
-
- # Ensure there are entries before proceeding
- if not self.playlist_entries:
- raise Exception("Playlist contains no valid videos.")
-
- # Extract detailed info for the FIRST video in the playlist
- # This provides formats/subs for the UI, assuming consistency
- first_video_url = self.playlist_entries[0].get('url')
- if not first_video_url:
- raise Exception("Could not get URL for the first playlist video.")
- try:
- # Use the ydl_detail instance with no_warnings
- self.video_info = ydl_detail.extract_info(first_video_url, download=False)
- except Exception as first_video_error:
- raise Exception(f"Failed to extract info for the first playlist video: {first_video_error}")
-
- # Update playlist info label text (remains the same)
- playlist_text = (f"Playlist: {basic_info.get('title', 'Unknown Playlist')} | "
- f"{len(self.playlist_entries)} videos") # Simplified label
+
+ # Store all playlist entries
+ self.playlist_entries = [entry for entry in basic_info['entries'] if entry]
+
+ # Update playlist info text
+ playlist_text = (f"Playlist: {basic_info.get('title', 'Unknown')} | "
+ f"{len(self.playlist_entries)} videos | "
+ f"Enter video numbers (e.g. 1-5,7,9-11)")
QMetaObject.invokeMethod(
- self.playlist_info_label, "setText", Qt.ConnectionType.QueuedConnection,
+ self.playlist_info_label,
+ "setText",
+ Qt.ConnectionType.QueuedConnection,
Q_ARG(str, playlist_text)
)
QMetaObject.invokeMethod(
- self.playlist_info_label, "setVisible", Qt.ConnectionType.QueuedConnection,
+ self.playlist_info_label,
+ "setVisible",
+ Qt.ConnectionType.QueuedConnection,
Q_ARG(bool, True)
)
- # Show playlist selection BUTTON
+ # Show playlist selection input
QMetaObject.invokeMethod(
- self, # Target object is the YTSageApp instance
- 'update_playlist_button_text', # Name of the slot
+ self.playlist_selection_input,
+ "setVisible",
Qt.ConnectionType.QueuedConnection,
- Q_ARG(str, "Select Videos... (All selected)") # Argument for the slot
- )
- QMetaObject.invokeMethod(
- self.playlist_select_btn, "setVisible", Qt.ConnectionType.QueuedConnection,
Q_ARG(bool, True)
)
- else: # Single video
+ else:
self.is_playlist = False
- # Use ydl_detail instance here too for consistency
- self.video_info = ydl_detail.extract_info(url, download=False)
- self.playlist_entries = [] # Clear entries
- self.selected_playlist_items = None # Clear selection
-
- # Hide playlist info label and button
- QMetaObject.invokeMethod(
- self.playlist_info_label, "setVisible", Qt.ConnectionType.QueuedConnection,
- Q_ARG(bool, False)
- )
- QMetaObject.invokeMethod(
- self.playlist_select_btn, "setVisible", Qt.ConnectionType.QueuedConnection,
- Q_ARG(bool, False)
- )
+ self.video_info = ydl.extract_info(url, download=False)
+ self.playlist_info_label.setVisible(False)
# Verify we have format information
if not self.video_info or 'formats' not in self.video_info:
print(f"Debug - video_info keys: {self.video_info.keys() if self.video_info else 'None'}")
raise Exception("No format information available")
- self.signals.update_status.emit("Analyzing (60%)... Processing formats")
+ self.signals.update_status.emit("Analyzing (80%)... Processing formats")
self.all_formats = self.video_info['formats']
# Update UI
self.update_video_info(self.video_info)
# Update thumbnail
- self.signals.update_status.emit("Analyzing (75%)... Loading thumbnail")
- thumbnail_url = None
- if self.is_playlist:
- # Try to get thumbnail from playlist info first
- thumbnail_url = self.playlist_info.get('thumbnail')
-
- # Fallback to video thumbnail if playlist thumbnail not found or not a playlist
- if not thumbnail_url:
- thumbnail_url = self.video_info.get('thumbnail')
-
- self.download_thumbnail(thumbnail_url)
+ self.signals.update_status.emit("Analyzing (90%)... Loading thumbnail")
+ self.download_thumbnail(self.video_info.get('thumbnail'))
# Save thumbnail if enabled - use the stored VIDEO URL
if self.save_thumbnail:
self.download_thumbnail_file(self.video_url, self.path_input.text())
- # --- Subtitle Handling ---
- self.signals.update_status.emit("Analyzing (85%)... Processing subtitles")
- # Clear previous selections when analyzing a new video
- self.selected_subtitles = []
+ # Update subtitles
+ self.signals.update_status.emit("Analyzing (95%)... Processing subtitles")
self.available_subtitles = self.video_info.get('subtitles', {})
self.available_automatic_subtitles = self.video_info.get('automatic_captions', {})
- # Update the UI elements related to subtitle selection state
- QMetaObject.invokeMethod(self.selected_subs_label, "setText", Qt.ConnectionType.QueuedConnection, Q_ARG(str, "0 selected"))
- # QMetaObject.invokeMethod(self.subtitle_select_btn, "setProperty", Qt.ConnectionType.QueuedConnection, Q_ARG(str, "subtitlesSelected"), Q_ARG(bool, False)) # <-- COMMENT OUT THIS LINE
- # REMOVE the merge_subs_checkbox update call from here
- # QMetaObject.invokeMethod(self.merge_subs_checkbox, "setEnabled", Qt.ConnectionType.QueuedConnection, Q_ARG(bool, False))
-
+ self.update_subtitle_list()
# Update format table
- self.signals.update_status.emit("Analyzing (95%)... Updating format table")
+ self.signals.update_status.emit("Analyzing (98%)... Updating format table")
self.video_button.setChecked(True)
self.audio_button.setChecked(False)
self.filter_formats()
@@ -770,9 +628,6 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
error_message = str(e)
print(f"Error in analysis: {error_message}")
self.signals.update_status.emit(f"Error: {error_message}")
- # Ensure playlist UI is hidden on error too
- QMetaObject.invokeMethod(self.playlist_info_label, "setVisible", Qt.ConnectionType.QueuedConnection, Q_ARG(bool, False))
- QMetaObject.invokeMethod(self.playlist_select_btn, "setVisible", Qt.ConnectionType.QueuedConnection, Q_ARG(bool, False))
def paste_url(self):
clipboard = QApplication.clipboard()
@@ -782,55 +637,20 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
dialog = YTDLPUpdateDialog(self)
dialog.exec()
- def show_download_settings_dialog(self): # Renamed method
- dialog = DownloadSettingsDialog(
- self.last_path,
- self.speed_limit_value,
- self.speed_limit_unit_index,
- self
- )
- if dialog.exec():
- # Update Path
- new_path = dialog.get_selected_path()
- path_changed = False
- if new_path != self.last_path:
- self.last_path = new_path
- self.save_path(self.last_path) # Save the updated path
- path_changed = True
- print(f"Download path updated to: {self.last_path}")
-
- # Update Speed Limit
- new_limit_value = dialog.get_selected_speed_limit()
- new_unit_index = dialog.get_selected_unit_index()
- limit_changed = False
- if new_limit_value != self.speed_limit_value or new_unit_index != self.speed_limit_unit_index:
- self.speed_limit_value = new_limit_value
- self.speed_limit_unit_index = new_unit_index
- limit_changed = True
- print(f"Speed limit updated to: {self.speed_limit_value} {['KB/s', 'MB/s'][self.speed_limit_unit_index] if self.speed_limit_value else 'None'}")
-
- # Update Tooltip if anything changed
- if path_changed or limit_changed:
- limit_text = "None"
- if self.speed_limit_value:
- limit_text = f"{self.speed_limit_value} {['KB/s', 'MB/s'][self.speed_limit_unit_index]}"
- self.settings_button.setToolTip(f"Current Path: {self.last_path}\nSpeed Limit: {limit_text}")
+ def browse_path(self):
+ path = QFileDialog.getExistingDirectory(self, "Select Download Directory", self.last_path)
+ if path:
+ self.path_input.setText(path)
+ self.save_path(path) # Use save_path utility function
+ self.last_path = path
def start_download(self):
url = self.url_input.text().strip()
- # --- Use self.last_path instead of reading from QLineEdit ---
- path = self.last_path
+ path = self.path_input.text().strip()
if not url or not path:
- # More specific error message if path is missing
- if not path:
- self.status_label.setText("Please set a download path using 'Change Path'")
- elif not url:
- self.status_label.setText("Please enter a URL")
- else:
- self.status_label.setText("Please enter URL and set download path")
+ self.status_label.setText("Please enter URL and download path")
return
- # --- End Path Change ---
# Get selected format
format_id = self.get_selected_format()
@@ -851,60 +671,39 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
resolution = parts[0].strip().lower()
break
- # Get subtitle selection if available - Now get the list
- selected_subs = self.selected_subtitles if hasattr(self, 'selected_subtitles') else []
+ # Get subtitle selection if available
+ subtitle_lang = None
+ if hasattr(self, 'subtitle_combo') and self.subtitle_combo.currentIndex() > 0:
+ subtitle_lang = self.subtitle_combo.currentText()
- # Get playlist selection IF in playlist mode - USE STORED VALUE
- playlist_items_to_download = None
- if self.is_playlist:
- playlist_items_to_download = self.selected_playlist_items # Use the stored selection string
+ # Check if it's a playlist
+ is_playlist = 'playlist' in url.lower() and '/watch?' not in url
- # --- Use stored speed limit values ---
- rate_limit = None
- if self.speed_limit_value:
- try:
- limit_value = float(self.speed_limit_value)
- if self.speed_limit_unit_index == 0: # KB/s
- rate_limit = f"{int(limit_value * 1024)}"
- elif self.speed_limit_unit_index == 1: # MB/s
- rate_limit = f"{int(limit_value * 1024 * 1024)}"
- except ValueError:
- # Use a signal to show error in status bar, similar to URL/Path errors
- self.signals.update_status.emit("❌ Error: Invalid speed limit value set in settings.")
- return
- # --- End speed limit update ---
+ # Get playlist selection if available
+ playlist_items = None
+ if self.is_playlist and self.playlist_selection_input.isVisible():
+ playlist_items = self.playlist_selection_input.text().strip() or None
# Save thumbnail if enabled
if self.save_thumbnail:
- # Consider moving thumbnail download *after* successful video download
- # Or handle errors more gracefully if thumbnail download fails
- try:
- self.download_thumbnail_file(url, path)
- except Exception as e:
- print(f"Warning: Thumbnail download failed: {e}")
- # Optionally inform the user, but don't stop the main download
-
+ self.download_thumbnail_file(url, path)
# Create download thread with resolution in output template
self.download_thread = DownloadThread(
url=url,
path=path,
format_id=format_id,
- subtitle_langs=selected_subs, # Pass the list of selected subs
- is_playlist=self.is_playlist, # Use the flag directly
+ subtitle_lang=subtitle_lang,
+ is_playlist=is_playlist,
merge_subs=self.merge_subs_checkbox.isChecked(),
enable_sponsorblock=self.sponsorblock_checkbox.isChecked(),
resolution=resolution,
- playlist_items=playlist_items_to_download, # Pass the selection string
- save_description=self.save_description, # Pass the new flag here
- cookie_file=self.cookie_file_path, # Pass the cookie file path
- rate_limit=rate_limit # Pass the calculated rate limit
+ playlist_items=playlist_items
)
# Connect signals
self.download_thread.progress_signal.connect(self.update_progress_bar)
- self.download_thread.status_signal.connect(self.status_label.setText)
- self.download_thread.update_details.connect(self.download_details_label.setText)
+ self.download_thread.status_signal.connect(self.signals.update_status.emit)
self.download_thread.finished_signal.connect(self.download_finished)
self.download_thread.error_signal.connect(self.download_error)
self.download_thread.file_exists_signal.connect(self.file_already_exists)
@@ -928,31 +727,13 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
self.pause_btn.setVisible(False)
self.cancel_btn.setVisible(False)
self.progress_bar.setValue(100)
-
- # Set completion message based on the file type of last downloaded file
- if self.download_thread and self.download_thread.current_filename:
- filename = self.download_thread.current_filename
- ext = os.path.splitext(filename)[1].lower()
-
- # Video file extensions
- if ext in ['.mp4', '.webm', '.mkv', '.avi', '.mov', '.flv']:
- self.status_label.setText(f"✅ Video download completed!")
- # Audio file extensions
- elif ext in ['.mp3', '.m4a', '.aac', '.wav', '.ogg', '.opus', '.flac']:
- self.status_label.setText(f"✅ Audio download completed!")
- # Subtitle file extensions
- elif ext in ['.vtt', '.srt', '.ass', '.ssa']:
- self.status_label.setText(f"✅ Subtitle download completed!")
- # Default case
- else:
- self.status_label.setText("✅ Download completed!")
+ self.status_label.setText("Download completed!")
def download_error(self, error_message):
self.toggle_download_controls(True)
self.pause_btn.setVisible(False)
self.cancel_btn.setVisible(False)
self.status_label.setText(f"Error: {error_message}")
- self.download_details_label.setText("") # Clear details label on error
def update_progress_bar(self, value):
try:
@@ -986,65 +767,26 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
# Compare versions
if version.parse(latest_version) > version.parse(self.version):
- changelog = latest_release.get("body", "No changelog available.") # Get changelog body
- self.show_update_dialog(latest_version, latest_release["html_url"], changelog) # Pass changelog
+ self.show_update_dialog(latest_version, latest_release["html_url"])
except Exception as e:
print(f"Failed to check for updates: {str(e)}")
- def show_update_dialog(self, latest_version, release_url, changelog): # Added changelog parameter
+ def show_update_dialog(self, latest_version, release_url):
msg = QDialog(self)
msg.setWindowTitle("Update Available")
- msg.setMinimumWidth(500) # Increased width for changelog
- msg.setMinimumHeight(400) # Added min height
-
- # Set custom icon directly
- icon_path = os.path.join(os.path.dirname(__file__), 'Icon', 'icon.png')
- if os.path.exists(icon_path):
- msg.setWindowIcon(QIcon(icon_path))
- else:
- # Fallback to main window icon if file not found
- msg.setWindowIcon(self.windowIcon())
+ msg.setMinimumWidth(400)
layout = QVBoxLayout(msg)
- layout.setSpacing(10) # Added spacing
# Update message
message_label = QLabel(
- f"A new version of YTSage is available!\n\n"
+ f"A new version of YTSage is available!\n\n"
f"Current version: {self.version}\n"
f"Latest version: {latest_version}"
)
message_label.setWordWrap(True)
layout.addWidget(message_label)
- # Changelog Section
- changelog_label = QLabel("Changelog:")
- layout.addWidget(changelog_label)
-
- changelog_text = QTextEdit()
- changelog_text.setReadOnly(True)
- # Convert Markdown to HTML and set it
- try:
- html_changelog = markdown.markdown(changelog, extensions=['markdown.extensions.tables', 'markdown.extensions.fenced_code'])
- changelog_text.setHtml(html_changelog)
- except Exception as e:
- print(f"Error converting changelog markdown to HTML: {e}")
- changelog_text.setPlainText(changelog) # Fallback to plain text
-
- changelog_text.setStyleSheet("""
- QTextEdit {
- background-color: #1e1e1e;
- border: 1px solid #3d3d3d;
- border-radius: 4px;
- color: #cccccc;
- padding: 5px;
- font-family: Consolas, monospace;
- font-size: 11px; /* Slightly smaller font */
- }
- """)
- changelog_text.setMaximumHeight(200) # Limit height
- layout.addWidget(changelog_text)
-
# Buttons
button_layout = QHBoxLayout()
@@ -1066,7 +808,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
QLabel {
color: #ffffff;
font-size: 12px;
- padding: 5px 0; /* Adjusted padding */
+ padding: 10px;
}
QPushButton {
padding: 8px 15px;
@@ -1075,7 +817,6 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
border-radius: 4px;
color: white;
font-weight: bold;
- min-width: 120px; /* Adjusted min-width */
}
QPushButton:hover {
background-color: #cc0000;
@@ -1094,8 +835,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
def cancel_download(self):
if self.current_download:
self.current_download.cancelled = True
- self.status_label.setText("Cancelling download...") # Set status directly
- self.download_details_label.setText("") # Clear details label on cancellation
+ self.signals.update_status.emit("Cancelling download...")
def show_ffmpeg_dialog(self):
dialog = FFmpegCheckDialog(self)
@@ -1104,55 +844,21 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
def toggle_download_controls(self, enabled=True):
"""Enable or disable download-related controls"""
self.url_input.setEnabled(enabled)
- self.analyze_button.setEnabled(enabled)
+ self.analyze_btn.setEnabled(enabled)
self.format_table.setEnabled(enabled) # Changed from format_scroll_area to format_table
+ self.path_input.setEnabled(enabled)
+ self.browse_btn.setEnabled(enabled)
self.download_btn.setEnabled(enabled)
if hasattr(self, 'subtitle_combo'):
self.subtitle_combo.setEnabled(enabled)
self.video_button.setEnabled(enabled)
self.audio_button.setEnabled(enabled)
self.sponsorblock_checkbox.setEnabled(enabled)
- self.merge_subs_checkbox.setEnabled(enabled) # Enable/disable merge subs checkbox
- self.custom_cmd_btn.setEnabled(enabled) # Enable/disable custom command button
- self.cookie_login_button.setEnabled(enabled) # Enable/disable login button
- self.update_ytdlp_btn.setEnabled(enabled) # Enable/disable update button
- self.settings_button.setEnabled(enabled) # Enable/disable settings button
-
- # Clear progress/status when controls are re-enabled
- if enabled:
- self.progress_bar.setValue(0)
- self.status_label.setText("Ready")
- self.download_details_label.setText("") # Clear details label
def handle_format_selection(self, button):
# Update formats
self.filter_formats()
- def handle_mode_change(self):
- """Enable or disable features based on video/audio mode"""
- if self.audio_button.isChecked():
- # In Audio Only mode, disable video-specific features
- self.sponsorblock_checkbox.setEnabled(False)
- self.sponsorblock_checkbox.setChecked(False) # Uncheck when disabled
- self.merge_subs_checkbox.setEnabled(False)
- self.merge_subs_checkbox.setChecked(False) # Uncheck when disabled
-
- # Allow subtitle selection in Audio Only mode too
- if hasattr(self, 'subtitle_select_btn'):
- self.subtitle_select_btn.setEnabled(True)
- else:
- # In Video mode, enable video-specific features
- self.sponsorblock_checkbox.setEnabled(True)
- # Don't auto-check - leave it to user preference
-
- # Enable merge_subs only if subtitles are selected
- has_subs_selected = len(getattr(self, 'selected_subtitles', [])) > 0
- self.merge_subs_checkbox.setEnabled(has_subs_selected)
-
- # Re-enable subtitle selection button in Video mode
- if hasattr(self, 'subtitle_select_btn'):
- self.subtitle_select_btn.setEnabled(True)
-
def show_about_dialog(self): # ADDED METHOD HERE
dialog = AboutDialog(self)
dialog.exec()
@@ -1163,22 +869,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
self.pause_btn.setVisible(False)
self.cancel_btn.setVisible(False)
self.progress_bar.setValue(100)
-
- # Determine file type based on extension
- ext = os.path.splitext(filename)[1].lower()
-
- # Video file extensions
- if ext in ['.mp4', '.webm', '.mkv', '.avi', '.mov', '.flv']:
- self.status_label.setText(f"⚠️ Video file already exists")
- # Audio file extensions
- elif ext in ['.mp3', '.m4a', '.aac', '.wav', '.ogg', '.opus', '.flac']:
- self.status_label.setText(f"⚠️ Audio file already exists")
- # Subtitle file extensions
- elif ext in ['.vtt', '.srt', '.ass', '.ssa']:
- self.status_label.setText(f"⚠️ Subtitle file already exists")
- # Default case
- else:
- self.status_label.setText("⚠️ File already exists")
+ self.status_label.setText(f"⚠️ File already exists: {filename}")
# Show a simple message dialog
msg_box = QMessageBox()
@@ -1213,55 +904,4 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
}
""")
- msg_box.exec()
-
- # --- Add Toggle Methods Here ---
- def toggle_save_thumbnail(self, state):
- print(f"Raw thumbnail state received: {state}") # Debug: Print raw state
- self.save_thumbnail = bool(state == 2) # Compare state directly with 2 (Checked state)
- print(f"Save thumbnail toggled: {self.save_thumbnail}")
-
- def toggle_save_description(self, state):
- print(f"Raw description state received: {state}") # Debug: Print raw state
- self.save_description = bool(state == 2) # Compare state directly with 2 (Checked state)
- print(f"Save description toggled: {self.save_description}")
- # --- End Toggle Methods ---
-
- def open_playlist_selection_dialog(self):
- if not self.is_playlist or not self.playlist_entries:
- print("No playlist data available to select from.")
- return
-
- dialog = PlaylistSelectionDialog(self.playlist_entries, self.selected_playlist_items, self)
-
- if dialog.exec():
- self.selected_playlist_items = dialog.get_selected_items_string()
- print(f"Playlist items selected: {self.selected_playlist_items}")
-
- # Update button text (this call is safe as it happens in the main thread after dialog closes)
- if self.selected_playlist_items is None:
- button_text = "Select Videos... (All selected)"
- else:
- selected_indices = dialog._parse_selection_string(self.selected_playlist_items)
- count = len(selected_indices)
- display_text = self.selected_playlist_items if len(self.selected_playlist_items) < 30 else f"{count} videos selected"
- button_text = f"Select Videos... ({display_text})"
- self.playlist_select_btn.setText(button_text) # Direct call is fine here
-
- # --- New Slot for Updating Playlist Button Text ---
- @Slot(str)
- def update_playlist_button_text(self, text):
- """Safely updates the playlist selection button's text from any thread."""
- if hasattr(self, 'playlist_select_btn'):
- self.playlist_select_btn.setText(text)
- # --- End New Slot ---
-
- def show_cookie_login_dialog(self):
- dialog = CookieLoginDialog(self)
- if dialog.exec():
- self.cookie_file_path = dialog.get_cookie_file_path()
- if self.cookie_file_path:
- print(f"Selected cookie file: {self.cookie_file_path}") # For debugging
- QMessageBox.information(self, "Cookie File Selected", f"Cookie file selected: {self.cookie_file_path}")
- else:
- self.cookie_file_path = None # Clear path if dialog accepted but no file selected
\ No newline at end of file
+ msg_box.exec()
\ No newline at end of file
diff --git a/ytsage_gui_video_info.py b/ytsage_gui_video_info.py
index c05f591..bad0511 100644
--- a/ytsage_gui_video_info.py
+++ b/ytsage_gui_video_info.py
@@ -17,7 +17,6 @@ from packaging import version
import subprocess
import re
import yt_dlp
-from ytsage_gui_dialogs import SubtitleSelectionDialog
class VideoInfoMixin:
def setup_video_info_section(self):
@@ -56,10 +55,9 @@ class VideoInfoMixin:
self.views_label = QLabel()
self.date_label = QLabel()
self.duration_label = QLabel()
- self.like_count_label = QLabel()
# Style the info labels
- for label in [self.channel_label, self.views_label, self.date_label, self.duration_label, self.like_count_label]:
+ for label in [self.channel_label, self.views_label, self.date_label, self.duration_label]:
label.setStyleSheet("""
QLabel {
color: #cccccc;
@@ -72,53 +70,115 @@ class VideoInfoMixin:
video_info_layout.addWidget(self.title_label)
video_info_layout.addWidget(self.channel_label)
video_info_layout.addWidget(self.views_label)
- video_info_layout.addWidget(self.like_count_label)
video_info_layout.addWidget(self.date_label)
video_info_layout.addWidget(self.duration_label)
# Add spacing before subtitle section
video_info_layout.addSpacing(10)
- # --- Subtitle Section ---
+ # Create a horizontal layout for subtitle controls
subtitle_layout = QHBoxLayout()
- subtitle_layout.setSpacing(10)
+ subtitle_layout.setSpacing(5) # Reduce spacing between elements
- # Subtitle selection button
- self.subtitle_select_btn = QPushButton("Select Subtitles...") # Renamed & changed text
- self.subtitle_select_btn.setFixedHeight(30)
- # self.subtitle_select_btn.setFixedWidth(150) # Let it size naturally or adjust as needed
- self.subtitle_select_btn.clicked.connect(self.open_subtitle_dialog)
- self.subtitle_select_btn.setStyleSheet("""
+ # Create subtitle button
+ self.subtitle_check = QPushButton("Download Subtitles")
+ self.subtitle_check.setFixedHeight(30)
+ self.subtitle_check.setFixedWidth(150) # Set fixed width
+ self.subtitle_check.setCheckable(True)
+ self.subtitle_check.clicked.connect(self.toggle_subtitle_controls)
+ self.subtitle_check.setStyleSheet("""
QPushButton {
- background-color: #1d1e22;
- border: 2px solid #1d1e22;
+ background-color: #363636;
+ border: 2px solid #3d3d3d;
border-radius: 4px;
- padding: 5px 10px; /* Adjusted padding */
+ padding: 5px;
+ min-height: 30px;
}
- QPushButton:hover { background-color: #2a2d36; }
- /* Optional: Style differently if subtitles ARE selected */
- QPushButton[subtitlesSelected="true"] {
- border-color: #c90000; /* Indicate selection */
- }
- /* Style for disabled state */
- QPushButton:disabled {
- background-color: #3d3d3d;
- color: #888888;
- border-color: #3d3d3d;
+ QPushButton:checked {
+ background-color: #ff0000;
+ border-color: #cc0000;
}
""")
- self.subtitle_select_btn.setProperty("subtitlesSelected", False) # Custom property for styling
- subtitle_layout.addWidget(self.subtitle_select_btn)
+ subtitle_layout.addWidget(self.subtitle_check)
- # Label to show number of selected subtitles
- self.selected_subs_label = QLabel("0 selected")
- self.selected_subs_label.setStyleSheet("color: #cccccc; padding-left: 5px;")
- subtitle_layout.addWidget(self.selected_subs_label)
+ # Create subtitle combo box
+ self.subtitle_combo = QComboBox()
+ self.subtitle_combo.setFixedHeight(30)
+ self.subtitle_combo.setFixedWidth(200)
+ self.subtitle_combo.setVisible(False)
+ self.subtitle_combo.setStyleSheet("""
+ QComboBox {
+ background-color: #363636;
+ border: 2px solid #3d3d3d;
+ border-radius: 4px;
+ padding: 5px;
+ min-height: 30px;
+ }
+ """)
+ subtitle_layout.addWidget(self.subtitle_combo)
- # Add the subtitle layout to the main video info layout
+ # Create merge subtitles checkbox
+ self.merge_subs_checkbox = QCheckBox("Merge Subtitles")
+ self.merge_subs_checkbox.setFixedHeight(30)
+ self.merge_subs_checkbox.setVisible(False)
+ self.merge_subs_checkbox.setStyleSheet("""
+ QCheckBox {
+ color: #ffffff;
+ padding: 5px;
+ margin-left: 10px;
+ }
+ QCheckBox::indicator {
+ width: 18px;
+ height: 18px;
+ border-radius: 9px;
+ }
+ QCheckBox::indicator:unchecked {
+ border: 2px solid #666666;
+ background: #2b2b2b;
+ border-radius: 9px;
+ }
+ QCheckBox::indicator:checked {
+ border: 2px solid #ff0000;
+ background: #ff0000;
+ border-radius: 9px;
+ }
+ """)
+ subtitle_layout.addWidget(self.merge_subs_checkbox)
+
+ # Add stretch to push everything to the left
+ subtitle_layout.addStretch()
+
+ # Create a second row for the filter input
+ filter_layout = QHBoxLayout()
+ filter_layout.setSpacing(5)
+
+ # Create subtitle filter input
+ self.subtitle_filter_input = QLineEdit()
+ self.subtitle_filter_input.setFixedHeight(30)
+ self.subtitle_filter_input.setFixedWidth(200)
+ self.subtitle_filter_input.setPlaceholderText("Filter languages (e.g., en, es)")
+ self.subtitle_filter_input.textChanged.connect(self.filter_subtitles)
+ self.subtitle_filter_input.setVisible(False)
+ self.subtitle_filter_input.setStyleSheet("""
+ QLineEdit {
+ background-color: #363636;
+ border: 2px solid #3d3d3d;
+ border-radius: 4px;
+ padding: 5px;
+ min-height: 30px;
+ color: white;
+ }
+ QLineEdit:focus {
+ border-color: #ff0000;
+ }
+ """)
+ filter_layout.addWidget(self.subtitle_filter_input)
+ filter_layout.addStretch()
+
+ # Add both layouts to video info
video_info_layout.addLayout(subtitle_layout)
- # --- End Subtitle Section ---
-
+ video_info_layout.addLayout(filter_layout)
+
# Add stretch at the bottom
video_info_layout.addStretch()
@@ -133,11 +193,11 @@ class VideoInfoMixin:
self.playlist_info_label.setStyleSheet("""
QLabel {
font-size: 12px;
- color: #ffffff;
+ color: #ff9900;
padding: 5px 8px;
margin: 0;
- background-color: #1d1e22;
- border: 1px solid #c90000;
+ background-color: #2b2b2b;
+ border: 1px solid #3d3d3d;
border-radius: 4px;
min-height: 30px;
max-height: 30px;
@@ -147,109 +207,63 @@ class VideoInfoMixin:
return self.playlist_info_label
def update_video_info(self, info):
- if hasattr(self, 'is_playlist') and self.is_playlist:
- # Playlist Mode: Show playlist title and video count
- self.title_label.setText(self.playlist_info.get('title', 'Unknown Playlist'))
-
- num_videos = len(getattr(self, 'playlist_entries', []))
- self.duration_label.setText(f"Total Videos: {num_videos}")
-
- # Hide video-specific info
- self.channel_label.setText("")
- self.views_label.setText("")
- self.date_label.setText("")
- self.like_count_label.setText("")
- self.channel_label.setVisible(False)
- self.views_label.setVisible(False)
- self.date_label.setVisible(False)
- self.like_count_label.setVisible(False)
+ # Format view count with commas
+ views = int(info.get('view_count', 0))
+ formatted_views = f"{views:,}"
+
+ # Format upload date
+ upload_date = info.get('upload_date', '')
+ if upload_date:
+ date_obj = datetime.strptime(upload_date, '%Y%m%d')
+ formatted_date = date_obj.strftime('%B %d, %Y')
else:
- # Single Video Mode: Show standard video info
- # Ensure labels are visible first
- self.channel_label.setVisible(True)
- self.views_label.setVisible(True)
- self.date_label.setVisible(True)
- self.like_count_label.setVisible(True)
+ formatted_date = 'Unknown date'
- # Format view count with commas
- views = info.get('view_count')
- formatted_views = f"{views:,}" if views is not None else 'N/A'
+ # Format duration
+ duration = info.get('duration', 0)
+ minutes = duration // 60
+ seconds = duration % 60
+ duration_str = f"{minutes}:{seconds:02d}"
- # Format like count with commas
- likes = info.get('like_count')
- formatted_likes = f"{likes:,}" if likes is not None else 'N/A'
-
- # Format upload date
- upload_date = info.get('upload_date', '')
- if upload_date:
- date_obj = datetime.strptime(upload_date, '%Y%m%d')
- formatted_date = date_obj.strftime('%B %d, %Y')
- else:
- formatted_date = 'Unknown date'
-
- # Format duration
- duration = info.get('duration', 0)
- minutes = duration // 60
- seconds = duration % 60
- duration_str = f"{minutes}:{seconds:02d}"
-
- # Update labels
- self.title_label.setText(info.get('title', 'Unknown title'))
- self.channel_label.setText(f"Channel: {info.get('uploader', 'Unknown channel')}")
- self.views_label.setText(f"Views: {formatted_views}")
- self.like_count_label.setText(f"Likes: {formatted_likes}")
- self.date_label.setText(f"Upload date: {formatted_date}")
- self.duration_label.setText(f"Duration: {duration_str}")
+ # Update labels
+ self.title_label.setText(info.get('title', 'Unknown title'))
+ self.channel_label.setText(f"Channel: {info.get('uploader', 'Unknown channel')}")
+ self.views_label.setText(f"Views: {formatted_views}")
+ self.date_label.setText(f"Upload date: {formatted_date}")
+ self.duration_label.setText(f"Duration: {duration_str}")
- def open_subtitle_dialog(self):
- if not hasattr(self, 'available_subtitles') or not hasattr(self, 'available_automatic_subtitles'):
- print("Subtitle info not loaded yet.")
- return
+ def toggle_subtitle_controls(self):
+ is_checked = self.subtitle_check.isChecked()
+ self.subtitle_combo.setVisible(is_checked)
+ self.subtitle_filter_input.setVisible(is_checked)
+ self.merge_subs_checkbox.setVisible(is_checked)
- if not hasattr(self, 'selected_subtitles'):
- self.selected_subtitles = []
+ def update_subtitle_list(self):
+ self.subtitle_combo.clear()
- dialog = SubtitleSelectionDialog(
- self.available_subtitles,
- self.available_automatic_subtitles,
- self.selected_subtitles,
- self # Parent for the dialog
- )
+ if not (self.available_subtitles or self.available_automatic_subtitles):
+ self.subtitle_combo.addItem("No subtitles available")
+ return
- # Access the main application window (parent of the mixin's widget)
- # to find the merge checkbox
- main_window = self # In this context, self should be the YTSageApp instance
- if not isinstance(main_window, QMainWindow):
- # If the structure is different, this might need adjustment
- # Maybe self.parentWidget() or similar depending on how Mixin is used
- print("Warning: Cannot find main window to access merge checkbox.")
- merge_checkbox = None
- else:
- merge_checkbox = getattr(main_window, 'merge_subs_checkbox', None)
+ # Add subtitle options
+ self.subtitle_combo.addItem("Select subtitle language")
+ # Filter and add subtitles
+ filter_text = self.subtitle_filter_input.text().lower()
- if dialog.exec(): # If user clicks OK
- self.selected_subtitles = dialog.get_selected_subtitles()
- print(f"Selected subtitles: {self.selected_subtitles}")
- # Update UI to reflect selection
- count = len(self.selected_subtitles)
- self.selected_subs_label.setText(f"{count} selected")
- self.subtitle_select_btn.setProperty("subtitlesSelected", count > 0)
+ # Add manual subtitles
+ for lang_code, subtitle_info in self.available_subtitles.items():
+ if not filter_text or filter_text in lang_code.lower():
+ self.subtitle_combo.addItem(f"{lang_code} - Manual")
- # Enable/disable the merge checkbox in the parent window
- if merge_checkbox:
- # Only enable merge checkbox if we're not in Audio Only mode
- is_audio_only = hasattr(main_window, 'audio_button') and main_window.audio_button.isChecked()
- # In audio-only mode, we still allow subtitle selection but not merging
- should_enable = count > 0 and not is_audio_only
- merge_checkbox.setEnabled(should_enable)
- else:
- print("Warning: merge_subs_checkbox not found on parent window.")
+ # Add auto-generated subtitles
+ for lang_code, subtitle_info in self.available_automatic_subtitles.items():
+ if not filter_text or filter_text in lang_code.lower():
+ self.subtitle_combo.addItem(f"{lang_code} - Auto-generated")
- # Re-apply stylesheet to update button border if property changed
- self.subtitle_select_btn.style().unpolish(self.subtitle_select_btn)
- self.subtitle_select_btn.style().polish(self.subtitle_select_btn)
- # No else needed for cancel, state remains unchanged
+ def filter_subtitles(self):
+ self.subtitle_filter = self.subtitle_filter_input.text()
+ self.update_subtitle_list()
def download_thumbnail(self, url):
try:
@@ -271,6 +285,10 @@ class VideoInfoMixin:
except Exception as e:
print(f"Error loading thumbnail: {str(e)}")
+ def toggle_save_thumbnail(self):
+ self.save_thumbnail = self.save_thumbnail_checkbox.isChecked()
+ print(f"Save thumbnail toggled: {self.save_thumbnail}") # Debug print
+
def download_thumbnail_file(self, video_url, path):
if not self.save_thumbnail:
return False
diff --git a/ytsage_style.py b/ytsage_style.py
index 6ad7f38..84c3b8d 100644
--- a/ytsage_style.py
+++ b/ytsage_style.py
@@ -1,24 +1,24 @@
MAIN_STYLE = """
QMainWindow {
- background-color: #15181b;
+ background-color: #2b2b2b;
}
QWidget {
- background-color: #15181b;
+ background-color: #2b2b2b;
color: #ffffff;
font-size: 12px;
}
QLineEdit {
padding: 8px;
- border: 2px solid #1b2021;
+ border: 2px solid #3d3d3d;
border-radius: 4px;
- background-color: #1b2021;
+ background-color: #363636;
color: #ffffff;
- selection-background-color: #c90000;
+ selection-background-color: #ff0000;
selection-color: #ffffff;
}
QPushButton {
padding: 8px 15px;
- background-color: #c90000;
+ background-color: #ff0000; /* YouTube red */
border: none;
border-radius: 4px;
color: white;
@@ -26,33 +26,33 @@ QPushButton {
min-height: 20px;
}
QPushButton:hover {
- background-color: #a50000;
+ background-color: #cc0000; /* Darker red on hover */
}
QPushButton:pressed {
- background-color: #800000;
+ background-color: #990000; /* Even darker red when pressed */
}
QPushButton:disabled {
- background-color: #666666;
+ background-color: #666666; /* Gray when disabled */
color: #999999;
}
QTableWidget {
- border: 2px solid #1b2021;
+ border: 2px solid #3d3d3d;
border-radius: 4px;
- background-color: #1b2021;
- gridline-color: #1b2021;
- selection-background-color: #c90000;
+ background-color: #363636;
+ gridline-color: #3d3d3d;
+ selection-background-color: #ff0000;
selection-color: #ffffff;
}
QHeaderView::section {
- background-color: #15181b;
+ background-color: #2b2b2b;
padding: 5px;
- border: 1px solid #1b2021;
+ border: 1px solid #3d3d3d;
color: #ffffff;
font-weight: bold;
}
QScrollBar:vertical {
border: none;
- background-color: #15181b;
+ background-color: #2b2b2b;
width: 12px;
margin: 0px;
}
@@ -62,27 +62,27 @@ QScrollBar::handle:vertical {
border-radius: 6px;
}
QScrollBar::handle:vertical:hover {
- background-color: #c90000;
+ background-color: #ff0000;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
height: 0px;
}
QProgressBar {
- border: 2px solid #1b2021;
+ border: 2px solid #3d3d3d;
border-radius: 4px;
text-align: center;
color: white;
- background-color: #1b2021;
+ background-color: #363636;
}
QProgressBar::chunk {
- background-color: #c90000;
+ background-color: #ff0000;
border-radius: 2px;
}
QComboBox {
padding: 5px;
- border: 2px solid #1b2021;
+ border: 2px solid #3d3d3d;
border-radius: 4px;
- background-color: #1b2021;
+ background-color: #363636;
color: #ffffff;
min-height: 20px;
}
@@ -106,25 +106,25 @@ QCheckBox::indicator {
}
QCheckBox::indicator:unchecked {
border: 2px solid #666666;
- background: #15181b;
+ background: #2b2b2b;
}
QCheckBox::indicator:checked {
- border: 2px solid #c90000;
- background: #c90000;
+ border: 2px solid #ff0000;
+ background: #ff0000;
}
QLabel {
color: #ffffff;
}
QTextEdit, QPlainTextEdit {
- background-color: #1b2021;
+ background-color: #363636;
color: #ffffff;
- border: 2px solid #1b2021;
+ border: 2px solid #3d3d3d;
border-radius: 4px;
- selection-background-color: #c90000;
+ selection-background-color: #ff0000;
selection-color: #ffffff;
}
QMessageBox {
- background-color: #15181b;
+ background-color: #2b2b2b;
}
QMessageBox QLabel {
color: #ffffff;
diff --git a/ytsage_utils.py b/ytsage_utils.py
index 698625d..1666c83 100644
--- a/ytsage_utils.py
+++ b/ytsage_utils.py
@@ -4,7 +4,6 @@ import json
from pathlib import Path
import subprocess
import tempfile
-import shutil
from ytsage_ffmpeg import check_ffmpeg_installed, get_ffmpeg_install_path
def check_ffmpeg():
@@ -50,24 +49,45 @@ def check_ffmpeg():
return False
def get_yt_dlp_path():
- """Get the yt-dlp command or path, prioritizing the system PATH."""
+ """Get the appropriate yt-dlp path with enhanced error handling."""
try:
- # Use shutil.which to find yt-dlp in the system's PATH
- yt_dlp_executable = shutil.which('yt-dlp')
-
- if yt_dlp_executable:
- print(f"Found yt-dlp executable in PATH: {yt_dlp_executable}")
- return yt_dlp_executable
+ if getattr(sys, 'frozen', False):
+ if sys.platform == 'darwin':
+ # For macOS .app bundle
+ if 'Contents/MacOS' in sys.executable:
+ base_path = os.path.dirname(sys.executable)
+ else:
+ # Fallback to user's home directory for macOS
+ base_path = os.path.expanduser('~/Library/Application Support/YTSage')
+ elif sys.platform == 'win32':
+ # For Windows executable
+ app_data = os.getenv('APPDATA')
+ base_path = os.path.join(app_data, 'YTSage') if app_data else os.path.dirname(sys.executable)
+ else:
+ # For Linux AppImage or binary
+ if 'APPIMAGE' in os.environ:
+ 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)
+
+ # Create directory if it doesn't exist
+ try:
+ os.makedirs(base_path, exist_ok=True)
+ except Exception as e:
+ print(f"Error creating directory: {e}")
+ base_path = os.path.dirname(sys.executable)
+
+ return os.path.join(base_path, 'yt-dlp.exe' if sys.platform == 'win32' else 'yt-dlp')
else:
- # If not found in PATH, assume 'yt-dlp' is the command name
- print("yt-dlp not found in PATH. Will attempt to use 'yt-dlp' as the command.")
- return 'yt-dlp'
-
+ # 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 finding yt-dlp path: {e}")
- # Fallback to the command name on any error
- print("An error occurred during yt-dlp path detection. Falling back to command 'yt-dlp'.")
- return 'yt-dlp'
+ 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):
"""Load saved download path with enhanced error handling."""