"""File browser implementation with tools and audio playback. this includes a midi player""" # flake8: noqa: E501 # This is the main file this file is the one to run. import sys import os import warnings from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QPushButton, QTreeView, QLabel, QHBoxLayout, QLineEdit, QProgressBar, QSplitter, QMessageBox, QSlider, QMenu, QFileDialog, QDialog, QToolTip, QFrame, ) from PyQt6.QtCore import Qt, QDir, QUrl, QTimer, QEvent, QPoint from PyQt6.QtGui import QFileSystemModel, QCursor from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput from MidPlay import MidPlay from timer_m import Timer_Ui from ScanOrg101 import ( Organizer, FileFilterProxyModel, DirectoryFilterProxyModel, ArchiveExtractor, mutagen, ) warnings.filterwarnings("ignore", category=DeprecationWarning) class MetadataTooltip(QFrame): """Custom styled tooltip for displaying audio metadata""" def __init__(self, parent=None): super().__init__( parent, Qt.WindowType.ToolTip | Qt.WindowType.FramelessWindowHint, ) self.setStyleSheet( """ MetadataTooltip { background-color: #2a2a2a; border: 1px solid #555555; border-radius: 5px; color: white; padding: 5px; } QLabel { color: white; } QLabel#title { font-weight: bold; font-size: 14px; color: #e0e0e0; } """ ) layout = QVBoxLayout(self) self.title_label = QLabel("") self.title_label.setObjectName("title") self.artist_label = QLabel("") self.album_label = QLabel("") self.genre_label = QLabel("") self.year_label = QLabel("") self.size_label = QLabel("") layout.addWidget(self.title_label) layout.addWidget(self.artist_label) layout.addWidget(self.album_label) layout.addWidget(self.genre_label) layout.addWidget(self.year_label) layout.addWidget(self.size_label) self.setLayout(layout) self.hide() def set_metadata(self, metadata): """Update the tooltip with metadata""" self.title_label.setText(metadata["title"]) self.artist_label.setText(f"Artist: {metadata['artist']}") self.album_label.setText(f"Album: {metadata['album']}") self.genre_label.setText(f"Genre: {metadata['genre']}") self.year_label.setText(f"Year: {metadata['year']}") self.size_label.setText(f"Size: {metadata['size']}") # Adjust size to fit content self.adjustSize() class Fbrowser(QMainWindow): """Fbrowser main class""" def __init__(self): super().__init__() self.midplay = None self.current_path = QDir.homePath() self.organizer = Organizer() self.player = QMediaPlayer() self.audio_output = QAudioOutput() self.player.setAudioOutput(self.audio_output) self.audio_output.setVolume(0.5) # 50% volume self.init_ui() self.timer = Timer_Ui() self.history = [] self.history_position = -1 self.hover_timer = QTimer() self.hover_timer.setSingleShot(True) self.hover_timer.timeout.connect(self.on_hover_timeout) self.hover_position = None self.hover_path = None self.metadata_cache = {} def init_ui(self): """initilize UI""" self.setWindowTitle("File Browser") central_widget = QWidget() layout = QVBoxLayout(central_widget) # Address bar address_layout = QHBoxLayout() self.address_bar = QLineEdit(self.current_path) self.address_bar.returnPressed.connect(self.navigate_to_address) address_layout.addWidget(self.address_bar) # Navigation buttons back_button = QPushButton("Back") back_button.clicked.connect(self.go_back) address_layout.addWidget(back_button) forward_button = QPushButton("Forward") forward_button.clicked.connect(self.go_forward_directory) address_layout.addWidget(forward_button) up_button = QPushButton("Up") up_button.clicked.connect(self.go_up_directory) address_layout.addWidget(up_button) layout.addLayout(address_layout) # Add a progress bar self.progress_bar = QProgressBar() layout.addWidget(self.progress_bar) # File view with splitter splitter = QSplitter() # Directory tree self.tree_model = QFileSystemModel() self.tree_model.setRootPath(self.current_path) self.directory_model = DirectoryFilterProxyModel() self.directory_model.setSourceModel(self.tree_model) self.file_tree = QTreeView() self.file_tree.setModel(self.directory_model) self.file_tree.setRootIndex( self.directory_model.mapFromSource( self.tree_model.index(self.current_path) ) ) self.file_tree.setHeaderHidden(True) self.file_tree.clicked.connect(self.change_directory) splitter.addWidget(self.file_tree) # File list self.list_model = QFileSystemModel() self.list_model.setRootPath(self.current_path) self.file_filter_model = FileFilterProxyModel() self.file_filter_model.setSourceModel(self.list_model) self.folder_contents_view = QTreeView() self.folder_contents_view.setModel(self.file_filter_model) self.folder_contents_view.setRootIndex( self.file_filter_model.mapFromSource( self.list_model.index(self.current_path) ) ) self.folder_contents_view.setEditTriggers( QTreeView.EditTrigger.NoEditTriggers ) self.folder_contents_view.doubleClicked.connect( self.on_item_double_clicked ) splitter.addWidget(self.folder_contents_view) # Set up context menu for both views self.folder_contents_view.setContextMenuPolicy( Qt.ContextMenuPolicy.CustomContextMenu ) self.folder_contents_view.customContextMenuRequested.connect( self.on_folder_contents_context_menu ) self.file_tree.setContextMenuPolicy( Qt.ContextMenuPolicy.CustomContextMenu ) self.file_tree.customContextMenuRequested.connect( self.on_file_tree_context_menu ) layout.addWidget(splitter) # Current directory label self.current_dir_label = QLabel(self.current_path) layout.addWidget(self.current_dir_label) # Media controls media_controls = QHBoxLayout() play_button = QPushButton("Play") play_button.clicked.connect(self.player.play) media_controls.addWidget(play_button) pause_button = QPushButton("Pause") pause_button.clicked.connect(self.player.pause) media_controls.addWidget(pause_button) stop_button = QPushButton("Stop") stop_button.clicked.connect(self.player.stop) media_controls.addWidget(stop_button) volume_slider = QSlider(Qt.Orientation.Horizontal) volume_slider.setRange(0, 100) volume_slider.setValue(50) volume_slider.valueChanged.connect(self.set_volume) media_controls.addWidget(volume_slider) layout.addLayout(media_controls) # MidPlay button open_midplay_button = QPushButton("Open MidPlay") open_midplay_button.clicked.connect(self.open_midplay) layout.addWidget(open_midplay_button) # Timer Button self.timer_button = QPushButton("Start Timer") self.timer_button.clicked.connect(self.open_timer) layout.addWidget(self.timer_button) # Exit button exit_button = QPushButton("Exit") exit_button.clicked.connect(self.show_exit_popup) layout.addWidget(exit_button) self.setCentralWidget(central_widget) self.resize(800, 600) self.folder_contents_view.setMouseTracking(True) self.folder_contents_view.viewport().setMouseTracking(True) # type: ignore self.folder_contents_view.viewport().installEventFilter(self) # type: ignore def set_volume(self, volume): """Set Volume""" self.audio_output.setVolume(volume / 100.0) def show_exit_popup(self): """Exit Prompt""" reply = QMessageBox.question( self, "Exit", "Are you sure you want to exit?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if reply == QMessageBox.StandardButton.Yes: sys.exit() def navigate_to_address(self): """Navigation Via Address Bar""" new_path = self.address_bar.text() if os.path.isdir(new_path): self.add_to_history(self.current_path) self.current_path = new_path self.update_file_views(new_path) else: self.address_bar.setText(self.current_path) def add_to_history(self, path): """History""" # If we're not at the end of the history, truncate it if self.history_position < len(self.history) - 1: self.history = self.history[: self.history_position + 1] self.history.append(path) self.history_position = len(self.history) - 1 def change_directory(self, index): """Change dir to user dir""" index = self.directory_model.mapToSource(index) try: file_path = self.tree_model.filePath(index) if os.path.isdir(file_path): self.add_to_history(self.current_path) self.current_path = file_path self.update_file_views(file_path) except (OSError, IOError) as e: print(f"Error Changing Dirs.: {e}") def update_file_views(self, path): """Updates the File trees""" self.address_bar.setText(path) self.current_dir_label.setText(path) # Update tree view self.file_tree.setRootIndex( self.directory_model.mapFromSource(self.tree_model.index(path)) ) # Update file list view self.list_model.setRootPath(path) self.folder_contents_view.setRootIndex( self.file_filter_model.mapFromSource(self.list_model.index(path)) ) def on_item_double_clicked(self, index): """on double click do stuff""" source_index = self.file_filter_model.mapToSource(index) path = self.list_model.filePath(source_index) if os.path.isdir(path): self.add_to_history(self.current_path) self.current_path = path self.update_file_views(path) else: self.play_file(path) def play_file(self, file_path): """Play a Audio file if midi load midi player""" if file_path.lower().endswith((".mid", ".midi")): if not self.midplay: self.midplay = MidPlay() self.midplay.add_to_playlist(file_path) self.midplay.load_midi(file_path) self.midplay.play_midi(file_path) else: # For audio files self.player.setSource(QUrl.fromLocalFile(file_path)) self.player.play() def go_back(self): """Go back a dir""" if self.history_position > 0: self.history_position -= 1 path = self.history[self.history_position] self.current_path = path self.update_file_views(path) def go_forward_directory(self): """Go forward a dir""" if self.history_position < len(self.history) - 1: self.history_position += 1 path = self.history[self.history_position] self.current_path = path self.update_file_views(path) def go_up_directory(self): """Similar to back""" parent_dir = os.path.dirname(self.current_path) if parent_dir != self.current_path: self.add_to_history(self.current_path) self.current_path = parent_dir self.update_file_views(parent_dir) def open_midplay(self): """Opens the midi player""" if not self.midplay: self.midplay = MidPlay() self.midplay.showUI() def open_timer(self): """Opens the Timer""" self.timer.showUI() def create_context_menu(self, position, view): """Create and display a context menu for files and folders""" # Get the index at position if view == self.folder_contents_view: index = self.folder_contents_view.indexAt(position) if not index.isValid(): return # Map to source model to get the actual file path source_index = self.file_filter_model.mapToSource(index) path = self.list_model.filePath(source_index) else: # Directory tree index = self.file_tree.indexAt(position) if not index.isValid(): return source_index = self.directory_model.mapToSource(index) path = self.tree_model.filePath(source_index) # Create menu menu = QMenu() if os.path.isdir(path): # Directory options scan_action = menu.addAction("Scan Directory") scan_action.triggered.connect(lambda: self.scan_directory(path)) # type: ignore extract_metadata_action = menu.addAction("Extract Audio Metadata") extract_metadata_action.triggered.connect( # type: ignore lambda: self.extract_audio_metadata(path) ) menu.addSeparator() elif os.path.isfile(path): # File options if path.lower().endswith((".zip", ".rar", ".7z")): # Archive options extract_here_action = menu.addAction("Extract Here") extract_here_action.triggered.connect( # type: ignore lambda: self.extract_archive(path, self.current_path) ) extract_to_action = menu.addAction("Extract To...") extract_to_action.triggered.connect( # type: ignore lambda: self.extract_archive_to(path) ) if path.lower().endswith( (".mp3", ".wav", ".flac", ".m4a", ".wma", ".mid", ".midi") ): # Audio file options play_action = menu.addAction("Play") play_action.triggered.connect(lambda: self.play_file(path)) # type: ignore extract_metadata_action = menu.addAction("Extract Metadata") extract_metadata_action.triggered.connect( # type: ignore lambda: self.extract_file_metadata(path) ) # Add general options menu.addSeparator() refresh_action = menu.addAction("Refresh") refresh_action.triggered.connect( # type: ignore lambda: self.update_file_views(self.current_path) ) # Show menu menu.exec(view.viewport().mapToGlobal(position)) def on_folder_contents_context_menu(self, position): """Handle context menu in folder contents view""" self.create_context_menu(position, self.folder_contents_view) def on_file_tree_context_menu(self, position): """Handle context menu in file tree view""" self.create_context_menu(position, self.file_tree) def scan_directory(self, path): """Scan a directory using ScanOrg101""" self.progress_bar.setValue(0) # Connect organizer signals self.organizer.on_progress_update = self.update_progress # type: ignore self.organizer.on_scan_complete = lambda: self.show_scan_results(path) # type: ignore # Start scan self.organizer.start_scan(path) def extract_archive(self, archive_path, extract_dir): """Extract an archive to specified directory""" self.progress_bar.setValue(0) # Create an extractor instance self.extract_dir = extract_dir self.archive_extractor = self.organizer.archive_extractor = ( ArchiveExtractor(archive_path, extract_dir) ) # Connect signals self.archive_extractor.extraction_progress.connect( self.update_progress ) self.archive_extractor.extraction_complete.connect( self.extraction_complete ) self.archive_extractor.extraction_error.connect( self.show_error_message ) # Start extraction self.archive_extractor.start() def extract_archive_to(self, archive_path): """Extract an archive to a user-selected directory""" extract_dir = QFileDialog.getExistingDirectory( self, "Select Extraction Directory", self.current_path ) if extract_dir: self.extract_archive(archive_path, extract_dir) def extraction_complete(self, extracted_files): """Handle archive extraction completion""" self.progress_bar.setValue(100) self.update_file_views(self.extract_dir) self.show_message( f"Extraction complete: {len(extracted_files)} files extracted" ) def extract_audio_metadata(self, directory_path): """Extract metadata from audio files in a directory""" self.progress_bar.setValue(0) # Scan directory first to get file list self.organizer.on_progress_update = self.update_progress # type: ignore self.organizer.on_scan_complete = ( # type: ignore lambda: self.start_metadata_extraction(directory_path) ) self.organizer.start_scan(directory_path) def start_metadata_extraction(self, directory_path): """Start metadata extraction after scanning is complete""" self.organizer.on_progress_update = self.update_progress # type: ignore self.organizer.on_metadata_complete = ( # type: ignore lambda: self.show_metadata_results() ) self.organizer.extract_metadata() def extract_file_metadata(self, file_path): """Extract metadata from a single audio file""" self.progress_bar.setValue(0) # Create a temporary file list with just this file self.organizer.file_list = [file_path] # Set up extraction self.organizer.on_progress_update = self.update_progress # type: ignore self.organizer.on_metadata_complete = ( # type: ignore lambda: self.show_metadata_results(single_file=True) ) # Start extraction self.organizer.extract_metadata() def update_progress(self, value): """Update the progress bar""" self.progress_bar.setValue(value) def show_scan_results(self, path): """Display scan results""" self.progress_bar.setValue(100) file_count = len(self.organizer.file_list) dir_count = len(self.organizer.dir_list) self.show_message( f"Scan complete: {file_count} files, {dir_count} directories found" ) def show_metadata_results(self, single_file=False): """Display metadata extraction results""" self.progress_bar.setValue(100) if single_file: self.show_message("Metadata extraction complete") # dialog to show metadata in a context menu self.show_metadata_dialog() else: artists = len(self.organizer.artists) albums = len(self.organizer.albums) genres = len(self.organizer.genres) self.show_message( f"Metadata extraction complete: {artists} artists, {albums} albums, {genres} genres found" ) def show_metadata_dialog(self): """Show a dialog with metadata information""" # Create a dialog with metadata information dialog = QDialog(self) dialog.setWindowTitle("Metadata Information") # layout = QVBoxLayout() def show_message(self, message): """Show a message in a message box""" QMessageBox.information(self, "Information", message) def show_error_message(self, error): """Show an error message""" QMessageBox.critical(self, "Error", error) def eventFilter(self, obj, event): """Handle hover events for metadata tooltips""" if obj == self.folder_contents_view.viewport(): if event.type() == QEvent.Type.HoverMove: pos = event.position().toPoint() # Get the item under the cursor index = self.folder_contents_view.indexAt(pos) if index.isValid(): source_index = self.file_filter_model.mapToSource(index) path = self.list_model.filePath(source_index) # Only proceed for audio files if path.lower().endswith( ( ".mp3", ".wav", ".flac", ".m4a", ".wma", ".mid", ".midi", ) ): # If we've moved to a new position or new file, restart the timer if self.hover_path != path or ( self.hover_position and ( abs(self.hover_position.x() - pos.x()) > 5 or abs(self.hover_position.y() - pos.y()) > 5 ) ): self.hover_timer.stop() self.hover_position = pos self.hover_path = path self.hover_timer.start(1200) return True else: # Not an audio file, stop any running timer self.hover_timer.stop() self.hover_path = None else: # No valid item under cursor self.hover_timer.stop() self.hover_path = None elif event.type() == QEvent.Type.Leave: # Mouse left the widget, stop the timer self.hover_timer.stop() self.hover_path = None QToolTip.hideText() return super().eventFilter(obj, event) def on_hover_timeout(self): """Called when the hover timer expires, extract and show metadata""" if not self.hover_path: return # Check if we already have metadata for this file in cache if self.hover_path in self.metadata_cache: self.show_metadata_tooltip(self.metadata_cache[self.hover_path]) else: # Extract metadata in background self.extract_hover_metadata(self.hover_path) def extract_hover_metadata(self, file_path): """Extract metadata for the hovered file""" try: # Use mutagen directly for speed audio = mutagen.File(file_path) # type: ignore if not audio: return # Extract basic metadata metadata = { "file_path": file_path, "filename": os.path.basename(file_path), "size": f"{os.path.getsize(file_path) / (1024*1024):.2f} MB", "artist": self._get_tag(audio, "artist", "Unknown Artist"), "album": self._get_tag(audio, "album", "Unknown Album"), "title": self._get_tag( audio, "title", os.path.basename(file_path) ), "genre": self._get_tag(audio, "genre", "Unknown Genre"), "year": self._get_tag(audio, "date", "Unknown Year"), } # Cache the metadata self.metadata_cache[file_path] = metadata # Show the tooltip if we're still hovering over the same file if self.hover_path == file_path: self.show_metadata_tooltip(metadata) except Exception as e: print(f"Error extracting metadata for hover: {e}") def _get_tag(self, audio, tag_name, default_value): """Helper method to safely extract tags from audio files""" try: if tag_name in audio: value = audio[tag_name] if isinstance(value, list) and len(value) > 0: return str(value[0]) return str(value) except Exception: pass return default_value def show_metadata_tooltip(self, metadata): """Display metadata in a stylish custom tooltip""" # Create or get the tooltip if not hasattr(self, "metadata_tooltip"): self.metadata_tooltip = MetadataTooltip() # Update tooltip content self.metadata_tooltip.set_metadata(metadata) # Position near but not under cursor cursor_pos = QCursor.pos() tooltip_pos = QPoint(cursor_pos.x() + 15, cursor_pos.y() + 15) # Show the tooltip self.metadata_tooltip.move(tooltip_pos) self.metadata_tooltip.show() # Auto-hide after 8 seconds QTimer.singleShot(8000, self.metadata_tooltip.hide) if __name__ == "__main__": app = QApplication(sys.argv) ex = Fbrowser() ex.show() sys.exit(app.exec())