import logging import subprocess from pathlib import Path from PySide6.QtWidgets import ( QMainWindow, QWidget, QLabel, QPushButton, QFrame, QScrollArea, QVBoxLayout, QHBoxLayout, QMessageBox, QLineEdit, QDialog, QApplication, ) from PySide6.QtCore import Qt, Signal, QObject from constant import VERSION from dialog import DownloadProgressDialog, PlayerDialog, SwitchProfileDialog, PlaylistWindow from utils import download_file from widgets import PlaylistItem from yt_lib import PlaylistFetchException, resolve_video_page_url logger = logging.getLogger("MainWindow") class PlaylistMainWindow(QMainWindow): def __init__(self, app): super(PlaylistMainWindow, self).__init__() self.app = app self.child_windows = [] self.setWindowTitle("PlaylistSaver") self.resize(700, 800) # Central widget self.central_widget = QWidget(self) self.setCentralWidget(self.central_widget) # Layouts self.main_layout = QHBoxLayout(self.central_widget) self.main_layout.setContentsMargins(12, 12, 12, 12) self.main_layout.setSpacing(12) self.right_layout = QVBoxLayout() self.right_layout.setContentsMargins(12, 12, 12, 12) self.right_layout.setSpacing(10) self.playlists_layout = QVBoxLayout() self.playlists_layout.setContentsMargins(10, 10, 10, 10) self.playlists_layout.setSpacing(8) # Frame self.playlists_scroll = QScrollArea(self) self.playlists_scroll.setWidgetResizable(True) self.playlists_scroll.setFrameShape(QFrame.Shape.NoFrame) self.playlists_frame = QFrame(self.playlists_scroll) self.playlists_frame.setLayout(self.playlists_layout) self.playlists_scroll.setWidget(self.playlists_frame) self.playlists_scroll.setMinimumWidth(400) self.right_panel = QFrame(self.central_widget) self.right_panel.setObjectName("rightPanel") self.right_panel.setFixedWidth(190) self.right_panel.setLayout(self.right_layout) # Items self.profile_label = QLabel(f"Current Profile: {self.app.get_active_profile_name()}" ,) self.profile_label.setObjectName("profileLabel") self.profile_label.setWordWrap(True) self.loading_label = QLabel("Loading...") self.loading_label.setObjectName("loadingLabel") self.loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignBottom) self.switch_profile_btn = QPushButton("Switch Profile") self.helper_key_btn = QPushButton("Helper Secret Key") self.refresh_playlists_btn = QPushButton("Refresh playlists") self.save_all_playlists_btn = QPushButton("Save All Playlists") self.open_download_folder_btn = QPushButton("Open Download Folder") self.switch_profile_btn.setMinimumHeight(34) self.helper_key_btn.setMinimumHeight(34) self.refresh_playlists_btn.setMinimumHeight(34) self.save_all_playlists_btn.setMinimumHeight(34) self.version_label = QLabel(f"v{VERSION}") self.version_label.setObjectName("versionLabel") self.playlist_items_layout = QVBoxLayout() self.playlist_items_layout.setContentsMargins(0, 0, 0, 0) self.playlist_items_layout.setSpacing(8) self.playlists_layout.addLayout(self.playlist_items_layout) self.playlists_layout.addWidget(self.loading_label, 0, Qt.AlignmentFlag.AlignHCenter) # Bind item self.left_layout = QVBoxLayout() self.left_layout.setContentsMargins(0, 0, 0, 0) self.left_layout.setSpacing(10) self.left_layout.addWidget(self.playlists_scroll, 1) self.right_layout.addWidget(self.profile_label) self.right_layout.addWidget(self.switch_profile_btn) self.right_layout.addWidget(self.helper_key_btn) self.right_layout.addWidget(self.refresh_playlists_btn) self.right_layout.addWidget(self.save_all_playlists_btn) self.right_layout.addWidget(self.open_download_folder_btn) self.right_layout.addStretch(1) self.right_layout.addWidget(self.version_label, 0, Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter) self.main_layout.addLayout(self.left_layout, 1) self.main_layout.addWidget(self.right_panel) self.setStyleSheet(""" QMainWindow { background-color: #202020; } QScrollArea, QFrame { background-color: #242424; } QLabel { background-color: transparent; } #rightPanel { background-color: #2b2b2b; border: 1px solid #3c3c3c; border-radius: 8px; } #profileLabel { color: #f0f0f0; font-weight: 600; padding-bottom: 6px; } #loadingLabel { color: #a8a8a8; padding: 4px; } QPushButton { background-color: #3a3a3a; color: #f0f0f0; border: 1px solid #505050; border-radius: 6px; padding: 7px 10px; } QPushButton:hover { background-color: #464646; } QPushButton:pressed { background-color: #303030; } """) # Bind signals function self.signals = self.PlaylistWorkerSignals() self.signals.playlists_ready.connect(self.build_ui) self.signals.error.connect(self.show_error) self.signals.playlist_fetch_error.connect(self.show_playlist_fetch_error) self.signals.download_status.connect(self.loading_label.setText) self.switch_profile_btn.clicked.connect(self.open_switch_profile_dialog) self.helper_key_btn.clicked.connect(self.show_helper_secret_key) self.refresh_playlists_btn.clicked.connect(self.app.reload_playlists) self.save_all_playlists_btn.clicked.connect(self.save_all_playlists) self.open_download_folder_btn.clicked.connect(self.app.open_download_folder) class PlaylistWorkerSignals(QObject): # Signals playlists_ready = Signal(dict) profile_delete = Signal(str) # playlist playlist_clicked = Signal(dict) video_clicked = Signal(str) # error error = Signal(str) playlist_fetch_error = Signal(object) download_status = Signal(str) def build_ui(self, playlists: dict[str, list]): self.clear_layout(self.playlist_items_layout) playlists = playlists.get("entries", []) playlists = [playlist for playlist in playlists if playlist is not None] # ignore None item for playlist in playlists: plist_id = playlist.get("id", None) title = playlist.get("title", "Title not found") item = PlaylistItem( self.playlists_frame, playlist, title=title, thumbnail_size=self.app.THUMBNAIL_SIZE ) if len(playlist.get("thumbnails", [])) > 0: thumbnail_url = playlist.get("thumbnails", [])[0].get('url', None) thumbnail_resolution = playlist.get("thumbnails", [])[0].get('resolution', None) thumbnail_path = Path(self.app.temp_path, "thumbnails", f"thumbnail_{plist_id}_{thumbnail_resolution}.jpg") result = download_file(thumbnail_url, thumbnail_path) if result or (thumbnail_path.exists() and thumbnail_path.is_file()): item.set_thumbnail(thumbnail_path) item.clicked.connect(self.open_playlist_window) self.playlist_items_layout.addWidget(item) self.loading_label.setText("Done") def save_all_playlists(self): playlists = [item for item in self.app.playlists.get("entries", []) if item] if not playlists: QMessageBox.information(self, "Download", "No playlists loaded.") return self.progress_dialog = DownloadProgressDialog( "Save All Playlists", 0, self, producers=len(playlists) ) self.app.download_all_playlists( self.progress_dialog.finish_future, self.progress_dialog.tasks_added.emit, self.progress_dialog.producer_finished.emit, ) def on_download_done(self, result, video=None, playlist_name=None): if isinstance(result, Exception): self.signals.error.emit(f"Download failed: {result}") return try: path = result.result() except Exception as error: self.signals.error.emit(f"Download failed: {error}") return self.signals.download_status.emit(f"Downloaded: {path.name}") def clear_layout(self, layout): while layout.count(): item = layout.takeAt(0) child_layout = item.layout() widget = item.widget() if child_layout: self.clear_layout(child_layout) if widget: widget.deleteLater() def open_playlist_window(self, playlist: dict): window = PlaylistWindow(self.app, playlist, parent=self, open_player_callback=self.open_video_player) self.child_windows.append(window) window.destroyed.connect( lambda _obj=None, w=window: self.child_windows.remove(w) if w in self.child_windows else None ) window.show() def show_error(self, message: str): QMessageBox.critical(self, "Error", message) def show_playlist_fetch_error(self, error: PlaylistFetchException): error.create_messagebox(self) def open_video_player(self, video: dict): video_url = resolve_video_page_url(video) if not video_url: QMessageBox.critical(self, "Error", "Unable to resolve video URL.") return dialog = PlayerDialog(self.app, video, parent=self) self.child_windows.append(dialog) dialog.destroyed.connect( lambda _obj=None, w=dialog: self.child_windows.remove(w) if w in self.child_windows else None) dialog.show() def open_switch_profile_dialog(self): dialog = SwitchProfileDialog(self.app, parent=self) dialog.profile_changed.connect(self.apply_profile_switch) dialog.exec() def show_helper_secret_key(self): dialog = QDialog(self) dialog.setWindowTitle("Helper Secret Key") layout = QVBoxLayout(dialog) layout.addWidget(QLabel("Copy this key into the PlaylistSaver Helper settings page:")) key_field = QLineEdit(self.app.secret_key or "") key_field.setReadOnly(True) key_field.setEchoMode(QLineEdit.EchoMode.Password) layout.addWidget(key_field) buttons = QHBoxLayout() reveal_btn = QPushButton("Show") copy_btn = QPushButton("Copy") close_btn = QPushButton("Close") reveal_btn.clicked.connect(lambda: key_field.setEchoMode(QLineEdit.EchoMode.Normal)) copy_btn.clicked.connect(lambda: QApplication.clipboard().setText(key_field.text())) close_btn.clicked.connect(dialog.accept) buttons.addWidget(reveal_btn) buttons.addWidget(copy_btn) buttons.addWidget(close_btn) layout.addLayout(buttons) dialog.resize(540, 140) dialog.exec() def apply_profile_switch(self, profile_name: str): self.app.set_active_profile(profile_name) self.profile_label.setText(f"Current Profile: {profile_name}") def closeEvent(self, event): if not self.app.has_active_download_tasks(): event.accept() return answer = QMessageBox.question( self, "Downloads in progress", "Downloads are still running. Close the application and cancel them?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if answer == QMessageBox.StandardButton.Yes: self.app.cancel_download_tasks() event.accept() else: event.ignore()