ErrorDialog window geometry fix, Lyrics window simplification, PlaylistsPane no longer triggers cell data changed event, PlaylistsPane open root nodes by default

This commit is contained in:
billypom on debian 2024-08-06 19:24:00 -04:00
parent c9b78db5b1
commit dcd0844d99
7 changed files with 76 additions and 46 deletions

View File

@ -1,6 +1,6 @@
# MusicPom # MusicPom
PyQt5 music player inspired by MusicBee & iTunes PyQt5 music player for Linux inspired by MusicBee & iTunes
## Installation: ## Installation:
clone the repo clone the repo
@ -27,9 +27,9 @@ python3 main.py
## Todo: ## Todo:
- [x] Right-click menu - [x] right-click menu
- [x] Editable lyrics textbox - [x] editable lyrics window
- [ ] Delete songs from library (del key || right-click delete) - [ ] playlists
- [ ] delete songs from library (del key || right-click delete)
- [ ] .wav, .ogg, .flac convertor - [ ] .wav, .ogg, .flac convertor
- [ ] batch metadata changer (red text on fields that have differing info) - [ ] batch metadata changer (red text on fields that have differing info)
- [ ] Alternatives to Gstreamer?

View File

@ -1,17 +1,17 @@
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton
class ErrorDialog(QDialog): class ErrorDialog(QDialog):
def __init__(self, message, parent=None): def __init__(self, message, parent=None):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("An error occurred") self.setWindowTitle("An error occurred")
self.setGeometry(100,100,400,200)
layout = QVBoxLayout() layout = QVBoxLayout()
self.label = QLabel(message) self.label = QLabel(message)
layout.addWidget(self.label) layout.addWidget(self.label)
self.button = QPushButton("ok") self.button = QPushButton("ok")
self.button.clicked.connect(self.accept) # press ok self.button.clicked.connect(self.accept) # press ok
layout.addWidget(self.button) layout.addWidget(self.button)
self.setLayout(layout) self.setLayout(layout)

View File

@ -6,26 +6,20 @@ from PyQt5.QtWidgets import (
QPushButton, QPushButton,
) )
from PyQt5.QtGui import QFont from PyQt5.QtGui import QFont
from components.ErrorDialog import ErrorDialog
from utils import set_id3_tag from utils import set_id3_tag
class LyricsWindow(QDialog): class LyricsWindow(QDialog):
def __init__(self, song_filepath, lyrics): def __init__(self, song_filepath: str, lyrics: str):
super(LyricsWindow, self).__init__() super(LyricsWindow, self).__init__()
self.setWindowTitle("Lyrics") self.setWindowTitle("Lyrics")
self.lyrics = lyrics self.setMinimumSize(400, 400)
self.song_filepath = song_filepath self.lyrics: str = lyrics
self.input_field = "empty" self.song_filepath: str = song_filepath
layout = QVBoxLayout() layout = QVBoxLayout()
# label = QLabel("Lyrics")
# layout.addWidget(label)
# Labels & input fields # Labels & input fields
self.input_fields = {}
lyrics_label = QLabel("Lyrics")
lyrics_label.setFont(QFont("Sans", weight=QFont.Bold)) # bold category
lyrics_label.setStyleSheet("text-transform:uppercase;") # uppercase category
layout.addWidget(lyrics_label)
self.input_field = QPlainTextEdit(self.lyrics) self.input_field = QPlainTextEdit(self.lyrics)
layout.addWidget(self.input_field) layout.addWidget(self.input_field)
@ -44,6 +38,9 @@ class LyricsWindow(QDialog):
) )
if success: if success:
print("success! yay") print("success! yay")
error_dialog = ErrorDialog("Could not save lyrics :( sad")
error_dialog.exec()
else: else:
print("NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN") error_dialog = ErrorDialog("Could not save lyrics :( sad")
error_dialog.exec()
self.close() self.close()

View File

