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:
parent
c9b78db5b1
commit
dcd0844d99
10
README.md
10
README.md
@ -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?
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
1
main.py
1
main.py
@ -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(
|
||||||
|
|||||||
@ -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}")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user