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:
Viren Hirpara
2025-09-10 01:31:19 +05:30
committed by GitHub
parent c140acef5a
commit e9de913b47
23 changed files with 825 additions and 714 deletions
+33 -19
View File
@@ -2,22 +2,28 @@ import re
from datetime import datetime
from io import BytesIO
from pathlib import Path
from typing import TYPE_CHECKING, cast
import requests
from PIL import Image
from PySide6.QtCore import Qt
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QHBoxLayout, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
from src.core.ytsage_logging import logger
from src.gui.ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__init__.py
SponsorBlockCategoryDialog,
SubtitleSelectionDialog,
)
from src.utils.ytsage_logger import logger
if TYPE_CHECKING:
from src.gui.ytsage_gui_main import YTSageApp
class VideoInfoMixin:
def setup_video_info_section(self) -> QHBoxLayout:
self = cast("YTSageApp", self) # for autocompletion and type inference.
# Create a horizontal layout for thumbnail and video info
media_info_layout = QHBoxLayout()
media_info_layout.setSpacing(15)
@@ -184,6 +190,8 @@ class VideoInfoMixin:
return media_info_layout
def setup_playlist_info_section(self) -> QLabel:
self = cast("YTSageApp", self) # for autocompletion and type inference.
self.playlist_info_label = QLabel()
self.playlist_info_label.setVisible(False)
self.playlist_info_label.setStyleSheet(
@@ -205,6 +213,8 @@ class VideoInfoMixin:
return self.playlist_info_label
def update_video_info(self, info) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference.
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"))
@@ -260,6 +270,8 @@ class VideoInfoMixin:
self.duration_label.setText(f"Duration: {duration_str}")
def open_subtitle_dialog(self) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference.
if not hasattr(self, "available_subtitles") or not hasattr(self, "available_automatic_subtitles"):
logger.warning("Subtitle info not loaded yet.")
return
@@ -274,16 +286,8 @@ class VideoInfoMixin:
self, # Parent for the dialog
)
# 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
logger.warning("Cannot find main window to access merge checkbox.")
merge_checkbox = None
else:
merge_checkbox = getattr(main_window, "merge_subs_checkbox", None)
# removed extra logic for mapping to main_windows
merge_checkbox = getattr(self, "merge_subs_checkbox", None)
if dialog.exec(): # If user clicks OK
self.selected_subtitles = dialog.get_selected_subtitles()
@@ -296,7 +300,7 @@ class VideoInfoMixin:
# 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()
is_audio_only = hasattr(self, "audio_button") and self.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)
@@ -310,6 +314,8 @@ class VideoInfoMixin:
def open_sponsorblock_dialog(self) -> None:
"""Open the SponsorBlock category selection dialog."""
self = cast("YTSageApp", self) # for autocompletion and type inference.
# Initialize selected categories if not exists or empty (first time opening)
if not hasattr(self, "selected_sponsorblock_categories") or not self.selected_sponsorblock_categories:
# Use None to let the dialog set its own defaults
@@ -326,6 +332,8 @@ class VideoInfoMixin:
def _update_sponsorblock_display(self) -> None:
"""Update the SponsorBlock button and label to reflect current selection."""
self = cast("YTSageApp", self) # for autocompletion and type inference.
if not hasattr(self, "selected_sponsorblock_categories"):
self.selected_sponsorblock_categories = []
@@ -347,6 +355,8 @@ class VideoInfoMixin:
self.sponsorblock_select_btn.style().polish(self.sponsorblock_select_btn)
def download_thumbnail(self, url) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference.
try:
# Store both thumbnail URL and video URL
self.thumbnail_url = url
@@ -364,7 +374,10 @@ class VideoInfoMixin:
pixmap.loadFromData(img_byte_arr.getvalue())
self.thumbnail_label.setPixmap(pixmap)
except Exception as e:
logger.error(f"Error loading thumbnail: {str(e)}")
logger.exception(f"Error loading thumbnail: {e}")
def download_thumbnail_file(self, video_url, path) -> bool:
self = cast("YTSageApp", self) # for autocompletion and type inference.
def download_thumbnail_file(self, video_url, path) -> bool:
if not self.save_thumbnail:
@@ -373,10 +386,11 @@ class VideoInfoMixin:
try:
# Import yt_dlp locally to avoid import errors when yt-dlp is not installed
from yt_dlp import YoutubeDL
logger.debug(f"Attempting to save thumbnail for URL: {video_url}")
ydl_opts = {
"logger": logger,
"quiet": True,
"skip_download": True,
"force_generic_extractor": False,
@@ -389,7 +403,7 @@ class VideoInfoMixin:
thumbnails = info.get("thumbnails", [])
if not thumbnails:
raise ValueError("No thumbnails available")
logger.info("No thumbnails available")
thumbnail_url = max(
thumbnails,
@@ -397,7 +411,7 @@ class VideoInfoMixin:
).get("url")
if not thumbnail_url:
raise ValueError("Failed to extract thumbnail URL")
logger.info("Failed to extract thumbnail URL")
# Download using requests
response = requests.get(thumbnail_url)
@@ -418,8 +432,8 @@ class VideoInfoMixin:
return True
except Exception as e:
error_msg = f"❌ Thumbnail error: {str(e)}"
logger.error(f"Thumbnail Save Error: {str(e)}")
error_msg = f"❌ Thumbnail error: {e}"
logger.exception(f"Thumbnail Save Error: {e}")
self.signals.update_status.emit(error_msg)
return False