@ -17,6 +17,7 @@ from PyQt5.QtWidgets import (
QAbstractItemView, QAbstractItemView,
) )
from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt, pyqtSignal, QTimer from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt, pyqtSignal, QTimer
from components.ErrorDialog import ErrorDialog
from components.LyricsWindow import LyricsWindow from components.LyricsWindow import LyricsWindow
from components.AddToPlaylistWindow import AddToPlaylistWindow from components.AddToPlaylistWindow import AddToPlaylistWindow
from utils.delete_song_id_from_database import delete_song_id_from_database from utils.delete_song_id_from_database import delete_song_id_from_database
@ -42,7 +43,9 @@ class MusicTable(QTableView):
super().__init__(parent) super().__init__(parent)
# Necessary for actions related to cell values # Necessary for actions related to cell values
self.model = QStandardItemModel(self) self.model = QStandardItemModel(self)
self.setModel(self.model) # Same as above self.setModel(self.model)
# Config
self.config = configparser.ConfigParser() self.config = configparser.ConfigParser()
self.config.read("config.ini") self.config.read("config.ini")
# gui names of headers # gui names of headers
@ -81,11 +84,12 @@ class MusicTable(QTableView):
self.doubleClicked.connect(self.set_current_song_filepath) self.doubleClicked.connect(self.set_current_song_filepath)
self.enterKey.connect(self.set_current_song_filepath) self.enterKey.connect(self.set_current_song_filepath)
self.deleteKey.connect(self.delete_songs) self.deleteKey.connect(self.delete_songs)
self.load_music_table()
self.setup_keyboard_shortcuts()
self.model.dataChanged.connect(self.on_cell_data_changed) # editing cells self.model.dataChanged.connect(self.on_cell_data_changed) # editing cells
self.model.layoutChanged.connect(self.restore_scroll_position) self.model.layoutChanged.connect(self.restore_scroll_position)
self.load_music_table()
self.setup_keyboard_shortcuts()
def contextMenuEvent(self, event): def contextMenuEvent(self, event):
"""Right-click context menu for rows in Music Table""" """Right-click context menu for rows in Music Table"""
menu = QMenu(self) menu = QMenu(self)
@ -181,10 +185,12 @@ class MusicTable(QTableView):
if selected_song_filepath is None: if selected_song_filepath is None:
return return
current_song = self.get_selected_song_metadata() current_song = self.get_selected_song_metadata()
# print(f"MusicTable.py | show_lyrics_menu | current song: {current_song}")
try: try:
# Have to use USLT::XXX to retrieve uslt_tags = [tag for tag in current_song.keys() if tag.startswith("USLT::")]
lyrics = current_song["USLT::XXX"].text if uslt_tags:
lyrics = next((current_song[tag].text for tag in uslt_tags), "")
else:
raise RuntimeError("No USLT tags found in song metadata")
except Exception as e: except Exception as e:
print(f"MusicTable.py | show_lyrics_menu | could not retrieve lyrics | {e}") print(f"MusicTable.py | show_lyrics_menu | could not retrieve lyrics | {e}")
lyrics = "" lyrics = ""
@ -292,15 +298,24 @@ class MusicTable(QTableView):
try: try:
# Read file metadata # Read file metadata
audio = ID3(filepath) audio = ID3(filepath)
artist = ( try:
audio["TPE1"].text[0] if not "" or None else "Unknown Artist" artist = audio["TPE1"].text[0]
) if artist == "":
album = audio["TALB"].text[0] if not "" or None else "Unknown Album" artist = "Unknown Artist"
except KeyError:
artist = "Unknown Artist"
try:
album = audio["TALB"].text[0]
if album == "":
album = "Unknown Album"
except KeyError:
album = "Unknown Album"
# Determine the new path that needs to be made # Determine the new path that needs to be made
new_path = os.path.join( new_path = os.path.join(
target_dir, artist, album, os.path.basename(filepath) target_dir, artist, album, os.path.basename(filepath)
) )
print(new_path)
# Create the directories if they dont exist # Create the directories if they dont exist
os.makedirs(os.path.dirname(new_path), exist_ok=True) os.makedirs(os.path.dirname(new_path), exist_ok=True)
# Move the file to the new directory # Move the file to the new directory
@ -313,11 +328,16 @@ class MusicTable(QTableView):
) )
print(f"Moved: {filepath} -> {new_path}") print(f"Moved: {filepath} -> {new_path}")
except Exception as e: except Exception as e:
print(f"Error moving file: {filepath} | {e}") logging.warning(
f"MusicTable.py reorganize_selected_files() | Error moving file: {filepath} | {e}"
)
print(
f"MusicTable.py reorganize_selected_files() | Error moving file: {filepath} | {e}"
)
# Draw the rest of the owl # Draw the rest of the owl
self.model.dataChanged.disconnect(self.on_cell_data_changed) # self.model.dataChanged.disconnect(self.on_cell_data_changed)
self.load_music_table() self.load_music_table()
self.model.dataChanged.connect(self.on_cell_data_changed) # self.model.dataChanged.connect(self.on_cell_data_changed)
QMessageBox.information( QMessageBox.information(
self, "Reorganization complete", "Files successfully reorganized" self, "Reorganization complete", "Files successfully reorganized"
) )
@ -329,7 +349,21 @@ class MusicTable(QTableView):
self.playPauseSignal.emit() self.playPauseSignal.emit()
def load_music_table(self, *playlist_id): def load_music_table(self, *playlist_id):
"""Initializes the tableview model""" """
Loads data into self (QTableView)
Default to loading all songs.
If playlist_id is given, load songs in a particular playlist
"""
try:
# Loading the table also causes cell data to change, technically
# so we must disconnect the dataChanged trigger before loading
# then re-enable after we are done loading
self.model.dataChanged.disconnect(self.on_cell_data_changed)
except Exception as e:
print(
f"MusicTable.py load_music_table() | could not disconnect on_cell_data_changed trigger: {e}"
)
pass
self.vertical_scroll_position = ( self.vertical_scroll_position = (
self.verticalScrollBar().value() self.verticalScrollBar().value()
) # Get my scroll position before clearing ) # Get my scroll position before clearing
@ -339,9 +373,6 @@ class MusicTable(QTableView):
if playlist_id: if playlist_id:
playlist_id = playlist_id[0] playlist_id = playlist_id[0]
# Fetch playlist data # Fetch playlist data
print(
f"MusicTable.py load_music_table() | fetching playlist data, playlist_id: {playlist_id}"
)
try: try:
with DBA.DBAccess() as db: with DBA.DBAccess() as db:
data = db.query( data = db.query(
@ -354,7 +385,6 @@ class MusicTable(QTableView):
) )
return return
else: else:
print("MusicTable.py load_music_table() | fetching library data")
# Fetch library data # Fetch library data
try: try:
with DBA.DBAccess() as db: with DBA.DBAccess() as db:
@ -376,11 +406,15 @@ class MusicTable(QTableView):
# row = self.model.rowCount() - 1 # row = self.model.rowCount() - 1
for item in items: for item in items:
item.setData(id, Qt.UserRole) item.setData(id, Qt.UserRole)
# Update the viewport/model
self.model.layoutChanged.emit() # emits a signal that the view should be updated self.model.layoutChanged.emit() # emits a signal that the view should be updated
try:
self.model.dataChanged.connect(self.on_cell_data_changed)
except Exception:
pass
def restore_scroll_position(self) -> None: def restore_scroll_position(self) -> None:
"""Restores the scroll position""" """Restores the scroll position"""
print("restore_scroll_position")
QTimer.singleShot( QTimer.singleShot(
100, 100,
lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position), lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position),

View File

@ -11,6 +11,7 @@ class PlaylistWidgetItem(QTreeWidgetItem):
class PlaylistsPane(QTreeWidget): class PlaylistsPane(QTreeWidget):
playlistChoiceSignal = pyqtSignal(int) playlistChoiceSignal = pyqtSignal(int)
allSongsSignal = pyqtSignal()
def __init__(self: QTreeWidget, parent=None): def __init__(self: QTreeWidget, parent=None):
super().__init__(parent) super().__init__(parent)
@ -27,6 +28,9 @@ class PlaylistsPane(QTreeWidget):
branch = PlaylistWidgetItem(self, playlist[0], playlist[1]) branch = PlaylistWidgetItem(self, playlist[0], playlist[1])
playlists_root.addChild(branch) playlists_root.addChild(branch)
library_root.setExpanded(True)
playlists_root.setExpanded(True)
self.currentItemChanged.connect(self.playlist_clicked) self.currentItemChanged.connect(self.playlist_clicked)
self.playlist_db_id_choice: int | None = None self.playlist_db_id_choice: int | None = None
@ -39,4 +43,4 @@ class PlaylistsPane(QTreeWidget):
self.all_songs_selected() self.all_songs_selected()
def all_songs_selected(self): def all_songs_selected(self):
print("all songs") self.allSongsSignal.emit()

View File

@ -121,6 +121,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.playlistTreeView.playlistChoiceSignal.connect( self.playlistTreeView.playlistChoiceSignal.connect(
self.tableView.load_music_table self.tableView.load_music_table
) )
self.playlistTreeView.allSongsSignal.connect(self.tableView.load_music_table)
# albumGraphicsView # albumGraphicsView
self.albumGraphicsView.albumArtDropped.connect( self.albumGraphicsView.albumArtDropped.connect(

View File

@ -38,28 +38,22 @@ def get_id3_tags(file):
try: try:
# Open the MP3 file and read its content # Open the MP3 file and read its content
audio = ID3(file) audio = ID3(file)
print("1")
if os.path.exists(file): if os.path.exists(file):
audio.save(os.path.abspath(file)) audio.save(os.path.abspath(file))
print("a")
# If 'TIT2' tag is not set, add it with a default value (title will be the filename without extension) # If 'TIT2' tag is not set, add it with a default value (title will be the filename without extension)
title = os.path.splitext(os.path.basename(file))[0] title = os.path.splitext(os.path.basename(file))[0]
for key in list(audio.keys()): for key in list(audio.keys()):
if key == "TIT2": if key == "TIT2":
print("key = tit2")
audio[key].text[0] = title audio[key].text[0] = title
break break
else: else:
tit2_tag = TIT2(encoding=3, text=[title]) tit2_tag = TIT2(encoding=3, text=[title])
audio["TIT2"] = tit2_tag audio["TIT2"] = tit2_tag
print("b")
# Save the updated tags # Save the updated tags
audio.save() audio.save()
print("c")
except Exception as e: except Exception as e:
print(f"get_id3_tags.py | Could not assign file ID3 tag: {e}") print(f"get_id3_tags.py | Could not assign file ID3 tag: {e}")