diff --git a/README.md b/README.md index 57541f5..250c66b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MusicPom -PyQt5 music player inspired by MusicBee & iTunes +PyQt5 music player for Linux inspired by MusicBee & iTunes ## Installation: clone the repo @@ -27,9 +27,9 @@ python3 main.py ## Todo: -- [x] Right-click menu -- [x] Editable lyrics textbox -- [ ] Delete songs from library (del key || right-click delete) +- [x] right-click menu +- [x] editable lyrics window +- [ ] playlists +- [ ] delete songs from library (del key || right-click delete) - [ ] .wav, .ogg, .flac convertor - [ ] batch metadata changer (red text on fields that have differing info) -- [ ] Alternatives to Gstreamer? diff --git a/components/ErrorDialog.py b/components/ErrorDialog.py index 33b29bf..ef2c7f1 100644 --- a/components/ErrorDialog.py +++ b/components/ErrorDialog.py @@ -1,17 +1,17 @@ from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton + class ErrorDialog(QDialog): def __init__(self, message, parent=None): super().__init__(parent) self.setWindowTitle("An error occurred") - self.setGeometry(100,100,400,200) layout = QVBoxLayout() self.label = QLabel(message) layout.addWidget(self.label) self.button = QPushButton("ok") - self.button.clicked.connect(self.accept) # press ok + self.button.clicked.connect(self.accept) # press ok layout.addWidget(self.button) self.setLayout(layout) diff --git a/components/LyricsWindow.py b/components/LyricsWindow.py index 8a9603a..24fbd9f 100644 --- a/components/LyricsWindow.py +++ b/components/LyricsWindow.py @@ -6,26 +6,20 @@ from PyQt5.QtWidgets import ( QPushButton, ) from PyQt5.QtGui import QFont +from components.ErrorDialog import ErrorDialog from utils import set_id3_tag class LyricsWindow(QDialog): - def __init__(self, song_filepath, lyrics): + def __init__(self, song_filepath: str, lyrics: str): super(LyricsWindow, self).__init__() self.setWindowTitle("Lyrics") - self.lyrics = lyrics - self.song_filepath = song_filepath - self.input_field = "empty" + self.setMinimumSize(400, 400) + self.lyrics: str = lyrics + self.song_filepath: str = song_filepath layout = QVBoxLayout() - # label = QLabel("Lyrics") - # layout.addWidget(label) # 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) layout.addWidget(self.input_field) @@ -44,6 +38,9 @@ class LyricsWindow(QDialog): ) if success: print("success! yay") + error_dialog = ErrorDialog("Could not save lyrics :( sad") + error_dialog.exec() else: - print("NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN") + error_dialog = ErrorDialog("Could not save lyrics :( sad") + error_dialog.exec() self.close() diff --git a/components/MusicTable.py b/components/MusicTable.py index c042228..3a81cf7 100644 --- a/components/MusicTable.py +++ b/components/MusicTable.py @@ -17,6 +17,7 @@ from PyQt5.QtWidgets import ( QAbstractItemView, ) from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt, pyqtSignal, QTimer +from components.ErrorDialog import ErrorDialog from components.LyricsWindow import LyricsWindow from components.AddToPlaylistWindow import AddToPlaylistWindow from utils.delete_song_id_from_database import delete_song_id_from_database @@ -42,7 +43,9 @@ class MusicTable(QTableView): super().__init__(parent) # Necessary for actions related to cell values self.model = QStandardItemModel(self) - self.setModel(self.model) # Same as above + self.setModel(self.model) + + # Config self.config = configparser.ConfigParser() self.config.read("config.ini") # gui names of headers @@ -81,11 +84,12 @@ class MusicTable(QTableView): self.doubleClicked.connect(self.set_current_song_filepath) self.enterKey.connect(self.set_current_song_filepath) 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.layoutChanged.connect(self.restore_scroll_position) + self.load_music_table() + self.setup_keyboard_shortcuts() + def contextMenuEvent(self, event): """Right-click context menu for rows in Music Table""" menu = QMenu(self) @@ -181,10 +185,12 @@ class MusicTable(QTableView): if selected_song_filepath is None: return current_song = self.get_selected_song_metadata() - # print(f"MusicTable.py | show_lyrics_menu | current song: {current_song}") try: - # Have to use USLT::XXX to retrieve - lyrics = current_song["USLT::XXX"].text + uslt_tags = [tag for tag in current_song.keys() if tag.startswith("USLT::")] + 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: print(f"MusicTable.py | show_lyrics_menu | could not retrieve lyrics | {e}") lyrics = "" @@ -292,15 +298,24 @@ class MusicTable(QTableView): try: # Read file metadata audio = ID3(filepath) - artist = ( - audio["TPE1"].text[0] if not "" or None else "Unknown Artist" - ) - album = audio["TALB"].text[0] if not "" or None else "Unknown Album" + try: + artist = audio["TPE1"].text[0] + if artist == "": + 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 new_path = os.path.join( target_dir, artist, album, os.path.basename(filepath) ) - print(new_path) # Create the directories if they dont exist os.makedirs(os.path.dirname(new_path), exist_ok=True) # Move the file to the new directory @@ -313,11 +328,16 @@ class MusicTable(QTableView): ) print(f"Moved: {filepath} -> {new_path}") 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 - self.model.dataChanged.disconnect(self.on_cell_data_changed) + # self.model.dataChanged.disconnect(self.on_cell_data_changed) self.load_music_table() - self.model.dataChanged.connect(self.on_cell_data_changed) + # self.model.dataChanged.connect(self.on_cell_data_changed) QMessageBox.information( self, "Reorganization complete", "Files successfully reorganized" ) @@ -329,7 +349,21 @@ class MusicTable(QTableView): self.playPauseSignal.emit() 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.verticalScrollBar().value() ) # Get my scroll position before clearing @@ -339,9 +373,6 @@ class MusicTable(QTableView): if playlist_id: playlist_id = playlist_id[0] # Fetch playlist data - print( - f"MusicTable.py load_music_table() | fetching playlist data, playlist_id: {playlist_id}" - ) try: with DBA.DBAccess() as db: data = db.query( @@ -354,7 +385,6 @@ class MusicTable(QTableView): ) return else: - print("MusicTable.py load_music_table() | fetching library data") # Fetch library data try: with DBA.DBAccess() as db: @@ -376,11 +406,15 @@ class MusicTable(QTableView): # row = self.model.rowCount() - 1 for item in items: item.setData(id, Qt.UserRole) - # Update the viewport/model 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: """Restores the scroll position""" + print("restore_scroll_position") QTimer.singleShot( 100, lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position), diff --git a/components/PlaylistsPane.py b/components/PlaylistsPane.py index d2ae7b9..0f69ab4 100644 --- a/components/PlaylistsPane.py +++ b/components/PlaylistsPane.py @@ -11,6 +11,7 @@ class PlaylistWidgetItem(QTreeWidgetItem): class PlaylistsPane(QTreeWidget): playlistChoiceSignal = pyqtSignal(int) + allSongsSignal = pyqtSignal() def __init__(self: QTreeWidget, parent=None): super().__init__(parent) @@ -27,6 +28,9 @@ class PlaylistsPane(QTreeWidget): branch = PlaylistWidgetItem(self, playlist[0], playlist[1]) playlists_root.addChild(branch) + library_root.setExpanded(True) + playlists_root.setExpanded(True) + self.currentItemChanged.connect(self.playlist_clicked) self.playlist_db_id_choice: int | None = None @@ -39,4 +43,4 @@ class PlaylistsPane(QTreeWidget): self.all_songs_selected() def all_songs_selected(self): - print("all songs") + self.allSongsSignal.emit() diff --git a/main.py b/main.py index 1cbd76a..1a07db6 100644 --- a/main.py +++ b/main.py @@ -121,6 +121,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.playlistTreeView.playlistChoiceSignal.connect( self.tableView.load_music_table ) + self.playlistTreeView.allSongsSignal.connect(self.tableView.load_music_table) # albumGraphicsView self.albumGraphicsView.albumArtDropped.connect( diff --git a/utils/get_id3_tags.py b/utils/get_id3_tags.py index 1396ada..cc6e335 100644 --- a/utils/get_id3_tags.py +++ b/utils/get_id3_tags.py @@ -38,28 +38,22 @@ def get_id3_tags(file): try: # Open the MP3 file and read its content audio = ID3(file) - print("1") if os.path.exists(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) title = os.path.splitext(os.path.basename(file))[0] for key in list(audio.keys()): if key == "TIT2": - print("key = tit2") audio[key].text[0] = title break else: tit2_tag = TIT2(encoding=3, text=[title]) audio["TIT2"] = tit2_tag - print("b") - # Save the updated tags audio.save() - print("c") except Exception as e: print(f"get_id3_tags.py | Could not assign file ID3 tag: {e}")