drag and drop retrigger fetch library fix with signal reconnection, library database update on id3 tag update

This commit is contained in:
billypom on debian 2024-03-28 20:29:47 -04:00
parent 7c4ef7e3fa
commit bb6725f3a3
7 changed files with 87 additions and 60 deletions

View File

@ -4,6 +4,7 @@ from PyQt5.QtGui import QStandardItem, QStandardItemModel, QKeySequence
from PyQt5.QtWidgets import QTableView, QShortcut, QMessageBox, QAbstractItemView
from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal, QTimer
from utils import add_files_to_library
from utils import update_song_in_library
from utils import get_id3_tags
from utils import get_album_art
from utils import set_id3_tag
@ -50,21 +51,23 @@ class MusicTable(QTableView):
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
id_index = self.model.index(topLeft.row(), 0) # ID is column 0, always
library_id = self.model.data(id_index, Qt.UserRole)
filepath_column_idx = self.model.columnCount() - 1 # filepath is always the last column
filepath_index = self.model.index(topLeft.row(), filepath_column_idx) # exact index of the edited cell in 2d space
filepath = self.model.data(filepath_index) # filepath
# update the ID3 information
user_input_data = topLeft.data()
edited_column_name = self.database_columns[topLeft.column()]
response = set_id3_tag(filepath, edited_column_name, user_input_data)
if response:
update_song_in_library(filepath, edited_column_name, user_input_data)
# Update the library with new metadata
update_song_in_library(library_id, edited_column_name, user_input_data)
def reorganize_selected_files(self):
"""Ctrl+Shift+R = Reorganize"""
filepaths = self.get_selected_songs_filepaths()
print(f'yay: {filepaths}')
# 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:
@ -82,16 +85,17 @@ class MusicTable(QTableView):
os.makedirs(os.path.dirname(new_path), exist_ok=True)
# Move the file to the new directory
shutil.move(filepath, new_path)
# Update the db?
# Update the db
with DBA.DBAccess() as db:
db.query('UPDATE library SET filepath = ? WHERE filepath = ?', (new_path, filepath))
print(f'Moved: {filepath} -> {new_path}')
except Exception as e:
print(f'Error moving file: {filepath} | {e}')
# Draw the rest of the owl
self.model.dataChanged.disconnect(self.on_cell_data_changed)
self.fetch_library()
self.model.dataChanged.connect(self.on_cell_data_changed)
QMessageBox.information(self, 'Reorganization complete', 'Files successfully reorganized')
# add new files to library
def keyPressEvent(self, event):
@ -125,11 +129,49 @@ class MusicTable(QTableView):
self.playPauseSignal.emit()
def fetch_library(self):
"""Initialize the tableview model"""
self.vertical_scroll_position = self.verticalScrollBar().value() # Get my scroll position before clearing
# temporarily disconnect the datachanged signal to avoid EVERY SONG getting triggered
self.model.clear()
self.model.setHorizontalHeaderLabels(self.table_headers)
# Fetch library data
with DBA.DBAccess() as db:
data = db.query('SELECT id, title, artist, album, genre, codec, album_date, filepath FROM library;', ())
# Populate the model
for row_data in data:
id, *rest_of_data = row_data
items = [QStandardItem(str(item)) for item in rest_of_data]
self.model.appendRow(items)
# store id using setData - useful for later faster db fetching
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
def restore_scroll_position(self):
"""Restores the scroll position"""
print(f'Returning to {self.vertical_scroll_position}')
QTimer.singleShot(100, lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position))
def add_files(self, files):
"""When song(s) added to the library, update the tableview model
- Drag & Drop song(s) on tableView
- File > Open > List of song(s)
"""
number_of_files_added = add_files_to_library(files)
if number_of_files_added:
self.model.dataChanged.disconnect(self.on_cell_data_changed)
self.fetch_library()
self.model.dataChanged.connect(self.on_cell_data_changed)
def set_selected_song_filepath(self):
"""Sets the filepath of the currently selected song"""
self.selected_song_filepath = self.currentIndex().siblingAtColumn(self.table_headers.index('path')).data()
print(f'Selected song: {self.selected_song_filepath}')
# print(get_id3_tags(self.selected_song_filepath))
def set_current_song_filepath(self):
@ -137,17 +179,12 @@ class MusicTable(QTableView):
# Setting the current song filepath automatically plays that song
# self.tableView listens to this function and plays the audio file located at self.current_song_filepath
self.current_song_filepath = self.currentIndex().siblingAtColumn(self.table_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()
return [index.row() for index in selection_model.selectedRows()]
# rows = []
# for idx in self.selectionModel().siblingAtColumn():
# rows.append(idx.row())
# return rows
def get_selected_songs_filepaths(self):
@ -185,39 +222,6 @@ class MusicTable(QTableView):
return get_album_art(self.current_song_filepath)
def fetch_library(self):
"""Initialize the tableview model"""
self.vertical_scroll_position = self.verticalScrollBar().value() # Get my scroll position before clearing
self.model.clear()
self.model.setHorizontalHeaderLabels(self.table_headers)
# Fetch library data
with DBA.DBAccess() as db:
data = db.query('SELECT title, artist, album, genre, codec, album_date, filepath FROM library;', ())
# Populate the model
for row_data in data:
items = [QStandardItem(str(item)) for item in row_data]
self.model.appendRow(items)
# Update the viewport/model
# self.viewport().update()
self.model.layoutChanged.emit()
def restore_scroll_position(self):
"""Restores the scroll position"""
print(f'Returning to {self.vertical_scroll_position}')
QTimer.singleShot(100, lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position))
def add_files(self, files):
"""When song(s) added to the library, update the tableview model
- Drag & Drop song(s) on tableView
- File > Open > List of song(s)
"""
print(f'tableView - adding files: {files}')
number_of_files_added = add_files_to_library(files)
if number_of_files_added:
self.fetch_library()
def load_qapp(self, qapp):
# why was this necessary again? :thinking:
self.qapp = qapp

