table editing signal emission

This commit is contained in:
billypom on debian 2024-03-23 16:40:25 -04:00
parent 6daadb09aa
commit ef02d30d05
5 changed files with 94 additions and 53 deletions

View File

@ -1,7 +1,7 @@
import DBA import DBA
from PyQt5.QtGui import QStandardItem, QStandardItemModel, QKeySequence from PyQt5.QtGui import QStandardItem, QStandardItemModel, QKeySequence
from PyQt5.QtWidgets import QTableView, QShortcut from PyQt5.QtWidgets import QTableView, QShortcut, QMessageBox, QAbstractItemView
from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal
from utils import add_files_to_library from utils import add_files_to_library
from utils import get_id3_tags from utils import get_id3_tags
from utils import get_album_art from utils import get_album_art
@ -12,15 +12,18 @@ import configparser
class MusicTable(QTableView): class MusicTable(QTableView):
playPauseSignal = pyqtSignal() playPauseSignal = pyqtSignal()
enterKey = pyqtSignal() enterKey = pyqtSignal()
def __init__(self, parent=None, qapp=None): def __init__(self, parent=None):
QTableView.__init__(self, parent) # QTableView.__init__(self, parent)
self.headers = ['title', 'artist', 'album', 'genre', 'codec', 'year', 'path'] super().__init__(parent)
self.model = QStandardItemModel(self) # Necessary for actions related to cell values
self.setModel(self.model) # Same as above
self.config = configparser.ConfigParser()
self.config.read('config.ini')
self.headers = ['title', 'artist', 'album', 'genre', 'codec', 'year', 'path'] # gui names of headers
self.columns = str(self.config['table']['columns']).split(',') # db names of headers
self.songChanged = None self.songChanged = None
self.selected_song_filepath = None self.selected_song_filepath = None
self.current_song_filepath = None self.current_song_filepath = None
self.qapp = None
self.config = configparser.ConfigParser()
self.config.read('config.ini')
# self.tableView.resizeColumnsToContents() # self.tableView.resizeColumnsToContents()
self.clicked.connect(self.set_selected_song_filepath) self.clicked.connect(self.set_selected_song_filepath)
# doubleClicked is a built in event for QTableView - we listen for this event and run set_current_song_filepath # doubleClicked is a built in event for QTableView - we listen for this event and run set_current_song_filepath
@ -28,24 +31,44 @@ class MusicTable(QTableView):
self.enterKey.connect(self.set_current_song_filepath) self.enterKey.connect(self.set_current_song_filepath)
self.fetch_library() self.fetch_library()
self.setup_keyboard_shortcuts() self.setup_keyboard_shortcuts()
self.model.dataChanged.connect(self.on_cell_data_changed) # editing cells
def setup_keyboard_shortcuts(self): def setup_keyboard_shortcuts(self):
"""Setup shortcuts here""" """Setup shortcuts here"""
shortcut = QShortcut(QKeySequence("Ctrl+Shift+R"), self) shortcut = QShortcut(QKeySequence("Ctrl+Shift+R"), self)
shortcut.activated.connect(self.reorganize_selected_files) shortcut.activated.connect(self.reorganize_selected_files)
def on_cell_data_changed(self, topLeft: QModelIndex, bottomRight: QModelIndex):
"""Handles updating ID3 tags when data changes in a cell"""
print('on_cell_data_changed')
filepath_column_idx = self.model.columnCount() - 1 # always the last column
filepath_index = self.model.index(topLeft.row(), filepath_column_idx) # exact index of the edited cell
filepath = self.model.data(filepath_index) # filepath
# update the ID3 information
new_data = topLeft.data()
edited_column = self.columns[topLeft.column()]
print(filepath)
print(edited_column)
def reorganize_selected_files(self): def reorganize_selected_files(self):
"""Ctrl+Shift+R = Reorganize""" """Ctrl+Shift+R = Reorganize"""
filepaths = self.get_selected_songs_filepaths() filepaths = self.get_selected_songs_filepaths()
# right now this just prints the filepaths to the songs obv.
print(f'yay: {filepaths}') print(f'yay: {filepaths}')
# TODO # Confirmation screen (yes, no)
# Get user confirmation screen (default yes, so i can press enter and go) reply = QMessageBox.question(self, 'Confirmation', 'Are you sure you want to reorganize these files?', QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
if reply:
target_dir = str(self.config['directories']['reorganize_destination'])
print(target_dir)
# Get target directory # Get target directory
# Copy files to new dir # Copy files to new dir
# delete files from current dir # delete files from current dir
# add new files to library # add new files to library
def keyPressEvent(self, event): def keyPressEvent(self, event):
"""Press a key. Do a thing""" """Press a key. Do a thing"""
key = event.key() key = event.key()
@ -62,21 +85,27 @@ class MusicTable(QTableView):
if new_index.isValid(): if new_index.isValid():
self.setCurrentIndex(new_index) self.setCurrentIndex(new_index)
elif key in (Qt.Key_Return, Qt.Key_Enter): elif key in (Qt.Key_Return, Qt.Key_Enter):
if self.state() != QAbstractItemView.EditingState:
self.enterKey.emit() # Enter key detected self.enterKey.emit() # Enter key detected
else:
super().keyPressEvent(event)
else: # Default behavior else: # Default behavior
super().keyPressEvent(event) super().keyPressEvent(event)
def toggle_play_pause(self): def toggle_play_pause(self):
"""Toggles the currently playing song by emitting a signal""" """Toggles the currently playing song by emitting a signal"""
if not self.current_song_filepath: if not self.current_song_filepath:
self.set_current_song_filepath() self.set_current_song_filepath()
self.playPauseSignal.emit() self.playPauseSignal.emit()
def set_selected_song_filepath(self): def set_selected_song_filepath(self):
"""Sets the filepath of the currently selected song""" """Sets the filepath of the currently selected song"""
self.selected_song_filepath = self.currentIndex().siblingAtColumn(self.headers.index('path')).data() self.selected_song_filepath = self.currentIndex().siblingAtColumn(self.headers.index('path')).data()
print(f'Selected song: {self.selected_song_filepath}') print(f'Selected song: {self.selected_song_filepath}')
def set_current_song_filepath(self): def set_current_song_filepath(self):
"""Sets the filepath of the currently playing song""" """Sets the filepath of the currently playing song"""
# Setting the current song filepath automatically plays that song # Setting the current song filepath automatically plays that song
@ -84,6 +113,7 @@ class MusicTable(QTableView):
self.current_song_filepath = self.currentIndex().siblingAtColumn(self.headers.index('path')).data() self.current_song_filepath = self.currentIndex().siblingAtColumn(self.headers.index('path')).data()
print(f'Current song: {self.current_song_filepath}') print(f'Current song: {self.current_song_filepath}')
def get_selected_rows(self): def get_selected_rows(self):
"""Returns a list of indexes for every selected row""" """Returns a list of indexes for every selected row"""
selection_model = self.selectionModel() selection_model = self.selectionModel()
@ -93,6 +123,7 @@ class MusicTable(QTableView):
# rows.append(idx.row()) # rows.append(idx.row())
# return rows # return rows
def get_selected_songs_filepaths(self): def get_selected_songs_filepaths(self):
"""Returns a list of the filepaths for the currently selected songs""" """Returns a list of the filepaths for the currently selected songs"""
selected_rows = self.get_selected_rows() selected_rows = self.get_selected_rows()
@ -102,29 +133,34 @@ class MusicTable(QTableView):
filepaths.append(idx.data()) filepaths.append(idx.data())
return filepaths return filepaths
def get_selected_song_filepath(self): def get_selected_song_filepath(self):
"""Returns the selected songs filepath""" """Returns the selected songs filepath"""
return self.selected_song_filepath return self.selected_song_filepath
def get_selected_song_metadata(self): def get_selected_song_metadata(self):
"""Returns the selected song's ID3 tags""" """Returns the selected song's ID3 tags"""
return get_id3_tags(self.selected_song_filepath) return get_id3_tags(self.selected_song_filepath)
def get_current_song_filepath(self): def get_current_song_filepath(self):
"""Returns the currently playing song filepath""" """Returns the currently playing song filepath"""
return self.current_song_filepath return self.current_song_filepath
def get_current_song_metadata(self): def get_current_song_metadata(self):
"""Returns the currently playing song's ID3 tags""" """Returns the currently playing song's ID3 tags"""
return get_id3_tags(self.current_song_filepath) return get_id3_tags(self.current_song_filepath)
def get_current_song_album_art(self): def get_current_song_album_art(self):
"""Returns the APIC data (album art lol) for the currently playing song""" """Returns the APIC data (album art lol) for the currently playing song"""
return get_album_art(self.current_song_filepath) return get_album_art(self.current_song_filepath)
def fetch_library(self): def fetch_library(self):
"""Initialize the tableview model""" """Initialize the tableview model"""
self.model = QStandardItemModel()
self.model.setHorizontalHeaderLabels(self.headers) self.model.setHorizontalHeaderLabels(self.headers)
# Fetch library data # Fetch library data
with DBA.DBAccess() as db: with DBA.DBAccess() as db:
@ -133,11 +169,10 @@ class MusicTable(QTableView):
for row_data in data: for row_data in data:
items = [QStandardItem(str(item)) for item in row_data] items = [QStandardItem(str(item)) for item in row_data]
self.model.appendRow(items) self.model.appendRow(items)
# Set the model to the tableView (we are the tableview) # Update the viewport/model
self.setModel(self.model)
# self.update()
self.viewport().update() self.viewport().update()
def add_files(self, files): def add_files(self, files):
"""When song(s) added to the library, update the tableview model """When song(s) added to the library, update the tableview model
- Drag & Drop song(s) on tableView - Drag & Drop song(s) on tableView
@ -148,6 +183,7 @@ class MusicTable(QTableView):
if number_of_files_added: if number_of_files_added:
self.fetch_library() self.fetch_library()
def load_qapp(self, qapp): def load_qapp(self, qapp):
# why was this necessary again? :thinking: # why was this necessary again? :thinking:
self.qapp = qapp self.qapp = qapp

