YoutubeDownloader/app/ui/windows/add_music_window.py
2026-03-18 19:28:41 +01:00

400 lines
17 KiB
Python

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)