v4.9.0 - Refactor (#41)
* fix imports
remove unused imports
use full import path
sort import (1. Standard Library, 2. Third-Party, 3. Local) in alphabetic.
* - remove: Method 3 from src.core.ytsage_downlader:cleanup_subtitle_file
- it could delete the subtitle file of other movies if it present in same directory as it scane recursively.
- refactor: migrate from os.path to pathlib.Path for path handling
- Replaced os.path methods with pathlib.Path to improve readability,
- avoid repeatation.
- cross-platform compatibility, and maintain cleaner code.
- improve: enhance code readability
- Standardized string literals to use double quotes for consistency
- Removed unnecessary spaces to maintain cleaner formatting
- Applied code formatting for better readability and maintainability
* - add: ytsage_constants.py file for one place to store all constants.
- imporve: return type hint for function.
- remove: src/gui/ytsage_gui_dialogs.py file to avoid repetation
- src/gui/dialogs is renamed to src/gui/ytsage_gui_dialogs for same naming convection. (future import will remains same)
- use of src/gui/ytsage_gui_dialogs/__init__.py to import the dilogs modules.
- change: variable self.parent to self._parent so it does not overwrite the parent()
- add type hint checking.
* - refactor: QMetaObject.invokeMethod to Signal
- I encounter error with incokeMethod. Could not solve it.
- So, Changed it to Signal to match app code language.
- implement: the ytsage_constants.py to code
- remove: unnecessary logic
- remove: repetitive code logic.
- update: yt-dlp logic for src\gui\ytsage_gui_dialogs\ytsage_dialogs_update:_update_binary
- yt-dlp update logic will use `yt-dlp -U`
* refactor: remove unused imports and streamline code formatting across multiple files
* - **refactor: drop `pygame` in favor of built-in `PySide6` sound**
- Removed `pygame` dependency (too heavy just for notifications).
- Replaced with `QSoundEffect`, which is lightweight and built into `PySide6`.
- Dropped `pygame.mixer` + threading → Qt handles async playback.
- Implemented sound playback with `QUrl.fromLocalFile()` and `.play()`.
- Added `setVolume(0.9)` as a configurable example.
- Converted notification sound from `.mp3` to `.wav` (only format supported).
* **refactor(utils): simplify logger module**
- Moved `logger` to `src.utils`
- Removed unnecessary import checks (logger is always available)
- Replaced `raise` statements with error logging to prevent crashes
- Use `logger.exception()` in `except` blocks to capture traceback (logged as error)
**style: remove redundant str() in f-strings**
- Dropped explicit `str()` calls inside f-strings
- f-strings already call `str()` under the hood
**chore: add type hints for GUI mixins**
- Added type hints for `FormatTableMixin` (`src.gui.ytsage_gui_format_table`)
- Added type hints for `VideoInfoMixin` (`src.gui.ytsage_gui_video_info`)
- Improves autocomplete and type safety in IDEs
* - **introduce the `ytsage_config_manager.py` module to manage app setting.**
- Loads settings from a JSON config file (`APP_CONFIG_FILE`).
- Creates the config file with default values if missing or corrupt.
- Retrieves, sets, and deletes settings using simple dot-separated keys.
- Provides safe error handling with logging instead of raising exceptions.
- Persists updates back to disk automatically.
- **Usage**
```python
from src.utils.ytsage_config_manager import ConfigManager
download_path = ConfigManager.get("download_path")
ConfigManager.set("download_path", "D:/Downloads")
last_check = ConfigManager.get("cached_versions.ytdlp.last_check")
ConfigManager.delete("cached_versions.ffmpeg.path")
```
* **Refactore: pkg_resources with importlib.metadata.version**
- UserWarning: pkg_resources is deprecated as an API.
- See https://setuptools.pypa.io/en/latest/pkg_resources.html.
- The pkg_resources package is slated for removal as early as 2025-11-30.
* **refactore: notification sound**
- `QSoundEffect` is chnaged back to `pyglet` as per mainter `@oop7` choise.
- simplify the logic.
* remove: import check, it should always work.
* **fix: runtime error**
- yt_dlp moved from `--excludes` to `--packages` in `build-windows.yml`
- In frozen build, logger will not log to consol. insted will log to file.
- In frozen build, `app_dir` is next to `.exe` file.
* chang back to checking import for ytdlp
* Bump version to 4.8.1
Update version references from 4.8.0b to 4.8.1 in __init__.py, main app, and About dialog to reflect the new release.
* Update asset paths in build workflows
Changed asset inclusion and screenshot removal paths from 'assets' to 'lib/assets' in Linux, macOS, and Windows build workflows to reflect new directory structure and ensure screenshots are excluded from packaged builds.
* Bump version to 4.8.2
Update version references from 4.8.1 to 4.8.2 in source files and documentation to prepare for a new patch release.
* Fix asset include path in Windows build workflow
Corrects the syntax for including asset files in the build-windows.yml workflow by changing 'assets,lib/assets' to 'assets=lib/assets'. This ensures assets are properly mapped during the build process.
* Update release tag examples in CI/CD README
Changed the example git tag commands from v4.8.0 and v4.8.2 to v4.8.1 for consistency in the CI/CD documentation.
* Fix include-files mapping in Windows build workflow
Changed the cx_Freeze --include-files argument from '=' to ':' for source:destination mapping in build-windows.yml. This resolves an issue where '=' was treated as a literal path, ensuring assets are correctly copied to the destination directory.
* Remove redundant comments in build-windows workflow
Deleted comments explaining the colon usage for source:destination mapping in the cx_Freeze CLI, as the mapping is already clear from the context.
* Refactor Windows build to use cx_Freeze setup script
Replaces direct cx_Freeze CLI calls with dynamically generated setup scripts for both standard and FFmpeg builds. This improves maintainability and flexibility of build configuration in the GitHub Actions workflow.
* Bump version to 4.8.3
Updated version references from 4.8.2 to 4.8.3 in __init__.py, main app, and About dialog to reflect the new release.
* revert(build): move yt_dlp from --packages to --excludes in build-windows.yml
---------
Co-authored-by: Your Name <mohamed.mohamed112@ai.mnu.edu.eg>
This commit is contained in:
@@ -7,9 +7,9 @@ from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QObject, QThread, Signal
|
||||
|
||||
from src.core.ytsage_logging import logger
|
||||
from src.core.ytsage_yt_dlp import get_yt_dlp_path
|
||||
from src.utils.ytsage_constants import SUBPROCESS_CREATIONFLAGS
|
||||
from src.utils.ytsage_logger import logger
|
||||
|
||||
try:
|
||||
import yt_dlp # Keep yt_dlp import here - only downloader uses it.
|
||||
@@ -38,6 +38,7 @@ class DownloadThread(QThread):
|
||||
error_signal = Signal(str)
|
||||
file_exists_signal = Signal(str) # New signal for file existence
|
||||
update_details = Signal(str) # New signal for filename, speed, ETA
|
||||
update_details = Signal(str) # New signal for filename, speed, ETA
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -97,9 +98,10 @@ class DownloadThread(QThread):
|
||||
try:
|
||||
file_path.unlink(missing_ok=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting {file_path.name}: {str(e)}")
|
||||
logger.exception(f"Error deleting {file_path.name}: {e}")
|
||||
except Exception as e:
|
||||
self.error_signal.emit(f"Error cleaning partial files: {str(e)}")
|
||||
logger.exception(f"Error cleaning partial files: {e}")
|
||||
self.error_signal.emit(f"Error cleaning partial files: {e}")
|
||||
|
||||
def cleanup_subtitle_files(self) -> None:
|
||||
"""Delete subtitle files after they have been merged into the video file"""
|
||||
@@ -111,7 +113,7 @@ class DownloadThread(QThread):
|
||||
logger.debug(f"Deleted subtitle file: {path.name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting subtitle file {path}: {e}")
|
||||
logger.exception(f"Error deleting subtitle file {path}: {e}")
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -130,7 +132,7 @@ class DownloadThread(QThread):
|
||||
else:
|
||||
logger.debug(f"Deleted {deleted_count[1]} of {len(new_subtitle_files)} new subtitle files")
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning subtitle files: {str(e)}")
|
||||
logger.exception(f"Error cleaning subtitle files: {e}")
|
||||
|
||||
def check_file_exists(self) -> bool | None:
|
||||
"""Check if the file already exists before downloading"""
|
||||
@@ -138,18 +140,21 @@ class DownloadThread(QThread):
|
||||
logger.debug("Starting file existence check")
|
||||
# Use yt-dlp to get the filename without downloading, suppressing warnings
|
||||
ydl_opts_check = {
|
||||
"logger": logger, # passed app logger
|
||||
"quiet": True,
|
||||
"skip_download": True,
|
||||
"no_warnings": True, # <-- Suppress warnings during check
|
||||
"ignoreerrors": True, # Also ignore other potential errors during this check
|
||||
"outtmpl": {"default": f"{self.path.as_posix()}/%(title)s.%(ext)s"},
|
||||
"outtmpl": {"default": str(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"] = str(self.cookie_file)
|
||||
elif self.browser_cookies:
|
||||
ydl_opts_check["cookiesfrombrowser"] = (self.browser_cookies.split(':')[0],
|
||||
self.browser_cookies.split(':')[1] if ':' in self.browser_cookies else None)
|
||||
ydl_opts_check["cookiesfrombrowser"] = (
|
||||
self.browser_cookies.split(":")[0],
|
||||
self.browser_cookies.split(":")[1] if ":" in self.browser_cookies else None,
|
||||
)
|
||||
|
||||
if YT_DLP_AVAILABLE:
|
||||
with yt_dlp.YoutubeDL(ydl_opts_check) as ydl:
|
||||
@@ -178,10 +183,7 @@ class DownloadThread(QThread):
|
||||
return False # Proceed with download attempt
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking file existence: {str(e)}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.exception(f"Error checking file existence: {e}")
|
||||
return None
|
||||
|
||||
def _build_yt_dlp_command(self) -> list:
|
||||
@@ -201,6 +203,7 @@ class DownloadThread(QThread):
|
||||
try:
|
||||
if YT_DLP_AVAILABLE:
|
||||
ydl_opts = {
|
||||
"logger": logger,
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"skip_download": True,
|
||||
@@ -214,7 +217,7 @@ class DownloadThread(QThread):
|
||||
logger.debug(f"Detected audio-only format for ID: {clean_format_id}")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking if format is audio-only: {e}")
|
||||
logger.exception(f"Error checking if format is audio-only: {e}")
|
||||
|
||||
# For audio-only formats, don't try to merge with video
|
||||
if is_audio_format:
|
||||
@@ -229,12 +232,9 @@ class DownloadThread(QThread):
|
||||
try:
|
||||
format_ext = None
|
||||
logger.debug(f"Getting format information for format ID: {self.format_id} (using: {clean_format_id})")
|
||||
|
||||
if YT_DLP_AVAILABLE:
|
||||
ydl_opts = {
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"skip_download": True,
|
||||
}
|
||||
ydl_opts = {"quiet": True, "no_warnings": True, "skip_download": True, "logger": logger}
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(self.url, download=False) or {}
|
||||
# Look for the clean format ID first
|
||||
@@ -254,7 +254,7 @@ class DownloadThread(QThread):
|
||||
# Ensure output matches the selected format - only for video formats
|
||||
cmd.extend(["--merge-output-format", format_ext])
|
||||
except Exception as e:
|
||||
logger.debug(f"Error detecting format extension: {e}")
|
||||
logger.exception(f"Error detecting format extension: {e}")
|
||||
# If we can't determine the format, don't specify merge-output-format
|
||||
pass
|
||||
else:
|
||||
@@ -272,7 +272,7 @@ class DownloadThread(QThread):
|
||||
else:
|
||||
output_template = f"{base_path}/%(title)s_%(resolution)s.%(ext)s"
|
||||
|
||||
cmd.extend(["-o", output_template])
|
||||
cmd.extend(["-o", str(output_template)])
|
||||
|
||||
# Add common options
|
||||
cmd.append("--force-overwrites")
|
||||
@@ -295,7 +295,7 @@ class DownloadThread(QThread):
|
||||
lang_code = sub_selection.split(" - ")[0]
|
||||
lang_codes.append(lang_code)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not parse subtitle selection '{sub_selection}': {e}")
|
||||
logger.exception(f"Could not parse subtitle selection '{sub_selection}': {e}")
|
||||
|
||||
if lang_codes:
|
||||
cmd.extend(["--sub-langs", ",".join(lang_codes)])
|
||||
@@ -366,7 +366,7 @@ class DownloadThread(QThread):
|
||||
self.initial_subtitle_files.add(file)
|
||||
logger.debug(f"Found {len(self.initial_subtitle_files)} existing subtitle files before download")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error scanning for initial subtitle files: {e}")
|
||||
logger.exception(f"Error scanning for initial subtitle files: {e}")
|
||||
|
||||
if self.use_direct_command:
|
||||
# Use direct CLI command instead of Python API
|
||||
@@ -377,10 +377,8 @@ class DownloadThread(QThread):
|
||||
|
||||
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()
|
||||
logger.critical(f"Critical error in download thread: {e}", exc_info=True)
|
||||
self.error_signal.emit(f"Critical error in download thread: {e}")
|
||||
|
||||
def _run_direct_command(self) -> None:
|
||||
"""Run yt-dlp as a direct command line process instead of using Python API."""
|
||||
@@ -460,7 +458,8 @@ class DownloadThread(QThread):
|
||||
self.cleanup_partial_files()
|
||||
|
||||
except Exception as e:
|
||||
self.error_signal.emit(f"Error in direct command: {str(e)}")
|
||||
logger.exception(f"Error in direct command: {e}")
|
||||
self.error_signal.emit(f"Error in direct command: {e}")
|
||||
self.cleanup_partial_files()
|
||||
|
||||
def _parse_output_line(self, line) -> None:
|
||||
@@ -513,7 +512,7 @@ class DownloadThread(QThread):
|
||||
else:
|
||||
self.status_signal.emit(f"⏬ Downloading...")
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting filename from line '{line}': {e}")
|
||||
logger.exception(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
|
||||
|
||||
@@ -535,16 +534,16 @@ class DownloadThread(QThread):
|
||||
)
|
||||
if subtitle_match:
|
||||
subtitle_file = subtitle_match.group(1).strip()
|
||||
|
||||
|
||||
# Clean up the path - remove any duplicated directory paths
|
||||
# Sometimes yt-dlp output contains malformed paths like "dir: dir/file"
|
||||
if ":" in subtitle_file and os.name == 'nt': # Windows paths
|
||||
if ":" in subtitle_file and os.name == "nt": # Windows paths
|
||||
# Look for pattern like "C:\path: C:\path\file" and extract the latter
|
||||
colon_parts = subtitle_file.split(": ")
|
||||
if len(colon_parts) > 1:
|
||||
# Take the last part which should be the actual file path
|
||||
subtitle_file = colon_parts[-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
|
||||
@@ -607,7 +606,7 @@ class DownloadThread(QThread):
|
||||
self.update_details.emit(status)
|
||||
except Exception as e:
|
||||
# If parsing fails, just show basic status (maybe log the error)
|
||||
logger.error(f"Error parsing download details line: {line} -> {e}")
|
||||
logger.exception(f"Error parsing download details line: {line} -> {e}")
|
||||
pass # Keep basic status emission below if needed, or emit generic details
|
||||
|
||||
# Check for post-processing
|
||||
|
||||
+21
-15
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
from src.core.ytsage_logging import logger
|
||||
from src.utils.ytsage_logger import logger
|
||||
from src.utils.ytsage_constants import (
|
||||
FFMPEG_7Z_DOWNLOAD_URL,
|
||||
FFMPEG_7Z_SHA256_URL,
|
||||
@@ -46,7 +46,7 @@ def download_file(url, dest_path, progress_callback=None) -> bool:
|
||||
progress_callback(f"⚡ Downloading FFmpeg components... {progress}%")
|
||||
return True
|
||||
except requests.RequestException as e:
|
||||
logger.info(f"Download error: {str(e)}")
|
||||
logger.info(f"Download error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ def verify_sha256(file_path, expected_hash_url) -> bool:
|
||||
logger.info(f"Actual: {actual_hash}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.info(f"⚠️ SHA-256 verification error: {str(e)}")
|
||||
logger.info(f"⚠️ SHA-256 verification error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ def get_ffmpeg_path() -> str | Path:
|
||||
ffmpeg_path = result.stdout.strip()
|
||||
return ffmpeg_path
|
||||
except Exception as e:
|
||||
logger.error(f"Error finding ffmpeg in PATH: {e}")
|
||||
logger.exception(f"Error finding ffmpeg in PATH: {e}")
|
||||
|
||||
# If not found in PATH, check the installation directory
|
||||
ffmpeg_install_path = get_ffmpeg_install_path()
|
||||
@@ -171,7 +171,7 @@ def check_ffmpeg_installed() -> bool:
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.info(f"FFmpeg check error: {str(e)}")
|
||||
logger.info(f"FFmpeg check error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ def install_ffmpeg_windows() -> bool:
|
||||
timeout=300,
|
||||
) # 5-minute timeout
|
||||
except Exception as e:
|
||||
logger.error(f"7z extraction failed: {str(e)}, trying zip fallback...")
|
||||
logger.exception(f"7z extraction failed: {e}, trying zip fallback...")
|
||||
use_7zip = False
|
||||
else:
|
||||
logger.error("SHA-256 verification failed for 7z file, trying zip fallback...")
|
||||
@@ -236,7 +236,8 @@ def install_ffmpeg_windows() -> bool:
|
||||
temp_file,
|
||||
progress_callback=lambda msg: logger.debug(msg),
|
||||
):
|
||||
raise Exception("Failed to download FFmpeg (both 7z and zip methods failed)")
|
||||
logger.exception("Failed to download FFmpeg (both 7z and zip methods failed)")
|
||||
return False
|
||||
|
||||
logger.info("Extracting FFmpeg components from zip archive...")
|
||||
try:
|
||||
@@ -245,7 +246,8 @@ def install_ffmpeg_windows() -> bool:
|
||||
with zipfile.ZipFile(temp_file, "r") as zip_ref:
|
||||
zip_ref.extractall(extract_dir)
|
||||
except Exception as e:
|
||||
raise Exception(f"Extraction failed: {str(e)}")
|
||||
logger.exception(f"Extraction failed: {e}")
|
||||
return False
|
||||
|
||||
logger.info("Configuring system paths...")
|
||||
# Add to System Path
|
||||
@@ -265,13 +267,14 @@ def install_ffmpeg_windows() -> bool:
|
||||
|
||||
# Verify installation
|
||||
if not check_ffmpeg_installed():
|
||||
raise Exception("FFmpeg installation verification failed")
|
||||
logger.error("FFmpeg installation verification failed")
|
||||
return False
|
||||
|
||||
logger.info("FFmpeg installation completed successfully!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error installing FFmpeg: {str(e)}")
|
||||
logger.exception(f"Error installing FFmpeg: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -298,12 +301,13 @@ def install_ffmpeg_macos() -> bool:
|
||||
|
||||
# Verify installation
|
||||
if not check_ffmpeg_installed():
|
||||
raise Exception("FFmpeg installation verification failed")
|
||||
logger.error("FFmpeg installation verification failed")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error installing FFmpeg: {str(e)}")
|
||||
logger.exception(f"Error installing FFmpeg: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -329,16 +333,18 @@ def install_ffmpeg_linux() -> bool:
|
||||
# Universal snap package
|
||||
subprocess.run(["sudo", "snap", "install", "ffmpeg"], check=True, timeout=300)
|
||||
else:
|
||||
raise Exception("No supported package manager found")
|
||||
logger.error("No supported package manager found")
|
||||
return False
|
||||
|
||||
# Verify installation
|
||||
if not check_ffmpeg_installed():
|
||||
raise Exception("FFmpeg installation verification failed")
|
||||
logger.error("FFmpeg installation verification failed")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error installing FFmpeg: {str(e)}")
|
||||
logger.exception(f"Error installing FFmpeg: {e}")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
"""
|
||||
YTSage logging configuration using loguru.
|
||||
|
||||
This module provides centralized logging configuration for the entire YTSage application.
|
||||
It replaces the inefficient print statements with structured logging using loguru.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from src.utils.ytsage_constants import APP_LOG_DIR
|
||||
|
||||
# Try to import loguru, but handle case where it might not be available
|
||||
try:
|
||||
from loguru import logger
|
||||
|
||||
LOGURU_AVAILABLE = True
|
||||
except ImportError:
|
||||
LOGURU_AVAILABLE = False
|
||||
|
||||
# Create a dummy logger class that does nothing
|
||||
class DummyLogger:
|
||||
def info(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def debug(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def warning(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def error(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def critical(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def remove(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def add(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def bind(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
@property
|
||||
def _core(self):
|
||||
class Core:
|
||||
handlers = []
|
||||
|
||||
return Core()
|
||||
|
||||
logger = DummyLogger()
|
||||
|
||||
|
||||
def setup_logging():
|
||||
"""
|
||||
Configure loguru logging for YTSage application.
|
||||
|
||||
Sets up multiple log levels and outputs:
|
||||
- Console output for INFO and above
|
||||
- File output for DEBUG and above
|
||||
- Separate error log file for ERROR and above
|
||||
"""
|
||||
|
||||
if not LOGURU_AVAILABLE:
|
||||
return logger
|
||||
|
||||
# Remove default logger to avoid duplicate output
|
||||
try:
|
||||
logger.remove()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Get the application data directory with fallbacks
|
||||
try:
|
||||
# logic moved to src\utils\ytsage_constants.py
|
||||
log_dir = APP_LOG_DIR
|
||||
except Exception:
|
||||
# Ultimate fallback - use current directory
|
||||
log_dir = Path.cwd() / "logs"
|
||||
|
||||
# Create log directory if it doesn't exist
|
||||
try:
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
except Exception:
|
||||
# If we can't create the log directory, fall back to current directory
|
||||
log_dir = Path.cwd()
|
||||
try:
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
except Exception:
|
||||
pass # If we still can't create it, we'll just log to console
|
||||
|
||||
# Console handler - INFO and above, with colors
|
||||
# Check if stdout is available (it might be None in PyInstaller windowed apps)
|
||||
stdout_available = sys.stdout is not None
|
||||
|
||||
if stdout_available:
|
||||
try:
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
level="INFO",
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
||||
colorize=True,
|
||||
catch=True,
|
||||
)
|
||||
except Exception:
|
||||
# Fallback to basic console logging without colors
|
||||
try:
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
level="INFO",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}",
|
||||
catch=True,
|
||||
)
|
||||
except Exception:
|
||||
stdout_available = False
|
||||
|
||||
# If stdout is not available, try stderr or skip console logging entirely
|
||||
if not stdout_available:
|
||||
try:
|
||||
if sys.stderr is not None:
|
||||
logger.add(
|
||||
sys.stderr,
|
||||
level="WARNING", # Only warnings and errors to stderr
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}",
|
||||
catch=True,
|
||||
)
|
||||
except Exception:
|
||||
# If even stderr fails, we'll rely only on file logging
|
||||
pass
|
||||
|
||||
# Only add file handlers if we successfully created a log directory
|
||||
if log_dir and log_dir.exists():
|
||||
try:
|
||||
# Main log file - DEBUG and above, with rotation
|
||||
logger.add(
|
||||
log_dir / "ytsage.log",
|
||||
level="DEBUG",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
|
||||
rotation="10 MB", # Rotate when file reaches 10MB
|
||||
retention="7 days", # Keep logs for 7 days
|
||||
compression="zip", # Compress old logs
|
||||
catch=True,
|
||||
)
|
||||
|
||||
# Error log file - ERROR and above only
|
||||
logger.add(
|
||||
log_dir / "ytsage_errors.log",
|
||||
level="ERROR",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
|
||||
rotation="5 MB",
|
||||
retention="30 days", # Keep error logs longer
|
||||
compression="zip",
|
||||
catch=True,
|
||||
)
|
||||
except Exception as e:
|
||||
# If file logging fails, just log to console
|
||||
logger.warning(f"Could not set up file logging: {e}")
|
||||
|
||||
# Log startup message if we have any handlers
|
||||
if logger._core.handlers:
|
||||
logger.info("YTSage logging system initialized")
|
||||
if log_dir and log_dir.exists():
|
||||
logger.debug(f"Log directory: {log_dir}")
|
||||
else:
|
||||
logger.warning("File logging disabled - could not create log directory")
|
||||
|
||||
# If no handlers were successfully added, add a null handler to prevent errors
|
||||
if not logger._core.handlers:
|
||||
# Add a minimal handler that just discards messages
|
||||
# This prevents loguru from complaining about no handlers
|
||||
import tempfile
|
||||
|
||||
try:
|
||||
# Try to add a temporary file handler as last resort
|
||||
temp_log = Path(tempfile.gettempdir()) / "ytsage_temp.log"
|
||||
logger.add(temp_log, level="ERROR", catch=True)
|
||||
except Exception:
|
||||
# If even that fails, we're in a very restricted environment
|
||||
# loguru should handle this gracefully with its internal fallbacks
|
||||
pass
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
def get_logger(name: str | None = None):
|
||||
"""
|
||||
Get a logger instance for a specific module.
|
||||
|
||||
Args:
|
||||
name: Name of the module/component requesting the logger
|
||||
|
||||
Returns:
|
||||
Configured logger instance
|
||||
"""
|
||||
if name:
|
||||
return logger.bind(name=name)
|
||||
return logger
|
||||
|
||||
|
||||
# Initialize logging when module is imported - with maximum safety
|
||||
_setup_complete = False
|
||||
|
||||
|
||||
def safe_setup():
|
||||
"""Safely initialize logging with multiple fallback strategies."""
|
||||
global _setup_complete
|
||||
if _setup_complete:
|
||||
return logger
|
||||
|
||||
try:
|
||||
setup_logging()
|
||||
_setup_complete = True
|
||||
except Exception:
|
||||
# If all else fails, create an even simpler logger that just prints
|
||||
if LOGURU_AVAILABLE:
|
||||
try:
|
||||
logger.remove()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# At this point, just ensure we have something that won't crash
|
||||
_setup_complete = True
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
# Try to set up logging, but don't let it crash the module import
|
||||
try:
|
||||
safe_setup()
|
||||
except Exception:
|
||||
# Ultimate fallback - the module will still import successfully
|
||||
pass
|
||||
|
||||
# Export the main logger for convenience
|
||||
__all__ = ["logger", "get_logger", "setup_logging"]
|
||||
+122
-94
@@ -4,28 +4,13 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from importlib.metadata import PackageNotFoundError
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from importlib.metadata import version as importlib_version
|
||||
from importlib.metadata import PackageNotFoundError as ImportlibPackageNotFoundError
|
||||
|
||||
def get_version(package_name: str) -> str:
|
||||
return importlib_version(package_name)
|
||||
|
||||
PackageNotFoundError = ImportlibPackageNotFoundError
|
||||
except ImportError:
|
||||
# Fallback for older Python versions
|
||||
import pkg_resources
|
||||
def get_version(package_name: str) -> str:
|
||||
return pkg_resources.get_distribution(package_name).version
|
||||
PackageNotFoundError = pkg_resources.DistributionNotFound
|
||||
|
||||
import requests
|
||||
from packaging import version
|
||||
|
||||
from src.core.ytsage_ffmpeg import check_ffmpeg_installed, get_ffmpeg_install_path
|
||||
from src.core.ytsage_logging import logger
|
||||
from src.core.ytsage_yt_dlp import get_yt_dlp_path
|
||||
from src.utils.ytsage_constants import (
|
||||
APP_CONFIG_FILE,
|
||||
@@ -35,6 +20,25 @@ from src.utils.ytsage_constants import (
|
||||
YTDLP_APP_BIN_PATH,
|
||||
YTDLP_DOWNLOAD_URL,
|
||||
)
|
||||
from src.utils.ytsage_logger import logger
|
||||
|
||||
try:
|
||||
from importlib.metadata import PackageNotFoundError as ImportlibPackageNotFoundError
|
||||
from importlib.metadata import version as importlib_version
|
||||
|
||||
def get_version(package_name: str) -> str:
|
||||
return importlib_version(package_name)
|
||||
|
||||
PackageNotFoundError = ImportlibPackageNotFoundError
|
||||
except ImportError:
|
||||
# Fallback for older Python versions
|
||||
import pkg_resources
|
||||
|
||||
def get_version(package_name: str) -> str:
|
||||
return pkg_resources.get_distribution(package_name).version
|
||||
|
||||
PackageNotFoundError = pkg_resources.DistributionNotFound
|
||||
|
||||
|
||||
# Cache for version information to avoid delays
|
||||
_version_cache = {
|
||||
@@ -108,7 +112,7 @@ def load_version_cache_from_config() -> None:
|
||||
if tool_name in _version_cache:
|
||||
_version_cache[tool_name].update(cache_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading version cache: {e}")
|
||||
logger.exception(f"Error loading version cache: {e}")
|
||||
|
||||
|
||||
def save_version_cache_to_config() -> None:
|
||||
@@ -118,7 +122,7 @@ def save_version_cache_to_config() -> None:
|
||||
config["cached_versions"] = _version_cache.copy()
|
||||
save_config(config)
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving version cache: {e}")
|
||||
logger.exception(f"Error saving version cache: {e}")
|
||||
|
||||
|
||||
def get_ytdlp_version_cached() -> str:
|
||||
@@ -140,7 +144,7 @@ def get_ytdlp_version_cached() -> str:
|
||||
|
||||
return version_info
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting cached yt-dlp version: {e}")
|
||||
logger.exception(f"Error getting cached yt-dlp version: {e}")
|
||||
return "Error getting version"
|
||||
|
||||
|
||||
@@ -164,7 +168,7 @@ def get_ffmpeg_version_cached() -> str:
|
||||
|
||||
return version_info
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting cached FFmpeg version: {e}")
|
||||
logger.exception(f"Error getting cached FFmpeg version: {e}")
|
||||
return "Error getting version"
|
||||
|
||||
|
||||
@@ -182,7 +186,7 @@ def refresh_version_cache(force=False) -> bool:
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error refreshing version cache: {e}")
|
||||
logger.exception(f"Error refreshing version cache: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -215,7 +219,7 @@ def get_ytdlp_version_direct(yt_dlp_path=None) -> str:
|
||||
else:
|
||||
return "Error getting version"
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting yt-dlp version: {e}")
|
||||
logger.exception(f"Error getting yt-dlp version: {e}")
|
||||
return "Error getting version"
|
||||
|
||||
|
||||
@@ -269,10 +273,10 @@ def get_ffmpeg_version_direct() -> str:
|
||||
return "Unknown version"
|
||||
return "Not found"
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting FFmpeg version from install path: {e}")
|
||||
logger.exception(f"Error getting FFmpeg version from install path: {e}")
|
||||
return "Not found"
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting FFmpeg version: {e}")
|
||||
logger.exception(f"Error getting FFmpeg version: {e}")
|
||||
return "Error getting version"
|
||||
|
||||
|
||||
@@ -308,7 +312,7 @@ def load_config() -> dict:
|
||||
config[key] = value
|
||||
return config
|
||||
except (json.JSONDecodeError, UnicodeError, Exception) as e:
|
||||
logger.error(f"Error reading config file: {e}")
|
||||
logger.exception(f"Error reading config file: {e}")
|
||||
# If config file is corrupted, create a new one with defaults
|
||||
save_config(default_config)
|
||||
|
||||
@@ -322,7 +326,7 @@ def save_config(config) -> bool:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving config: {e}")
|
||||
logger.exception(f"Error saving config: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -342,7 +346,7 @@ def check_ffmpeg() -> bool:
|
||||
os.environ["PATH"] = f"{ffmpeg_path}{os.pathsep}{os.environ.get('PATH', '')}"
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating PATH: {e}")
|
||||
logger.exception(f"Error updating PATH: {e}")
|
||||
return False
|
||||
|
||||
# For macOS, check common paths
|
||||
@@ -359,13 +363,13 @@ def check_ffmpeg() -> bool:
|
||||
os.environ["PATH"] = f"{ffmpeg_dir}{os.pathsep}{os.environ.get('PATH', '')}"
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating PATH: {e}")
|
||||
logger.exception(f"Error updating PATH: {e}")
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking FFmpeg: {e}")
|
||||
logger.exception(f"Error checking FFmpeg: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -381,7 +385,7 @@ def load_saved_path(main_window_instance) -> None:
|
||||
main_window_instance.last_path = saved_path
|
||||
return
|
||||
except (json.JSONDecodeError, UnicodeError) as e:
|
||||
logger.error(f"Error reading config file: {e}")
|
||||
logger.exception(f"Error reading config file: {e}")
|
||||
# If config file is corrupted, try to remove it
|
||||
try:
|
||||
APP_CONFIG_FILE.unlink(missing_ok=True)
|
||||
@@ -397,7 +401,7 @@ def load_saved_path(main_window_instance) -> None:
|
||||
main_window_instance.last_path = tempfile.gettempdir()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading saved settings: {e}")
|
||||
logger.exception(f"Error loading saved settings: {e}")
|
||||
main_window_instance.last_path = tempfile.gettempdir()
|
||||
|
||||
|
||||
@@ -409,7 +413,7 @@ def save_path(main_window_instance, path) -> bool:
|
||||
try:
|
||||
Path(path).mkdir(exist_ok=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating directory: {e}")
|
||||
logger.exception(f"Error creating directory: {e}")
|
||||
return False
|
||||
|
||||
if not os.access(path, os.W_OK):
|
||||
@@ -423,7 +427,7 @@ def save_path(main_window_instance, path) -> bool:
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving settings: {e}")
|
||||
logger.exception(f"Error saving settings: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -484,13 +488,13 @@ def update_yt_dlp() -> bool:
|
||||
logger.info("yt-dlp binary successfully updated")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error replacing yt-dlp binary: {e}")
|
||||
logger.exception(f"Error replacing yt-dlp binary: {e}")
|
||||
return False
|
||||
else:
|
||||
logger.info(f"Failed to download latest yt-dlp: HTTP {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading yt-dlp update: {e}")
|
||||
logger.exception(f"Error downloading yt-dlp update: {e}")
|
||||
return False
|
||||
else:
|
||||
# We're using a system-installed yt-dlp, use pip to update
|
||||
@@ -540,9 +544,9 @@ def update_yt_dlp() -> bool:
|
||||
else:
|
||||
logger.info(f"Failed to get latest version info: HTTP {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking for yt-dlp updates: {e}")
|
||||
logger.exception(f"Error checking for yt-dlp updates: {e}")
|
||||
except Exception as e:
|
||||
logger.info(f"Unexpected error during yt-dlp update: {e}")
|
||||
logger.exception(f"Unexpected error during yt-dlp update: {e}")
|
||||
|
||||
return False
|
||||
|
||||
@@ -573,7 +577,7 @@ def should_check_for_auto_update() -> bool:
|
||||
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking auto-update schedule: {e}")
|
||||
logger.exception(f"Error checking auto-update schedule: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -602,9 +606,7 @@ def check_and_update_ytdlp_auto() -> bool:
|
||||
logger.info(f"Latest yt-dlp version: {latest_version}")
|
||||
|
||||
# Compare versions
|
||||
from packaging import version as version_parser
|
||||
|
||||
if version_parser.parse(latest_version) > version_parser.parse(current_version):
|
||||
if version.parse(latest_version) > version.parse(current_version):
|
||||
logger.info(f"Auto-updating yt-dlp from {current_version} to {latest_version}...")
|
||||
|
||||
# Perform the update
|
||||
@@ -630,11 +632,11 @@ def check_and_update_ytdlp_auto() -> bool:
|
||||
logger.info(f"Network error during auto-update check: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error during auto-update check: {e}")
|
||||
logger.exception(f"Error during auto-update check: {e}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.info(f"Critical error in auto-update: {e}")
|
||||
logger.critical(f"Critical error in auto-update: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
|
||||
@@ -657,77 +659,103 @@ def update_auto_update_settings(enabled, frequency) -> bool:
|
||||
save_config(config)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating auto-update settings: {e}")
|
||||
logger.exception(f"Error updating auto-update settings: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def parse_yt_dlp_error(error_message: str) -> str:
|
||||
"""
|
||||
Parse yt-dlp error messages and return user-friendly error messages.
|
||||
|
||||
|
||||
Args:
|
||||
error_message: The raw error message from yt-dlp
|
||||
|
||||
|
||||
Returns:
|
||||
str: A user-friendly error message with actionable advice
|
||||
"""
|
||||
error_str = str(error_message).lower()
|
||||
|
||||
error_str = error_message.lower()
|
||||
|
||||
# Private video errors
|
||||
if any(keyword in error_str for keyword in ['private video', 'login_required', 'sign in if you']):
|
||||
return ("This is a private video. You can download it by logging into your account using cookies.\n"
|
||||
"Go to 'Custom Options' → 'Login with Cookies' → 'Extract cookies from browser' to authenticate.")
|
||||
|
||||
if any(keyword in error_str for keyword in ["private video", "login_required", "sign in if you"]):
|
||||
return (
|
||||
"This is a private video. You can download it by logging into your account using cookies.\n"
|
||||
"Go to 'Custom Options' → 'Login with Cookies' → 'Extract cookies from browser' to authenticate."
|
||||
)
|
||||
|
||||
# Age-restricted content
|
||||
if any(keyword in error_str for keyword in ['age restricted', 'age-restricted', 'confirm your age']):
|
||||
return ("This video is age-restricted. You need to be logged in to access it.\n"
|
||||
"Use 'Custom Options' → 'Login with Cookies' to authenticate with your account.")
|
||||
|
||||
if any(keyword in error_str for keyword in ["age restricted", "age-restricted", "confirm your age"]):
|
||||
return (
|
||||
"This video is age-restricted. You need to be logged in to access it.\n"
|
||||
"Use 'Custom Options' → 'Login with Cookies' to authenticate with your account."
|
||||
)
|
||||
|
||||
# Geo-blocked content
|
||||
if any(keyword in error_str for keyword in ['not available in your country', 'geo-blocked', 'video is not available', 'not made this video available in your country']):
|
||||
return ("This video is not available in your region (geo-blocked).\n"
|
||||
"You may need to use a VPN or the video might be restricted in your country.")
|
||||
|
||||
if any(
|
||||
keyword in error_str
|
||||
for keyword in [
|
||||
"not available in your country",
|
||||
"geo-blocked",
|
||||
"video is not available",
|
||||
"not made this video available in your country",
|
||||
]
|
||||
):
|
||||
return (
|
||||
"This video is not available in your region (geo-blocked).\n"
|
||||
"You may need to use a VPN or the video might be restricted in your country."
|
||||
)
|
||||
|
||||
# Removed/deleted videos
|
||||
if any(keyword in error_str for keyword in ['video unavailable', 'this video has been removed', 'video does not exist']):
|
||||
return ("This video has been removed or is no longer available.\n"
|
||||
"The video may have been deleted by the uploader or removed due to policy violations.")
|
||||
|
||||
if any(keyword in error_str for keyword in ["video unavailable", "this video has been removed", "video does not exist"]):
|
||||
return (
|
||||
"This video has been removed or is no longer available.\n"
|
||||
"The video may have been deleted by the uploader or removed due to policy violations."
|
||||
)
|
||||
|
||||
# Live stream errors
|
||||
if any(keyword in error_str for keyword in ['live stream', 'livestream', 'is live']):
|
||||
return ("This is a live stream that cannot be downloaded while active.\n"
|
||||
"Wait for the stream to end, then try downloading the archived version.")
|
||||
|
||||
if any(keyword in error_str for keyword in ["live stream", "livestream", "is live"]):
|
||||
return (
|
||||
"This is a live stream that cannot be downloaded while active.\n"
|
||||
"Wait for the stream to end, then try downloading the archived version."
|
||||
)
|
||||
|
||||
# Playlist errors
|
||||
if any(keyword in error_str for keyword in ['playlist', 'no entries']):
|
||||
return ("Unable to access this playlist. It may be private, deleted, or empty.\n"
|
||||
"Check if the playlist exists and is publicly accessible.")
|
||||
|
||||
if any(keyword in error_str for keyword in ["playlist", "no entries"]):
|
||||
return (
|
||||
"Unable to access this playlist. It may be private, deleted, or empty.\n"
|
||||
"Check if the playlist exists and is publicly accessible."
|
||||
)
|
||||
|
||||
# Network/connection errors
|
||||
if any(keyword in error_str for keyword in ['network error', 'connection', 'timeout', 'unable to download']):
|
||||
return ("Network connection error. Please check your internet connection and try again.\n"
|
||||
"If the problem persists, the video server might be temporarily unavailable.")
|
||||
|
||||
if any(keyword in error_str for keyword in ["network error", "connection", "timeout", "unable to download"]):
|
||||
return (
|
||||
"Network connection error. Please check your internet connection and try again.\n"
|
||||
"If the problem persists, the video server might be temporarily unavailable."
|
||||
)
|
||||
|
||||
# Invalid URL
|
||||
if any(keyword in error_str for keyword in ['invalid url', 'unsupported url', 'no video found']):
|
||||
return ("Invalid or unsupported URL. Please check the link and try again.\n"
|
||||
"Make sure you're using a valid YouTube, Vimeo, or other supported platform URL.")
|
||||
|
||||
if any(keyword in error_str for keyword in ["invalid url", "unsupported url", "no video found"]):
|
||||
return (
|
||||
"Invalid or unsupported URL. Please check the link and try again.\n"
|
||||
"Make sure you're using a valid YouTube, Vimeo, or other supported platform URL."
|
||||
)
|
||||
|
||||
# YouTube premium content
|
||||
if any(keyword in error_str for keyword in ['youtube premium', 'premium', 'members only']):
|
||||
return ("This content requires YouTube Premium or channel membership.\n"
|
||||
"You need to be logged in with an account that has access to this content.")
|
||||
|
||||
if any(keyword in error_str for keyword in ["youtube premium", "premium", "members only"]):
|
||||
return (
|
||||
"This content requires YouTube Premium or channel membership.\n"
|
||||
"You need to be logged in with an account that has access to this content."
|
||||
)
|
||||
|
||||
# Copyright/DMCA
|
||||
if any(keyword in error_str for keyword in ['copyright', 'dmca', 'blocked']):
|
||||
return ("This video is blocked due to copyright claims.\n"
|
||||
"The content owner has restricted access to this video.")
|
||||
|
||||
if any(keyword in error_str for keyword in ["copyright", "dmca", "blocked"]):
|
||||
return "This video is blocked due to copyright claims.\n" "The content owner has restricted access to this video."
|
||||
|
||||
# Extraction errors (could be temporary)
|
||||
if any(keyword in error_str for keyword in ['unable to extract', 'extraction failed']):
|
||||
return ("Failed to extract video information. This might be a temporary issue.\n"
|
||||
"Please try again in a few minutes, or check if the video link is correct.")
|
||||
|
||||
if any(keyword in error_str for keyword in ["unable to extract", "extraction failed"]):
|
||||
return (
|
||||
"Failed to extract video information. This might be a temporary issue.\n"
|
||||
"Please try again in a few minutes, or check if the video link is correct."
|
||||
)
|
||||
|
||||
# Generic fallback with the original error for debugging
|
||||
return (f"Could not extract video information. Please check your link.\n"
|
||||
f"Technical details: {error_message}")
|
||||
return f"Could not extract video information. Please check your link.\n" f"Technical details: {error_message}"
|
||||
|
||||
@@ -20,7 +20,7 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from src.core.ytsage_logging import logger
|
||||
from src.utils.ytsage_logger import logger
|
||||
from src.utils.ytsage_constants import (
|
||||
APP_BIN_DIR,
|
||||
ICON_PATH,
|
||||
@@ -94,7 +94,7 @@ class YtdlpSetupDialog(QDialog):
|
||||
# icon_path logic moved to src\utils\ytsage_constants.py
|
||||
icon_path = ICON_PATH
|
||||
if Path.exists(icon_path):
|
||||
self.setWindowIcon(QIcon(icon_path.as_posix()))
|
||||
self.setWindowIcon(QIcon(str(icon_path)))
|
||||
|
||||
self.init_ui()
|
||||
|
||||
@@ -390,11 +390,11 @@ class YtdlpSetupDialog(QDialog):
|
||||
self.setup_complete.emit(target_path)
|
||||
self.accept()
|
||||
except Exception as copy_error:
|
||||
logger.debug(f"Error copying file: {str(copy_error)}")
|
||||
logger.debug(f"Error copying file: {copy_error}", exc_info=True)
|
||||
error_dialog = QMessageBox(self)
|
||||
error_dialog.setIcon(QMessageBox.Icon.Critical)
|
||||
error_dialog.setWindowTitle("Setup Error")
|
||||
error_dialog.setText(f"Error copying yt-dlp to app directory: {str(copy_error)}")
|
||||
error_dialog.setText(f"Error copying yt-dlp to app directory: {copy_error}")
|
||||
error_dialog.setStyleSheet(
|
||||
"""
|
||||
QMessageBox {
|
||||
@@ -448,11 +448,11 @@ class YtdlpSetupDialog(QDialog):
|
||||
)
|
||||
error_dialog.exec()
|
||||
except Exception as e:
|
||||
logger.debug(f"Exception during verification: {str(e)}")
|
||||
logger.debug(f"Exception during verification: {e}", exc_info=True)
|
||||
error_dialog = QMessageBox(self)
|
||||
error_dialog.setIcon(QMessageBox.Icon.Critical)
|
||||
error_dialog.setWindowTitle("Error")
|
||||
error_dialog.setText(f"Error verifying yt-dlp executable: {str(e)}")
|
||||
error_dialog.setText(f"Error verifying yt-dlp executable: {e}")
|
||||
error_dialog.setStyleSheet(
|
||||
"""
|
||||
QMessageBox {
|
||||
@@ -492,7 +492,7 @@ def check_ytdlp_binary() -> Optional[Path]:
|
||||
os.chmod(exe_path, 0o755)
|
||||
logger.info(f"Fixed permissions on yt-dlp at {exe_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not set executable permissions on {exe_path}: {e}")
|
||||
logger.exception(f"Could not set executable permissions on {exe_path}: {e}")
|
||||
return exe_path
|
||||
|
||||
# If not found in app directory, check if yt-dlp is available in PATH
|
||||
@@ -517,7 +517,7 @@ def check_ytdlp_binary() -> Optional[Path]:
|
||||
logger.info(f"Found yt-dlp in PATH: {yt_dlp_path}")
|
||||
return Path(yt_dlp_path)
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking for yt-dlp in PATH: {e}")
|
||||
logger.exception(f"Error checking for yt-dlp in PATH: {e}")
|
||||
# We're only interested in our app-specific installation or system PATH
|
||||
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user