19
main.py
View File

@ -2,9 +2,9 @@ from ui import Ui_MainWindow
from PyQt5.QtWidgets import QMainWindow, QApplication, QGraphicsScene, \ from PyQt5.QtWidgets import QMainWindow, QApplication, QGraphicsScene, \
QHeaderView, QGraphicsPixmapItem QHeaderView, QGraphicsPixmapItem
import qdarktheme import qdarktheme
from PyQt5.QtCore import QUrl, QTimer, QEvent, Qt from PyQt5.QtCore import QUrl, QTimer, QEvent, Qt, QModelIndex
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe
from PyQt5.QtGui import QPixmap from PyQt5.QtGui import QPixmap, QStandardItemModel
from utils import scan_for_music from utils import scan_for_music
from utils import initialize_library_database from utils import initialize_library_database
from components import AudioVisualizer from components import AudioVisualizer
@ -27,17 +27,18 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.current_song_album_art = None self.current_song_album_art = None
self.album_art_scene = QGraphicsScene() self.album_art_scene = QGraphicsScene()
self.qapp = qapp self.qapp = qapp
print(f'ApplicationWindow self.qapp: {self.qapp}') # print(f'ApplicationWindow self.qapp: {self.qapp}')
self.tableView.load_qapp(self.qapp) self.tableView.load_qapp(self.qapp)
# print(f'tableView type: {type(self.tableView)}')
self.config = configparser.ConfigParser() self.config = configparser.ConfigParser()
self.config.read('config.ini') self.config.read('config.ini')
global stopped global stopped
stopped = False stopped = False
# Initialization # Initialization
self.player = QMediaPlayer() # Audio player self.player = QMediaPlayer() # Audio player object
self.probe = QAudioProbe() # Get audio data self.probe = QAudioProbe() # Gets audio data
self.timer = QTimer(self) # Audio timing things self.timer = QTimer(self) # Audio timing things
# self.model = QStandardItemModel() # Table library listing # self.music_table_model = QStandardItemModel(self) # Table library listing
self.audio_visualizer = AudioVisualizer(self.player) self.audio_visualizer = AudioVisualizer(self.player)
self.current_volume = 50 self.current_volume = 50
self.player.setVolume(self.current_volume) self.player.setVolume(self.current_volume)
@ -60,25 +61,21 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.PlotWidget.getAxis('left').setTicks([]) # Remove y-axis ticks self.PlotWidget.getAxis('left').setTicks([]) # Remove y-axis ticks
self.PlotWidget.getAxis('left').setLabel('') # Remove y-axis label self.PlotWidget.getAxis('left').setLabel('') # Remove y-axis label
# Connections # Connections
# ! FIXME moving the slider while playing is happening frequently causes playback to halt - the pyqtgraph is also affected by this # ! FIXME moving the slider while playing is happening frequently causes playback to halt - the pyqtgraph is also affected by this
self.playbackSlider.sliderMoved[int].connect(lambda: self.player.setPosition(self.playbackSlider.value())) # Move slidet to adjust playback time self.playbackSlider.sliderMoved[int].connect(lambda: self.player.setPosition(self.playbackSlider.value())) # Move slidet to adjust playback time
self.volumeSlider.sliderMoved[int].connect(lambda: self.volume_changed()) # Move slider to adjust volume self.volumeSlider.sliderMoved[int].connect(lambda: self.volume_changed()) # Move slider to adjust volume
self.playButton.clicked.connect(self.on_play_clicked) # Click to play/pause self.playButton.clicked.connect(self.on_play_clicked) # Click to play/pause
# self.pauseButton.clicked.connect(self.on_pause_clicked)
self.previousButton.clicked.connect(self.on_previous_clicked) # Click to previous song self.previousButton.clicked.connect(self.on_previous_clicked) # Click to previous song
self.nextButton.clicked.connect(self.on_next_clicked) # Click to next song self.nextButton.clicked.connect(self.on_next_clicked) # Click to next song
self.actionPreferences.triggered.connect(self.actionPreferencesClicked) # Open preferences menu self.actionPreferences.triggered.connect(self.actionPreferencesClicked) # Open preferences menu
self.actionScanLibraries.triggered.connect(self.scan_libraries) # Scan library self.actionScanLibraries.triggered.connect(self.scan_libraries) # Scan library
self.actionClearDatabase.triggered.connect(self.clear_database) # Clear database self.actionClearDatabase.triggered.connect(self.clear_database) # Clear database
## tableView ## tableView
# self.tableView.clicked.connect(self.set_clicked_cell_filepath)
self.tableView.doubleClicked.connect(self.play_audio_file) # Listens for the double click event, then plays the song self.tableView.doubleClicked.connect(self.play_audio_file) # Listens for the double click event, then plays the song
self.tableView.enterKey.connect(self.play_audio_file) # Listens for the enter key event, then plays the song self.tableView.enterKey.connect(self.play_audio_file) # Listens for the enter key event, then plays the song
self.tableView.playPauseSignal.connect(self.on_play_clicked) # Spacebar toggle play/pause signal self.tableView.playPauseSignal.connect(self.on_play_clicked) # Spacebar toggle play/pause signal
self.tableView.viewport().installEventFilter(self) # for drag & drop functionality self.tableView.viewport().installEventFilter(self) # for drag & drop functionality
# self.tableView.model.layoutChanged()
# set column widths # set column widths
table_view_column_widths = str(self.config['table']['column_widths']).split(',') table_view_column_widths = str(self.config['table']['column_widths']).split(',')
for i in range(self.tableView.model.columnCount()): for i in range(self.tableView.model.columnCount()):
@ -119,6 +116,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.config.write(configfile) self.config.write(configfile)
super().closeEvent(event) super().closeEvent(event)
def play_audio_file(self): def play_audio_file(self):
"""Start playback of tableView.current_song_filepath track & moves playback slider""" """Start playback of tableView.current_song_filepath track & moves playback slider"""
self.current_song_metadata = self.tableView.get_current_song_metadata() # get metadata self.current_song_metadata = self.tableView.get_current_song_metadata() # get metadata
@ -141,7 +139,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
# set album artwork # set album artwork
self.load_album_art(self.current_song_album_art) self.load_album_art(self.current_song_album_art)
def load_album_art(self, album_art_data): def load_album_art(self, album_art_data):
"""Sets the album art for the currently playing track""" """Sets the album art for the currently playing track"""
if self.current_song_album_art: if self.current_song_album_art:

