drag and drop retrigger fetch library fix with signal reconnection, library database update on id3 tag update
This commit is contained in:
parent
7c4ef7e3fa
commit
bb6725f3a3
@ -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
|
||||
|
||||
4
main.py
4
main.py
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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}]')
|
||||
24
utils/update_song_in_library.py
Normal file
24
utils/update_song_in_library.py
Normal 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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user