Files

333 lines
12 KiB
Python

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()