View File

@ -12,5 +12,6 @@ reorganize_destination = /where/to/reorganize/to
extensions = mp3,wav,ogg,flac extensions = mp3,wav,ogg,flac
[table] [table]
# Music table column widths # Music table options
columns = title,artist,album,genre,codec,album_date,filepath
column_widths = 181,116,222,76,74,72,287 column_widths = 181,116,222,76,74,72,287

View File

@ -9,14 +9,12 @@ config.read("config.ini")
def add_files_to_library(files): def add_files_to_library(files):
"""Adds audio file(s) to the sqllite db """Adds audio file(s) to the sqllite db
files = list() of fully qualified paths to audio file(s)
files | list() of fully qualified paths to audio file(s)
Returns true if any files were added Returns true if any files were added
""" """
if not files: if not files:
return False return False
print(f"utils | adding files to library: {files}") print(f"utils/add_files_to_library: {files}")
extensions = config.get("settings", "extensions").split(",") extensions = config.get("settings", "extensions").split(",")
insert_data = [] # To store data for batch insert insert_data = [] # To store data for batch insert
for filepath in files: for filepath in files:

View File

@ -1,18 +1,27 @@
from mutagen.easyid3 import EasyID3 from mutagen.easyid3 import EasyID3
from mutagen import File from mutagen import File
def get_id3_tags(file): def get_id3_tags(file):
"""Get the ID3 tags for an audio file """Get the ID3 tags for an audio file
# Parameters # Parameters
`file` | str | Fully qualified path to file `file` | str | Fully qualified path to file
# Returns # Returns
dict of all id3 tags dict of all id3 tags
if all tags are empty, at minimum fill in the 'title'
""" """
try: try:
audio = EasyID3(file) audio = EasyID3(file)
print(f'ID3 Tags: {audio}') # Check if all tags are empty
tags_are_empty = all(not values for values in audio.values())
if tags_are_empty:
audio['title'] = [file.split('/')[-1]]
return audio return audio
except Exception as e: except Exception as e:
print(f"Error: {e}") print(f"Error: {e}")
return {} return {}
# import sys
# my_file = sys.argv[1]
# data = get_id3_tags(my_file)
# print(data)