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 import yt_dlp from ytsage_gui_dialogs import SubtitleSelectionDialog class VideoInfoMixin: def setup_video_info_section(self): # Create a horizontal layout for thumbnail and video info media_info_layout = QHBoxLayout() media_info_layout.setSpacing(15) # Left side container for thumbnail thumbnail_container = QWidget() 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) self.thumbnail_label.setStyleSheet("border: 2px solid #3d3d3d; border-radius: 4px;") 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) self.title_label.setStyleSheet("font-size: 14px; font-weight: bold;") # Add basic info labels self.channel_label = QLabel() 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]: label.setStyleSheet(""" QLabel { color: #cccccc; font-size: 12px; padding: 1px 0; } """) # Add labels to video info layout 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 --- subtitle_layout = QHBoxLayout() subtitle_layout.setSpacing(10) # 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(""" QPushButton { background-color: #1d1e22; border: 2px solid #1d1e22; border-radius: 4px; padding: 5px 10px; /* Adjusted padding */ } 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; } """) 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 self.selected_subs_label = QLabel("0 selected") self.selected_subs_label.setStyleSheet("color: #cccccc; padding-left: 5px;") subtitle_layout.addWidget(self.selected_subs_label) # Add the subtitle layout to the main video info layout video_info_layout.addLayout(subtitle_layout) # --- End Subtitle Section --- # Add stretch at the bottom video_info_layout.addStretch() # Add video info layout to main layout media_info_layout.addLayout(video_info_layout, stretch=1) return media_info_layout def setup_playlist_info_section(self): self.playlist_info_label = QLabel() self.playlist_info_label.setVisible(False) self.playlist_info_label.setStyleSheet(""" QLabel { font-size: 12px; color: #ffffff; padding: 5px 8px; margin: 0; background-color: #1d1e22; border: 1px solid #c90000; border-radius: 4px; 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: # 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) 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) # Format view count with commas 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' # 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}") 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 if not hasattr(self, 'selected_subtitles'): self.selected_subtitles = [] dialog = SubtitleSelectionDialog( self.available_subtitles, self.available_automatic_subtitles, self.selected_subtitles, 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 print("Warning: Cannot find main window to access merge checkbox.") merge_checkbox = None else: merge_checkbox = getattr(main_window, 'merge_subs_checkbox', None) 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) # 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.") # 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 download_thumbnail(self, url): try: # Store both thumbnail URL and video URL self.thumbnail_url = url self.video_url = self.url_input.text() # Get actual video URL # Download thumbnail but don't save yet response = requests.get(url) self.thumbnail_image = Image.open(BytesIO(response.content)) # Display thumbnail image = self.thumbnail_image.resize((320, 180), Image.Resampling.LANCZOS) img_byte_arr = BytesIO() image.save(img_byte_arr, format='PNG') pixmap = QPixmap() pixmap.loadFromData(img_byte_arr.getvalue()) self.thumbnail_label.setPixmap(pixmap) except Exception as e: print(f"Error loading thumbnail: {str(e)}") def download_thumbnail_file(self, video_url, path): if not self.save_thumbnail: return False try: from yt_dlp import YoutubeDL import requests # Use requests instead of urlopen print(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 } with YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(video_url, download=False) 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') if not thumbnail_url: raise ValueError("Failed to extract thumbnail URL") # Download using requests response = requests.get(thumbnail_url) response.raise_for_status() # Save the thumbnail thumb_dir = os.path.join(path, 'Thumbnails') os.makedirs(thumb_dir, exist_ok=True) filename = f"{self.sanitize_filename(info['title'])}.jpg" thumbnail_path = os.path.join(thumb_dir, filename) with open(thumbnail_path, 'wb') as f: f.write(response.content) print(f"Thumbnail saved to: {thumbnail_path}") self.signals.update_status.emit(f"✅ Thumbnail saved: {filename}") return True except Exception as e: error_msg = f"❌ Thumbnail error: {str(e)}" print(f"Thumbnail Save Error: {str(e)}") self.signals.update_status.emit(error_msg) return False def sanitize_filename(self, name): """Clean filename for filesystem safety""" return re.sub(r'[\\/*?:"<>|]', "", name).strip()[:75]