generated from LouisMazin/PythonApplicationTemplate
400 lines
17 KiB
Python
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) |