View File

@ -6,7 +6,7 @@ from PyQt5.QtCore import QUrl, QTimer, QEvent, Qt, QModelIndex
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe
from PyQt5.QtGui import QPixmap, QStandardItemModel
from utils import scan_for_music
from utils import initialize_library_database
from utils import delete_and_create_library_database
from components import AudioVisualizer
from components import PreferencesWindow
from pyqtgraph import mkBrush
@ -241,7 +241,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.tableView.fetch_library()
def clear_database(self):
initialize_library_database()
delete_and_create_library_database()
self.tableView.fetch_library()
def process_probe(self, buff):

View File

@ -2,7 +2,8 @@ from .safe_get import safe_get
from .get_album_art import get_album_art
from .get_id3_tags import get_id3_tags
from .set_id3_tag import set_id3_tag
from .initialize_library_database import initialize_library_database
from .delete_and_create_library_database import delete_and_create_library_database
from .update_song_in_library import update_song_in_library
from .scan_for_music import scan_for_music
from .fft_analyser import FFTAnalyser
from .add_files_to_library import add_files_to_library

View File

@ -10,10 +10,10 @@ 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)
Returns true if any files were added
Returns a list of dictionaries of metadata
"""
if not files:
return False
return []
print(f"utils/add_files_to_library: {files}")
extensions = config.get("settings", "extensions").split(",")
insert_data = [] # To store data for batch insert
@ -21,9 +21,9 @@ def add_files_to_library(files):
if any(filepath.lower().endswith(ext) for ext in extensions):
filename = filepath.split("/")[-1]
audio = get_id3_tags(filepath)
if "title" not in audio: # This should never run
# get_id3_tags sets the title when no tags exist
return False
# Skip if no title is found (but should never happen
if "title" not in audio:
continue
# Append data tuple to insert_data list
insert_data.append(
(
@ -39,7 +39,6 @@ def add_files_to_library(files):
safe_get(audio, "bitrate", [])[0] if "birate" in audio else None,
)
)
# Check if batch size is reached
if len(insert_data) >= 1000:
with DBA.DBAccess() as db:
@ -48,7 +47,6 @@ def add_files_to_library(files):
insert_data,
)
insert_data = [] # Reset the insert_data list
# Insert any remaining data
if insert_data:
with DBA.DBAccess() as db:

View File

@ -1,7 +1,7 @@
import DBA
def initialize_library_database():
with open('utils/create_library.sql', 'r') as file:
def delete_and_create_library_database():
with open('utils/delete_and_create_library.sql', 'r') as file:
lines = file.read()
for statement in lines.split(';'):
print(f'executing [{statement}]')

View File

@ -0,0 +1,24 @@
import DBA
from components.ErrorDialog import ErrorDialog
def update_song_in_library(library_id: int, edited_column_name: str, user_input_data: str):
"""Updates a field in the library database based on an ID
Args:
library_id: the database ID of the song
edited_column_name: the name of the database column
user_input_data: the data to input
Returns:
True or False"""
try:
with DBA.DBAccess() as db:
# yeah yeah this is bad... the column names are defined in the program by me so im ok with it because it works
db.execute(f'UPDATE library SET {edited_column_name} = ? WHERE id = ?', (user_input_data, library_id))
except Exception as e:
dialog = ErrorDialog(f'Unable to update [{edited_column_name}] to [{user_input_data}]. ID: {library_id} | {e}')
dialog.exec_()
return False
return True