737 lines
25 KiB
Python
737 lines
25 KiB
Python
"""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())
|