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
from PyQt5.QtGui import QStandardItem, QStandardItemModel, QKeySequence
from PyQt5.QtWidgets import QTableView, QShortcut
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QStandardItem, QStandardItemModel, QKeySequence
from PyQt5.QtWidgets import QTableView, QShortcut, QMessageBox, QAbstractItemView
from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal
from utils import add_files_to_library
from utils import get_id3_tags
from utils import get_album_art
@ -12,15 +12,18 @@ import configparser
class MusicTable(QTableView):
playPauseSignal = pyqtSignal()
enterKey = pyqtSignal()
def __init__(self, parent=None, qapp=None):
QTableView.__init__(self, parent)
self.headers = ['title', 'artist', 'album', 'genre', 'codec', 'year', 'path']
def __init__(self, parent=None):
# QTableView.__init__(self, parent)
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.selected_song_filepath = None
self.current_song_filepath = None
self.qapp = None
self.config = configparser.ConfigParser()
self.config.read('config.ini')
# self.tableView.resizeColumnsToContents()
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
@ -28,23 +31,43 @@ class MusicTable(QTableView):
self.enterKey.connect(self.set_current_song_filepath)
self.fetch_library()
self.setup_keyboard_shortcuts()
self.model.dataChanged.connect(self.on_cell_data_changed) # editing cells
def setup_keyboard_shortcuts(self):
"""Setup shortcuts here"""
shortcut = QShortcut(QKeySequence("Ctrl+Shift+R"), self)
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):
"""Ctrl+Shift+R = Reorganize"""
filepaths = self.get_selected_songs_filepaths()
# right now this just prints the filepaths to the songs obv.
print(f'yay: {filepaths}')
# TODO
# Get user confirmation screen (default yes, so i can press enter and go)
# Get target directory
# Copy files to new dir
# delete files from current dir
# add new files to library
# Confirmation screen (yes, no)
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
# Copy files to new dir
# delete files from current dir
# add new files to library
def keyPressEvent(self, event):
"""Press a key. Do a thing"""
@ -62,21 +85,27 @@ class MusicTable(QTableView):
if new_index.isValid():
self.setCurrentIndex(new_index)
elif key in (Qt.Key_Return, Qt.Key_Enter):
self.enterKey.emit() # Enter key detected
if self.state() != QAbstractItemView.EditingState:
self.enterKey.emit() # Enter key detected
else:
super().keyPressEvent(event)
else: # Default behavior
super().keyPressEvent(event)
def toggle_play_pause(self):
"""Toggles the currently playing song by emitting a signal"""
if not self.current_song_filepath:
self.set_current_song_filepath()
self.playPauseSignal.emit()
def set_selected_song_filepath(self):
"""Sets the filepath of the currently selected song"""
self.selected_song_filepath = self.currentIndex().siblingAtColumn(self.headers.index('path')).data()
print(f'Selected song: {self.selected_song_filepath}')
def set_current_song_filepath(self):
"""Sets the filepath of the currently playing 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()
print(f'Current song: {self.current_song_filepath}')
def get_selected_rows(self):
"""Returns a list of indexes for every selected row"""
selection_model = self.selectionModel()
@ -93,6 +123,7 @@ class MusicTable(QTableView):
# rows.append(idx.row())
# return rows
def get_selected_songs_filepaths(self):
"""Returns a list of the filepaths for the currently selected songs"""
selected_rows = self.get_selected_rows()
@ -102,29 +133,34 @@ class MusicTable(QTableView):
filepaths.append(idx.data())
return filepaths
def get_selected_song_filepath(self):
"""Returns the selected songs filepath"""
return self.selected_song_filepath
def get_selected_song_metadata(self):
"""Returns the selected song's ID3 tags"""
return get_id3_tags(self.selected_song_filepath)
def get_current_song_filepath(self):
"""Returns the currently playing song filepath"""
return self.current_song_filepath
def get_current_song_metadata(self):
"""Returns the currently playing song's ID3 tags"""
return get_id3_tags(self.current_song_filepath)
def get_current_song_album_art(self):
"""Returns the APIC data (album art lol) for the currently playing song"""
return get_album_art(self.current_song_filepath)
def fetch_library(self):
"""Initialize the tableview model"""
self.model = QStandardItemModel()
self.model.setHorizontalHeaderLabels(self.headers)
# Fetch library data
with DBA.DBAccess() as db:
@ -133,11 +169,10 @@ class MusicTable(QTableView):
for row_data in data:
items = [QStandardItem(str(item)) for item in row_data]
self.model.appendRow(items)
# Set the model to the tableView (we are the tableview)
self.setModel(self.model)
# self.update()
# Update the viewport/model
self.viewport().update()
def add_files(self, files):
"""When song(s) added to the library, update the tableview model
- Drag & Drop song(s) on tableView
@ -148,6 +183,7 @@ class MusicTable(QTableView):
if number_of_files_added:
self.fetch_library()
def load_qapp(self, qapp):
# why was this necessary again? :thinking:
self.qapp = qapp

43
main.py
View File

@ -2,9 +2,9 @@ from ui import Ui_MainWindow
from PyQt5.QtWidgets import QMainWindow, QApplication, QGraphicsScene, \
QHeaderView, QGraphicsPixmapItem
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.QtGui import QPixmap
from PyQt5.QtGui import QPixmap, QStandardItemModel
from utils import scan_for_music
from utils import initialize_library_database
from components import AudioVisualizer
@ -27,17 +27,18 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.current_song_album_art = None
self.album_art_scene = QGraphicsScene()
self.qapp = qapp
print(f'ApplicationWindow self.qapp: {self.qapp}')
# print(f'ApplicationWindow self.qapp: {self.qapp}')
self.tableView.load_qapp(self.qapp)
# print(f'tableView type: {type(self.tableView)}')
self.config = configparser.ConfigParser()
self.config.read('config.ini')
global stopped
stopped = False
# Initialization
self.player = QMediaPlayer() # Audio player
self.probe = QAudioProbe() # Get audio data
self.player = QMediaPlayer() # Audio player object
self.probe = QAudioProbe() # Gets audio data
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.current_volume = 50
self.player.setVolume(self.current_volume)
@ -49,7 +50,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
# Slider Timer (realtime playback feedback horizontal bar)
self.timer.start(150) # 150ms update interval solved problem with drag seeking halting playback
self.timer.timeout.connect(self.move_slider)
# Graphics plot
self.PlotWidget.setXRange(0,100,padding=0) # x axis range
self.PlotWidget.setYRange(0,0.3,padding=0) # y axis range
@ -59,26 +60,22 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
# Remove y-axis labels and decorations
self.PlotWidget.getAxis('left').setTicks([]) # Remove y-axis ticks
self.PlotWidget.getAxis('left').setLabel('') # Remove y-axis label
# Connections
# ! 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.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.pauseButton.clicked.connect(self.on_pause_clicked)
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.actionPreferences.triggered.connect(self.actionPreferencesClicked) # Open preferences menu
self.actionScanLibraries.triggered.connect(self.scan_libraries) # Scan library
self.actionClearDatabase.triggered.connect(self.clear_database) # Clear database
## 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.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.viewport().installEventFilter(self) # for drag & drop functionality
# self.tableView.model.layoutChanged()
# set column widths
table_view_column_widths = str(self.config['table']['column_widths']).split(',')
for i in range(self.tableView.model.columnCount()):
@ -104,7 +101,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
event.accept()
return True
return super().eventFilter(source, event)
def closeEvent(self, event):
"""Save settings when closing the application"""
# MusicTable/tableView column widths
@ -113,12 +110,13 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
list_of_column_widths.append(str(self.tableView.columnWidth(i)))
column_widths_as_string = ','.join(list_of_column_widths)
self.config['table']['column_widths'] = column_widths_as_string
# Save the config
with open('config.ini', 'w') as configfile:
self.config.write(configfile)
super().closeEvent(event)
def play_audio_file(self):
"""Start playback of tableView.current_song_filepath track & moves playback slider"""
self.current_song_metadata = self.tableView.get_current_song_metadata() # get metadata
@ -128,7 +126,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.player.setMedia(content) # what content to play
self.player.play() # play
self.move_slider() # mover
# assign metadata
# FIXME when i change tinytag to something else
artist = self.current_song_metadata["artist"][0] if "artist" in self.current_song_metadata else None
@ -140,7 +138,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.titleLabel.setText(title)
# set album artwork
self.load_album_art(self.current_song_album_art)
def load_album_art(self, album_art_data):
"""Sets the album art for the currently playing track"""
@ -161,21 +158,21 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.albumGraphicsView.setScene(self.album_art_scene)
# Adjust the album art scaling
self.adjustPixmapScaling(pixmapItem)
def adjustPixmapScaling(self, pixmapItem):
"""Adjust the scaling of the pixmap item to fit the QGraphicsView, maintaining aspect ratio"""
viewWidth = self.albumGraphicsView.width()
viewHeight = self.albumGraphicsView.height()
pixmapSize = pixmapItem.pixmap().size()
# Calculate scaling factor while maintaining aspect ratio
scaleX = viewWidth / pixmapSize.width()
scaleY = viewHeight / pixmapSize.height()
scaleFactor = min(scaleX, scaleY)
# Apply scaling to the pixmap item
pixmapItem.setScale(scaleFactor)
def update_audio_visualization(self):
"""Handles upading points on the pyqtgraph visual"""
self.clear_audio_visualization()
@ -183,10 +180,10 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.x = [i for i in range(len(self.y))]
self.PlotWidget.plot(self.x, self.y, fillLevel=0, fillBrush=mkBrush('b'))
self.PlotWidget.show()
def clear_audio_visualization(self):
self.PlotWidget.clear()
def move_slider(self):
"""Handles moving the playback slider"""
if stopped:

View File

@ -12,5 +12,6 @@ reorganize_destination = /where/to/reorganize/to
extensions = mp3,wav,ogg,flac
[table]
# Music table column widths
column_widths = 181,116,222,76,74,72,287
# Music table options
columns = title,artist,album,genre,codec,album_date,filepath
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):
"""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
"""
if not files:
return False
print(f"utils | adding files to library: {files}")
print(f"utils/add_files_to_library: {files}")
extensions = config.get("settings", "extensions").split(",")
insert_data = [] # To store data for batch insert
for filepath in files:

View File

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