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.QtWidgets import QTableView, QShortcut, QMessageBox, QAbstractItemView
from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal, QTimer from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal, QTimer
from utils import add_files_to_library from utils import add_files_to_library
from utils import update_song_in_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
from utils import set_id3_tag from utils import set_id3_tag
@ -50,21 +51,23 @@ class MusicTable(QTableView):
def on_cell_data_changed(self, topLeft: QModelIndex, bottomRight: QModelIndex): def on_cell_data_changed(self, topLeft: QModelIndex, bottomRight: QModelIndex):
"""Handles updating ID3 tags when data changes in a cell""" """Handles updating ID3 tags when data changes in a cell"""
print('on_cell_data_changed') print('on_cell_data_changed')
filepath_column_idx = self.model.columnCount() - 1 # always the last column id_index = self.model.index(topLeft.row(), 0) # ID is column 0, always
filepath_index = self.model.index(topLeft.row(), filepath_column_idx) # exact index of the edited cell 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 filepath = self.model.data(filepath_index) # filepath
# update the ID3 information # update the ID3 information
user_input_data = topLeft.data() user_input_data = topLeft.data()
edited_column_name = self.database_columns[topLeft.column()] edited_column_name = self.database_columns[topLeft.column()]
response = set_id3_tag(filepath, edited_column_name, user_input_data) response = set_id3_tag(filepath, edited_column_name, user_input_data)
if response: 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): 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()
print(f'yay: {filepaths}')
# Confirmation screen (yes, no) # Confirmation screen (yes, no)
reply = QMessageBox.question(self, 'Confirmation', 'Are you sure you want to reorganize these files?', QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) reply = QMessageBox.question(self, 'Confirmation', 'Are you sure you want to reorganize these files?', QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
if reply: if reply:
@ -82,16 +85,17 @@ class MusicTable(QTableView):
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
shutil.move(filepath, new_path) shutil.move(filepath, new_path)
# Update the db? # Update the db
with DBA.DBAccess() as db: with DBA.DBAccess() as db:
db.query('UPDATE library SET filepath = ? WHERE filepath = ?', (new_path, filepath)) db.query('UPDATE library SET filepath = ? WHERE filepath = ?', (new_path, filepath))
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}') 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.fetch_library()
self.model.dataChanged.connect(self.on_cell_data_changed)
QMessageBox.information(self, 'Reorganization complete', 'Files successfully reorganized') QMessageBox.information(self, 'Reorganization complete', 'Files successfully reorganized')
# add new files to library
def keyPressEvent(self, event): def keyPressEvent(self, event):
@ -125,11 +129,49 @@ class MusicTable(QTableView):
self.playPauseSignal.emit() 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): 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.table_headers.index('path')).data() 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): def set_current_song_filepath(self):
@ -137,17 +179,12 @@ class MusicTable(QTableView):
# Setting the current song filepath automatically plays that song # 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.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() 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): 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()
return [index.row() for index in selection_model.selectedRows()] 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): def get_selected_songs_filepaths(self):
@ -185,39 +222,6 @@ class MusicTable(QTableView):
return get_album_art(self.current_song_filepath) 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): def load_qapp(self, qapp):
# why was this necessary again? :thinking: # why was this necessary again? :thinking:
self.qapp = qapp 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.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe
from PyQt5.QtGui import QPixmap, QStandardItemModel 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 delete_and_create_library_database
from components import AudioVisualizer from components import AudioVisualizer
from components import PreferencesWindow from components import PreferencesWindow
from pyqtgraph import mkBrush from pyqtgraph import mkBrush
@ -241,7 +241,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.tableView.fetch_library() self.tableView.fetch_library()
def clear_database(self): def clear_database(self):
initialize_library_database() delete_and_create_library_database()
self.tableView.fetch_library() self.tableView.fetch_library()
def process_probe(self, buff): 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_album_art import get_album_art
from .get_id3_tags import get_id3_tags from .get_id3_tags import get_id3_tags
from .set_id3_tag import set_id3_tag 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 .scan_for_music import scan_for_music
from .fft_analyser import FFTAnalyser from .fft_analyser import FFTAnalyser
from .add_files_to_library import add_files_to_library 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): 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 a list of dictionaries of metadata
""" """
if not files: if not files:
return False return []
print(f"utils/add_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
@ -21,9 +21,9 @@ def add_files_to_library(files):
if any(filepath.lower().endswith(ext) for ext in extensions): if any(filepath.lower().endswith(ext) for ext in extensions):
filename = filepath.split("/")[-1] filename = filepath.split("/")[-1]
audio = get_id3_tags(filepath) audio = get_id3_tags(filepath)
if "title" not in audio: # This should never run # Skip if no title is found (but should never happen
# get_id3_tags sets the title when no tags exist if "title" not in audio:
return False continue
# Append data tuple to insert_data list # Append data tuple to insert_data list
insert_data.append( insert_data.append(
( (
@ -39,7 +39,6 @@ def add_files_to_library(files):
safe_get(audio, "bitrate", [])[0] if "birate" in audio else None, safe_get(audio, "bitrate", [])[0] if "birate" in audio else None,
) )
) )
# Check if batch size is reached # Check if batch size is reached
if len(insert_data) >= 1000: if len(insert_data) >= 1000:
with DBA.DBAccess() as db: with DBA.DBAccess() as db:
@ -48,7 +47,6 @@ def add_files_to_library(files):
insert_data, insert_data,
) )
insert_data = [] # Reset the insert_data list insert_data = [] # Reset the insert_data list
# Insert any remaining data # Insert any remaining data
if insert_data: if insert_data:
with DBA.DBAccess() as db: with DBA.DBAccess() as db:

View File

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