from typing import Optional from PyQt6.QtCore import Qt, QUrl, QSize, pyqtSignal from PyQt6.QtGui import QIcon, QPixmap from PyQt6.QtMultimedia import QAudioOutput, QMediaPlayer from PyQt6.QtMultimediaWidgets import QVideoWidget from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest from PyQt6.QtWidgets import ( QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QPushButton, QStackedWidget, QVBoxLayout, QWidget, ) from app.core.main_manager import MainManager, NotificationType from app.ui.widgets.loading_bar import LoadingBar class AddMusicWindow(QWidget): from PyQt6.QtCore import QThread, pyqtSignal from app.ui.widgets.loading_bar import LoadingBar class PlaylistAddThread(QThread): progress = pyqtSignal(int, int) finished = pyqtSignal(bool, int) failed = pyqtSignal(str) def __init__(self, download_manager, url, parent=None): super().__init__(parent) self.download_manager = download_manager self.url = url def run(self): try: tracks = self.download_manager._extract_playlist_tracks(self.url) total = len(tracks) if not tracks: self.failed.emit("playlist_expand_failed") self.finished.emit(False, 0) return for i, track in enumerate(tracks, 1): self.download_manager.track_list.append(track) self.progress.emit(i, total) self.download_manager.last_playlist_added_count = total self.download_manager.list_changed.emit(self.download_manager.get_tracks()) self.finished.emit(True, total) except Exception as e: self.failed.emit(str(e)) self.finished.emit(False, 0) RESULTS_VIEW_INDEX = 0 PLAYER_VIEW_INDEX = 1 def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.main_manager = MainManager.get_instance() self.language_manager = self.main_manager.get_language_manager() self.alert_manager = self.main_manager.get_alert_manager() self.youtube_manager = self.main_manager.get_youtube_manager() self.metadata_manager = self.main_manager.get_metadata_manager() self.observer_manager = self.main_manager.get_observer_manager() self.observer_manager.subscribe(NotificationType.LANGUAGE, self.update_language) self.youtube_manager.failed.connect(self.on_manager_error) self.youtube_manager.search_started.connect(self.on_search_started) self.youtube_manager.search_results.connect(self.on_search_results) self.youtube_manager.preview_ready.connect(self.on_preview_ready) self.youtube_manager.preview_failed.connect(self.on_preview_failed) self.thumbnail_manager = QNetworkAccessManager(self) self.thumbnail_replies: dict[QNetworkReply, QListWidgetItem] = {} self.current_video: Optional[dict] = None self.audio_output = QAudioOutput(self) self.video_player = QMediaPlayer(self) self.video_player.setAudioOutput(self.audio_output) self.player_widget = None self.setup_ui() # Always initialize search_results and related widgets self.content_stack = QStackedWidget(self) self.layout().addWidget(self.content_stack, 1) self.results_view = QWidget(self) results_layout = QVBoxLayout(self.results_view) results_layout.setContentsMargins(0, 0, 0, 0) results_layout.setSpacing(0) self.search_results = QListWidget(self.results_view) self.search_results.setSelectionMode(QListWidget.SelectionMode.SingleSelection) self.search_results.setIconSize(QSize(120, 68)) self.search_results.setSpacing(6) self.search_results.itemClicked.connect(self.on_result_selected) results_layout.addWidget(self.search_results) self.player_view = QWidget(self) player_layout = QVBoxLayout(self.player_view) player_layout.setContentsMargins(0, 0, 0, 0) player_layout.setSpacing(10) player_top_row = QHBoxLayout() self.back_to_results_button = QPushButton(self.language_manager.get_text("back_to_results"), self.player_view) self.back_to_results_button.clicked.connect(self.go_back_to_results) player_top_row.addWidget(self.back_to_results_button) player_top_row.addStretch(1) player_layout.addLayout(player_top_row) self.player_widget = self._create_player_widget() player_layout.addWidget(self.player_widget, 1) actions_row = QHBoxLayout() self.add_selected_button = QPushButton(self.language_manager.get_text("add_selected_result"), self) self.add_selected_button.clicked.connect(self.add_selected_result) self.add_selected_button.setEnabled(False) actions_row.addStretch(1) actions_row.addWidget(self.add_selected_button) player_layout.addLayout(actions_row) self.content_stack.addWidget(self.results_view) self.content_stack.addWidget(self.player_view) self.content_stack.setCurrentIndex(self.RESULTS_VIEW_INDEX) if self.player_widget and isinstance(self.player_widget, QVideoWidget): self.video_player.setVideoOutput(self.player_widget) def setup_ui(self) -> None: layout = QVBoxLayout(self) layout.setAlignment(Qt.AlignmentFlag.AlignTop) layout.setSpacing(16) layout.setContentsMargins(20, 20, 20, 20) search_row = QHBoxLayout() self.search_input = QLineEdit(self) self.search_input.setPlaceholderText(self.language_manager.get_text("search_placeholder")) search_row.addWidget(self.search_input, 1) self.search_button = QPushButton(self.language_manager.get_text("search_youtube"), self) self.search_button.clicked.connect(self.search_tracks) search_row.addWidget(self.search_button) self.link_input = QLineEdit(self) self.link_input.setPlaceholderText(self.language_manager.get_text("add_link_placeholder")) search_row.addWidget(self.link_input, 1) self.add_link_button = QPushButton(self.language_manager.get_text("add_link_button"), self) self.add_link_button.clicked.connect(self.add_link) search_row.addWidget(self.add_link_button) layout.addLayout(search_row) self.setLayout(layout) def add_link(self) -> None: url = self.link_input.text().strip() if not url: self.alert_manager.show_error("invalid_link", self) return if "list=" in url: self._show_loading_bar(self.language_manager.get_text("downloading_audio")) # Always assign as self.playlist_thread and parent to self self.playlist_thread = self.PlaylistAddThread(self.youtube_manager, url, parent=self) self.playlist_thread.progress.connect(self.on_playlist_progress) self.playlist_thread.finished.connect(self.on_playlist_finished) self.playlist_thread.failed.connect(self.on_playlist_failed) self.playlist_thread.start() else: if not self.youtube_manager.add_track(url): self.alert_manager.show_error("invalid_link", self) def _show_loading_bar(self, label: str): if hasattr(self, "loading_bar") and self.loading_bar is not None: self.loading_bar.deleteLater() self.loading_bar = LoadingBar(label, self) self.layout().addWidget(self.loading_bar) self.loading_bar.set_label(label) self.loading_bar.set_progress(0) def on_playlist_progress(self, current, total): if hasattr(self, "loading_bar") and self.loading_bar is not None: self.loading_bar.set_label(f"Ajout {current}/{total} musiques...") self.loading_bar.set_progress(int(current / total * 100) if total else 0) def on_playlist_finished(self, success, count): if hasattr(self, "loading_bar") and self.loading_bar is not None: self.loading_bar.set_label(self.language_manager.get_text("playlist_added_count").replace("{count}", str(count))) self.loading_bar.set_progress(100) self.loading_bar.deleteLater() self.loading_bar = None self.alert_manager.show_info(self.language_manager.get_text("playlist_added_count").replace("{count}", str(count)), self) def on_playlist_failed(self, error_key): if hasattr(self, "loading_bar") and self.loading_bar is not None: self.loading_bar.set_label(self.language_manager.get_text(error_key)) self.loading_bar.set_progress(0) self.loading_bar.deleteLater() self.loading_bar = None self.alert_manager.show_error(error_key, self) self.content_stack = QStackedWidget(self) self.layout().addWidget(self.content_stack, 1) self.results_view = QWidget(self) results_layout = QVBoxLayout(self.results_view) results_layout.setContentsMargins(0, 0, 0, 0) results_layout.setSpacing(0) self.search_results = QListWidget(self.results_view) self.search_results.setSelectionMode(QListWidget.SelectionMode.SingleSelection) self.search_results.setIconSize(QSize(120, 68)) self.search_results.setSpacing(6) self.search_results.itemClicked.connect(self.on_result_selected) results_layout.addWidget(self.search_results) self.player_view = QWidget(self) player_layout = QVBoxLayout(self.player_view) player_layout.setContentsMargins(0, 0, 0, 0) player_layout.setSpacing(10) player_top_row = QHBoxLayout() self.back_to_results_button = QPushButton(self.language_manager.get_text("back_to_results"), self.player_view) self.back_to_results_button.clicked.connect(self.go_back_to_results) player_top_row.addWidget(self.back_to_results_button) player_top_row.addStretch(1) player_layout.addLayout(player_top_row) self.player_widget = self._create_player_widget() player_layout.addWidget(self.player_widget, 1) actions_row = QHBoxLayout() self.add_selected_button = QPushButton(self.language_manager.get_text("add_selected_result"), self) self.add_selected_button.clicked.connect(self.add_selected_result) self.add_selected_button.setEnabled(False) actions_row.addStretch(1) actions_row.addWidget(self.add_selected_button) player_layout.addLayout(actions_row) self.content_stack.addWidget(self.results_view) self.content_stack.addWidget(self.player_view) self.content_stack.setCurrentIndex(self.RESULTS_VIEW_INDEX) def search_tracks(self) -> None: self.youtube_manager.search_tracks(self.search_input.text()) def on_search_started(self) -> None: self.search_button.setEnabled(False) self.search_button.setText(self.language_manager.get_text("searching")) def on_search_results(self, tracks: list) -> None: self._clear_thumbnail_replies() self.search_results.clear() self.current_video = None self.add_selected_button.setEnabled(False) self.video_player.stop() self.content_stack.setCurrentIndex(self.RESULTS_VIEW_INDEX) video_tracks = [track for track in tracks if track.get("kind", "video") == "video"] playlist_tracks = [track for track in tracks if track.get("kind", "") == "playlist"] for track in video_tracks: title = track.get("title", "Untitled") channel = track.get("channel", "") duration = track.get("duration", "") subtitle = f"{self.language_manager.get_text('result_video')} • {duration}" if channel: subtitle = f"{subtitle} • {channel}" item = QListWidgetItem(f"{title}\n{subtitle}") item.setData(Qt.ItemDataRole.UserRole, track) self.search_results.addItem(item) thumbnail_url = track.get("thumbnail_url", "") if thumbnail_url: self._request_thumbnail(thumbnail_url, item) for track in playlist_tracks: title = track.get("title", "Untitled playlist") channel = track.get("channel", "") item_count = track.get("item_count", "") subtitle = f"{self.language_manager.get_text('result_playlist')}" if item_count: subtitle = f"{subtitle} • {item_count} tracks" if channel: subtitle = f"{subtitle} • {channel}" item = QListWidgetItem(f"{title}\n{subtitle}") item.setData(Qt.ItemDataRole.UserRole, track) self.search_results.addItem(item) thumbnail_url = track.get("thumbnail_url", "") if thumbnail_url: self._request_thumbnail(thumbnail_url, item) total_results = len(video_tracks) + len(playlist_tracks) if not total_results: self.alert_manager.show_error(self.language_manager.get_text("no_search_results"), self) self.search_button.setEnabled(True) self.search_button.setText(self.language_manager.get_text("search_youtube")) def add_selected_result(self) -> None: row = self.search_results.currentRow() if row < 0: self.alert_manager.show_error("no_search_result_selected", self) return item_data = self.search_results.item(row).data(Qt.ItemDataRole.UserRole) if not isinstance(item_data, dict): return if self.youtube_manager.add_search_result(item_data): self.alert_manager.show_success("track_added", self) def on_result_selected(self, current_item: Optional[QListWidgetItem]) -> None: if current_item is None: self.current_video = None self.add_selected_button.setEnabled(False) return item_data = current_item.data(Qt.ItemDataRole.UserRole) if not isinstance(item_data, dict): self.current_video = None self.add_selected_button.setEnabled(False) return self.current_video = item_data self.add_selected_button.setEnabled(True) self._load_video_in_player(item_data) def go_back_to_results(self) -> None: self.video_player.stop() self.content_stack.setCurrentIndex(self.RESULTS_VIEW_INDEX) def _load_video_in_player(self, track: dict) -> None: self.youtube_manager.start_preview(track) def _create_player_widget(self) -> QWidget: video_widget = QVideoWidget(self) video_widget.setMinimumHeight(320) return video_widget def on_preview_ready(self, stream_url: str, title: str) -> None: self.video_player.setSource(QUrl(stream_url)) self.video_player.play() self.content_stack.setCurrentIndex(self.PLAYER_VIEW_INDEX) def on_preview_failed(self, error_key: str) -> None: self.content_stack.setCurrentIndex(self.RESULTS_VIEW_INDEX) self.alert_manager.show_error(error_key, self) def _request_thumbnail(self, url: str, item: QListWidgetItem) -> None: request = QNetworkRequest(QUrl(url)) reply = self.thumbnail_manager.get(request) self.thumbnail_replies[reply] = item reply.finished.connect(lambda r=reply: self._on_thumbnail_finished(r)) def _on_thumbnail_finished(self, reply: QNetworkReply) -> None: item = self.thumbnail_replies.pop(reply, None) if item is None: reply.deleteLater() return if reply.error() == QNetworkReply.NetworkError.NoError: image_data = reply.readAll().data() if image_data: pixmap = QPixmap() if pixmap.loadFromData(image_data): icon = QIcon(pixmap.scaled(120, 68, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) item.setIcon(icon) reply.deleteLater() def _clear_thumbnail_replies(self) -> None: for reply in list(self.thumbnail_replies.keys()): reply.abort() reply.deleteLater() self.thumbnail_replies.clear() def on_manager_error(self, error_key: str) -> None: handled_errors = { "invalid_youtube_url", "metadata_fetch_failed", "invalid_search_query", "search_in_progress", "search_failed", "playlist_expand_failed", } if error_key not in handled_errors: return self.search_button.setEnabled(True) self.search_button.setText(self.language_manager.get_text("search_youtube")) self.alert_manager.show_error(error_key, self) def update_language(self) -> None: self.search_input.setPlaceholderText(self.language_manager.get_text("search_placeholder")) self.search_button.setText(self.language_manager.get_text("search_youtube")) self.back_to_results_button.setText(self.language_manager.get_text("back_to_results")) self.add_selected_button.setText(self.language_manager.get_text("add_selected_result")) def closeEvent(self, event) -> None: self.video_player.stop() self._clear_thumbnail_replies() super().closeEvent(event)