Refactor/code cleanup (#37)

* 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
This commit is contained in:
Viren Hirpara
2025-08-26 18:57:12 +05:30
committed by GitHub
parent 6652d7f00f
commit 9c13b4b61c
21 changed files with 2805 additions and 2628 deletions
+114 -112
View File
@@ -1,32 +1,24 @@
import sys
import os
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)
from PySide6.QtCore import Qt, Signal, QObject, QThread
from PySide6.QtGui import QIcon, QPalette, QColor, QPixmap
import requests
from io import BytesIO
from PIL import Image
from datetime import datetime
import json
from pathlib import Path
from packaging import version
import subprocess
import re
from ..core.ytsage_logging import logger
try:
import yt_dlp
YT_DLP_AVAILABLE = True
except ImportError:
YT_DLP_AVAILABLE = False
logger.warning("yt-dlp not available at startup, will be downloaded at runtime")
from .ytsage_gui_dialogs import SubtitleSelectionDialog, SponsorBlockCategoryDialog
from datetime import datetime
from io import BytesIO
from pathlib import Path
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 yt_dlp import YoutubeDL
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,
)
class VideoInfoMixin:
def setup_video_info_section(self):
def setup_video_info_section(self) -> QHBoxLayout:
# Create a horizontal layout for thumbnail and video info
media_info_layout = QHBoxLayout()
media_info_layout.setSpacing(15)
@@ -36,7 +28,7 @@ class VideoInfoMixin:
thumbnail_container.setFixedWidth(320)
thumbnail_layout = QVBoxLayout(thumbnail_container)
thumbnail_layout.setContentsMargins(0, 0, 0, 0)
# Thumbnail on the left
self.thumbnail_label = QLabel()
self.thumbnail_label.setFixedSize(320, 180)
@@ -44,14 +36,14 @@ class VideoInfoMixin:
self.thumbnail_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
thumbnail_layout.addWidget(self.thumbnail_label)
thumbnail_layout.addStretch()
media_info_layout.addWidget(thumbnail_container)
# Video information on the right
video_info_layout = QVBoxLayout()
video_info_layout.setSpacing(2) # Reduce spacing between elements
video_info_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# Title and info labels
self.title_label = QLabel()
self.title_label.setWordWrap(True)
@@ -65,14 +57,22 @@ class VideoInfoMixin:
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]:
label.setStyleSheet("""
for label in [
self.channel_label,
self.views_label,
self.date_label,
self.duration_label,
self.like_count_label,
]:
label.setStyleSheet(
"""
QLabel {
color: #cccccc;
font-size: 12px;
padding: 1px 0;
}
""")
"""
)
# Add labels to video info layout
video_info_layout.addWidget(self.title_label)
@@ -90,11 +90,12 @@ class VideoInfoMixin:
subtitle_layout.setSpacing(10)
# Subtitle selection button
self.subtitle_select_btn = QPushButton("Select Subtitles...") # Renamed & changed text
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("""
self.subtitle_select_btn.setStyleSheet(
"""
QPushButton {
background-color: #1d1e22;
border: 2px solid #1d1e22;
@@ -112,8 +113,9 @@ class VideoInfoMixin:
color: #888888;
border-color: #3d3d3d;
}
""")
self.subtitle_select_btn.setProperty("subtitlesSelected", False) # Custom property for styling
"""
)
self.subtitle_select_btn.setProperty("subtitlesSelected", False) # Custom property for styling
subtitle_layout.addWidget(self.subtitle_select_btn)
# Label to show number of selected subtitles
@@ -130,11 +132,12 @@ class VideoInfoMixin:
# --- SponsorBlock Section ---
sponsorblock_layout = QHBoxLayout()
self.sponsorblock_select_btn = QPushButton("SponsorBlock Categories...")
self.sponsorblock_select_btn.setFixedHeight(30)
self.sponsorblock_select_btn.clicked.connect(self.open_sponsorblock_dialog)
self.sponsorblock_select_btn.setStyleSheet("""
self.sponsorblock_select_btn.setStyleSheet(
"""
QPushButton {
background-color: #1d1e22;
border: 2px solid #1d1e22;
@@ -152,18 +155,19 @@ class VideoInfoMixin:
color: #888888;
border-color: #3d3d3d;
}
""")
"""
)
self.sponsorblock_select_btn.setProperty("sponsorBlockSelected", False)
self.sponsorblock_select_btn.setProperty("sponsorBlockSelected", False)
sponsorblock_layout.addWidget(self.sponsorblock_select_btn)
# Label to show selection count
self.selected_sponsorblock_label = QLabel("0 selected")
self.selected_sponsorblock_label.setStyleSheet("color: #cccccc; padding-left: 5px;")
sponsorblock_layout.addWidget(self.selected_sponsorblock_label)
sponsorblock_layout.addStretch()
# Add the sponsorblock layout to the main video info layout
video_info_layout.addLayout(sponsorblock_layout)
# --- End SponsorBlock Section ---
@@ -180,10 +184,11 @@ class VideoInfoMixin:
return media_info_layout
def setup_playlist_info_section(self):
def setup_playlist_info_section(self) -> QLabel:
self.playlist_info_label = QLabel()
self.playlist_info_label.setVisible(False)
self.playlist_info_label.setStyleSheet("""
self.playlist_info_label.setStyleSheet(
"""
QLabel {
font-size: 12px;
color: #ffffff;
@@ -195,18 +200,19 @@ class VideoInfoMixin:
min-height: 30px;
max-height: 30px;
}
""")
"""
)
self.playlist_info_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
return self.playlist_info_label
def update_video_info(self, info):
if hasattr(self, 'is_playlist') and self.is_playlist:
def update_video_info(self, info) -> None:
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.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("")
@@ -225,63 +231,62 @@ class VideoInfoMixin:
self.like_count_label.setVisible(True)
# Format view count with commas
views = info.get('view_count')
formatted_views = f"{views:,}" if views is not None else 'N/A'
views = info.get("view_count")
formatted_views = f"{views:,}" if views is not None else "N/A"
# Format like count with commas
likes = info.get('like_count')
formatted_likes = f"{likes:,}" if likes is not None else 'N/A'
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', '')
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')
date_obj = datetime.strptime(upload_date, "%Y%m%d")
formatted_date = date_obj.strftime("%B %d, %Y")
else:
formatted_date = 'Unknown date'
formatted_date = "Unknown date"
# Format duration
duration = info.get('duration', 0)
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.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}")
def open_subtitle_dialog(self):
if not hasattr(self, 'available_subtitles') or not hasattr(self, 'available_automatic_subtitles'):
logger.warning("Subtitle info not loaded yet.")
return
def open_subtitle_dialog(self) -> None:
if not hasattr(self, "available_subtitles") or not hasattr(self, "available_automatic_subtitles"):
logger.warning("Subtitle info not loaded yet.")
return
if not hasattr(self, 'selected_subtitles'):
if not hasattr(self, "selected_subtitles"):
self.selected_subtitles = []
dialog = SubtitleSelectionDialog(
self.available_subtitles,
self.available_automatic_subtitles,
self.available_subtitles, # type: ignore[reportAttributeAccessIssue]
self.available_automatic_subtitles, # type: ignore[reportAttributeAccessIssue]
self.selected_subtitles,
self # Parent for the dialog
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
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
# 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)
merge_checkbox = getattr(main_window, "merge_subs_checkbox", None)
if dialog.exec(): # If user clicks OK
if dialog.exec(): # If user clicks OK
self.selected_subtitles = dialog.get_selected_subtitles()
logger.info(f"Selected subtitles: {self.selected_subtitles}")
# Update UI to reflect selection
@@ -292,7 +297,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(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)
@@ -304,29 +309,29 @@ class VideoInfoMixin:
self.subtitle_select_btn.style().polish(self.subtitle_select_btn)
# No else needed for cancel, state remains unchanged
def open_sponsorblock_dialog(self):
def open_sponsorblock_dialog(self) -> None:
"""Open the SponsorBlock category selection dialog."""
# Initialize selected categories if not exists or empty (first time opening)
if not hasattr(self, 'selected_sponsorblock_categories') or not self.selected_sponsorblock_categories:
if not hasattr(self, "selected_sponsorblock_categories") or not self.selected_sponsorblock_categories:
# Use None to let the dialog set its own defaults
dialog_categories = None
else:
dialog_categories = self.selected_sponsorblock_categories
dialog = SponsorBlockCategoryDialog(dialog_categories, self)
if dialog.exec():
self.selected_sponsorblock_categories = dialog.get_selected_categories()
logger.info(f"SponsorBlock categories selected: {self.selected_sponsorblock_categories}")
self._update_sponsorblock_display()
def _update_sponsorblock_display(self):
def _update_sponsorblock_display(self) -> None:
"""Update the SponsorBlock button and label to reflect current selection."""
if not hasattr(self, 'selected_sponsorblock_categories'):
if not hasattr(self, "selected_sponsorblock_categories"):
self.selected_sponsorblock_categories = []
count = len(self.selected_sponsorblock_categories)
# Update label text
if count == 0:
self.selected_sponsorblock_label.setText("0 selected")
@@ -334,15 +339,15 @@ class VideoInfoMixin:
self.selected_sponsorblock_label.setText("1 category selected")
else:
self.selected_sponsorblock_label.setText(f"{count} categories selected")
# Update button property for styling
self.sponsorblock_select_btn.setProperty("sponsorBlockSelected", count > 0)
# Force style refresh
self.sponsorblock_select_btn.style().unpolish(self.sponsorblock_select_btn)
self.sponsorblock_select_btn.style().polish(self.sponsorblock_select_btn)
def download_thumbnail(self, url):
def download_thumbnail(self, url) -> None:
try:
# Store both thumbnail URL and video URL
self.thumbnail_url = url
@@ -355,42 +360,39 @@ class VideoInfoMixin:
# Display thumbnail
image = self.thumbnail_image.resize((320, 180), Image.Resampling.LANCZOS)
img_byte_arr = BytesIO()
image.save(img_byte_arr, format='PNG')
image.save(img_byte_arr, format="PNG")
pixmap = QPixmap()
pixmap.loadFromData(img_byte_arr.getvalue())
self.thumbnail_label.setPixmap(pixmap)
except Exception as e:
logger.error(f"Error loading thumbnail: {str(e)}")
def download_thumbnail_file(self, video_url, path):
def download_thumbnail_file(self, video_url, path) -> bool:
if not self.save_thumbnail:
return False
try:
from yt_dlp import YoutubeDL
import requests # Use requests instead of urlopen
logger.debug(f"Attempting to save thumbnail for URL: {video_url}")
ydl_opts = {
'quiet': True,
'skip_download': True,
'force_generic_extractor': False,
'no_warnings': True,
'extract_flat': False
"quiet": True,
"skip_download": True,
"force_generic_extractor": False,
"no_warnings": True,
"extract_flat": False,
}
with YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(video_url, download=False)
thumbnails = info.get('thumbnails', [])
thumbnails = info.get("thumbnails", [])
if not thumbnails:
raise ValueError("No thumbnails available")
thumbnail_url = max(
thumbnails,
key=lambda t: (t.get('height', 0) or 0) * (t.get('width', 0) or 0)
).get('url')
key=lambda t: (t.get("height", 0) or 0) * (t.get("width", 0) or 0),
).get("url")
if not thumbnail_url:
raise ValueError("Failed to extract thumbnail URL")
@@ -400,13 +402,13 @@ class VideoInfoMixin:
response.raise_for_status()
# Save the thumbnail
thumb_dir = os.path.join(path, 'Thumbnails')
os.makedirs(thumb_dir, exist_ok=True)
thumb_dir = Path(path).joinpath("Thumbnails")
thumb_dir.mkdir(exist_ok=True)
filename = f"{self.sanitize_filename(info['title'])}.jpg"
thumbnail_path = os.path.join(thumb_dir, filename)
thumbnail_path = thumb_dir.joinpath(filename)
with open(thumbnail_path, 'wb') as f:
with open(thumbnail_path, "wb") as f:
f.write(response.content)
logger.info(f"Thumbnail saved to: {thumbnail_path}")
@@ -419,6 +421,6 @@ class VideoInfoMixin:
self.signals.update_status.emit(error_msg)
return False
def sanitize_filename(self, name):
def sanitize_filename(self, name) -> str:
"""Clean filename for filesystem safety"""
return re.sub(r'[\\/*?:"<>|]', "", name).strip()[:75]
return re.sub(r'[\\/*?:"<>|]', "", name).strip()[:75]