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.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
|
||||||
|
|||||||
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.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):
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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, ())
|
||||||
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