updates and crap ugh
This commit is contained in:
parent
622c522e46
commit
bc24d6378a
@ -16,6 +16,7 @@ class SQLiteMap:
|
|||||||
album_date: str | None = None
|
album_date: str | None = None
|
||||||
codec: str | None = None
|
codec: str | None = None
|
||||||
filepath: str | None = None
|
filepath: str | None = None
|
||||||
|
bitrate: str | None = None
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -66,10 +67,11 @@ class HeaderTags:
|
|||||||
album_artist="alb artist",
|
album_artist="alb artist",
|
||||||
track_number="track",
|
track_number="track",
|
||||||
genre="genre",
|
genre="genre",
|
||||||
|
album_date="year",
|
||||||
codec="codec",
|
codec="codec",
|
||||||
length_seconds="length",
|
length_seconds="length",
|
||||||
album_date="year",
|
|
||||||
filepath="path",
|
filepath="path",
|
||||||
|
bitrate="bitrate",
|
||||||
)
|
)
|
||||||
# self.id3 = SQLiteMap(
|
# self.id3 = SQLiteMap(
|
||||||
# title = "TIT2",
|
# title = "TIT2",
|
||||||
@ -90,10 +92,11 @@ class HeaderTags:
|
|||||||
"album_artist": "album_artist",
|
"album_artist": "album_artist",
|
||||||
"track_number": "track_number",
|
"track_number": "track_number",
|
||||||
"genre": "genre",
|
"genre": "genre",
|
||||||
|
"album_date": "album_date",
|
||||||
"codec": "codec",
|
"codec": "codec",
|
||||||
"length_seconds": "length_seconds",
|
"length_seconds": "length_seconds",
|
||||||
"album_date": "album_date",
|
|
||||||
"filepath": "filepath",
|
"filepath": "filepath",
|
||||||
|
"bitrate": "bitrate",
|
||||||
}
|
}
|
||||||
self.gui: dict = {
|
self.gui: dict = {
|
||||||
"title": "title",
|
"title": "title",
|
||||||
@ -102,10 +105,11 @@ class HeaderTags:
|
|||||||
"album_artist": "alb artist",
|
"album_artist": "alb artist",
|
||||||
"track_number": "track",
|
"track_number": "track",
|
||||||
"genre": "genre",
|
"genre": "genre",
|
||||||
|
"album_date": "year",
|
||||||
"codec": "codec",
|
"codec": "codec",
|
||||||
"length_seconds": "length",
|
"length_seconds": "length",
|
||||||
"album_date": "year",
|
|
||||||
"filepath": "path",
|
"filepath": "path",
|
||||||
|
"bitrate": "bitrate",
|
||||||
}
|
}
|
||||||
self.id3: dict = {
|
self.id3: dict = {
|
||||||
"title": "TIT2",
|
"title": "TIT2",
|
||||||
@ -114,10 +118,11 @@ class HeaderTags:
|
|||||||
"album": "TALB",
|
"album": "TALB",
|
||||||
"track_number": "TRCK",
|
"track_number": "TRCK",
|
||||||
"genre": "TCON",
|
"genre": "TCON",
|
||||||
|
"album_date": "TDRC",
|
||||||
"codec": None,
|
"codec": None,
|
||||||
"length_seconds": "TLEN",
|
"length_seconds": "TLEN",
|
||||||
"album_date": "TDRC",
|
|
||||||
"filepath": None,
|
"filepath": None,
|
||||||
|
"bitrate": "TBIT",
|
||||||
}
|
}
|
||||||
# id3 is the key
|
# id3 is the key
|
||||||
self.id3_keys: dict = {}
|
self.id3_keys: dict = {}
|
||||||
|
|||||||
@ -96,19 +96,20 @@ class MusicTable(QTableView):
|
|||||||
# need a QStandardItemModel to load data & do actions on cells
|
# need a QStandardItemModel to load data & do actions on cells
|
||||||
self.model2: QStandardItemModel = QStandardItemModel()
|
self.model2: QStandardItemModel = QStandardItemModel()
|
||||||
self.proxymodel: QSortFilterProxyModel = QSortFilterProxyModel()
|
self.proxymodel: QSortFilterProxyModel = QSortFilterProxyModel()
|
||||||
self.cache_models: dict[int | None, QStandardItemModel] = {}
|
self.data_cache = {}
|
||||||
|
self.playlist_scroll_positions: dict[int | None, int] = {}
|
||||||
self.search_string: str | None = None
|
self.search_string: str | None = None
|
||||||
self.headers = HeaderTags()
|
self.headers = HeaderTags()
|
||||||
# db names of headers
|
# db names of headers
|
||||||
self.database_columns: list[str] = str(
|
self.database_columns: list[str] = str(
|
||||||
self.config["table"]["columns"]).split(",")
|
self.config["table"]["columns"]).split(",")
|
||||||
self.vertical_scroll_position = 0
|
|
||||||
self.selected_song_filepath = ""
|
self.selected_song_filepath = ""
|
||||||
self.selected_song_qmodel_index: QModelIndex
|
self.selected_song_qmodel_index: QModelIndex
|
||||||
self.current_song_filepath = ""
|
self.current_song_filepath = ""
|
||||||
self.current_song_db_id = None
|
self.current_song_db_id = None
|
||||||
self.current_song_qmodel_index: QModelIndex
|
self.current_song_qmodel_index: QModelIndex
|
||||||
self.selected_playlist_id: int | None = None
|
self.selected_playlist_id: int | None = None
|
||||||
|
self.current_playlist_id: int | None = None
|
||||||
|
|
||||||
# proxy model for sorting i guess?
|
# proxy model for sorting i guess?
|
||||||
self.proxymodel.setSourceModel(self.model2)
|
self.proxymodel.setSourceModel(self.model2)
|
||||||
@ -118,8 +119,8 @@ class MusicTable(QTableView):
|
|||||||
|
|
||||||
# Properties
|
# Properties
|
||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
self.setHorizontalScrollBarPolicy(
|
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||||
Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
# self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerItem)
|
||||||
self.setEditTriggers(QAbstractItemView.EditTrigger.EditKeyPressed)
|
self.setEditTriggers(QAbstractItemView.EditTrigger.EditKeyPressed)
|
||||||
self.setAlternatingRowColors(True)
|
self.setAlternatingRowColors(True)
|
||||||
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||||
@ -129,7 +130,6 @@ class MusicTable(QTableView):
|
|||||||
assert self.horizontal_header is not None # i hate look at linting errors
|
assert self.horizontal_header is not None # i hate look at linting errors
|
||||||
self.horizontal_header.setStretchLastSection(False)
|
self.horizontal_header.setStretchLastSection(False)
|
||||||
self.horizontal_header.setSectionResizeMode(QHeaderView.Interactive)
|
self.horizontal_header.setSectionResizeMode(QHeaderView.Interactive)
|
||||||
self.horizontal_header.sortIndicatorChanged.connect(self.on_sort)
|
|
||||||
# dumb vertical estupido
|
# dumb vertical estupido
|
||||||
self.vertical_header: QHeaderView = self.verticalHeader()
|
self.vertical_header: QHeaderView = self.verticalHeader()
|
||||||
assert self.vertical_header is not None
|
assert self.vertical_header is not None
|
||||||
@ -141,7 +141,6 @@ class MusicTable(QTableView):
|
|||||||
self.doubleClicked.connect(self.play_selected_audio_file)
|
self.doubleClicked.connect(self.play_selected_audio_file)
|
||||||
self.enterKey.connect(self.play_selected_audio_file)
|
self.enterKey.connect(self.play_selected_audio_file)
|
||||||
self.model2.dataChanged.connect(self.on_cell_data_changed) # editing cells
|
self.model2.dataChanged.connect(self.on_cell_data_changed) # editing cells
|
||||||
# self.model2.layoutChanged.connect(self.restore_scroll_position)
|
|
||||||
self.horizontal_header.sectionResized.connect(self.on_header_resized)
|
self.horizontal_header.sectionResized.connect(self.on_header_resized)
|
||||||
# Final actions
|
# Final actions
|
||||||
# self.load_music_table()
|
# self.load_music_table()
|
||||||
@ -331,6 +330,10 @@ class MusicTable(QTableView):
|
|||||||
else: # Default behavior
|
else: # Default behavior
|
||||||
super().keyPressEvent(e)
|
super().keyPressEvent(e)
|
||||||
|
|
||||||
|
def showEvent(self, a0):
|
||||||
|
# Restore scroll position
|
||||||
|
super().showEvent(a0)
|
||||||
|
|
||||||
# ____________________
|
# ____________________
|
||||||
# | |
|
# | |
|
||||||
# | |
|
# | |
|
||||||
@ -347,16 +350,7 @@ class MusicTable(QTableView):
|
|||||||
|
|
||||||
def on_sort(self):
|
def on_sort(self):
|
||||||
debug("on_sort")
|
debug("on_sort")
|
||||||
search_col_num = self.headers.user_fields.index("filepath")
|
self.find_current_and_selected_bits()
|
||||||
selected_qmodel_index = self.find_qmodel_index_by_value(
|
|
||||||
self.proxymodel, search_col_num, self.selected_song_filepath
|
|
||||||
)
|
|
||||||
current_qmodel_index = self.find_qmodel_index_by_value(
|
|
||||||
self.proxymodel, search_col_num, self.current_song_filepath
|
|
||||||
)
|
|
||||||
# Update the 2 QModelIndexes that we track
|
|
||||||
self.set_selected_song_qmodel_index(selected_qmodel_index)
|
|
||||||
self.set_current_song_qmodel_index(current_qmodel_index)
|
|
||||||
self.jump_to_selected_song()
|
self.jump_to_selected_song()
|
||||||
self.sortSignal.emit()
|
self.sortSignal.emit()
|
||||||
|
|
||||||
@ -376,22 +370,22 @@ class MusicTable(QTableView):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# https://stackoverflow.com/questions/46775438/how-to-limit-qheaderview-size-when-resizing-sections
|
# https://stackoverflow.com/questions/46775438/how-to-limit-qheaderview-size-when-resizing-sections
|
||||||
# col_count = self.model2.columnCount()
|
col_count = self.model2.columnCount()
|
||||||
# qtableview_width = self.size().width()
|
qtableview_width = self.size().width()
|
||||||
# sum_of_cols = self.horizontal_header.length()
|
sum_of_cols = self.horizontal_header.length()
|
||||||
# debug(f'qtable_width: {qtableview_width}')
|
debug(f'qtable_width: {qtableview_width}')
|
||||||
# debug(f'sum of cols: {sum_of_cols}')
|
debug(f'sum of cols: {sum_of_cols}')
|
||||||
|
|
||||||
# if sum_of_cols != qtableview_width: # check for discrepancy
|
if sum_of_cols != qtableview_width: # check for discrepancy
|
||||||
# if logicalIndex < col_count: # if not the last header
|
if logicalIndex < col_count: # if not the last header
|
||||||
# next_header_size = self.horizontal_header.sectionSize(logicalIndex + 1)
|
next_header_size = self.horizontal_header.sectionSize(logicalIndex + 1)
|
||||||
# if next_header_size > (sum_of_cols - qtableview_width): # if it should shrink
|
if next_header_size > (sum_of_cols - qtableview_width): # if it should shrink
|
||||||
# self.horizontal_header.resizeSection(
|
self.horizontal_header.resizeSection(
|
||||||
# logicalIndex + 1,
|
logicalIndex + 1,
|
||||||
# next_header_size - (sum_of_cols - qtableview_width),
|
next_header_size - (sum_of_cols - qtableview_width),
|
||||||
# ) # shrink it
|
) # shrink it
|
||||||
# else:
|
else:
|
||||||
# self.horizontal_header.resizeSection(logicalIndex, oldSize) # block the resize
|
self.horizontal_header.resizeSection(logicalIndex, oldSize) # block the resize
|
||||||
|
|
||||||
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"""
|
||||||
@ -414,8 +408,7 @@ class MusicTable(QTableView):
|
|||||||
)
|
)
|
||||||
if response:
|
if response:
|
||||||
# Update the library with new metadata
|
# Update the library with new metadata
|
||||||
_ = update_song_in_database(
|
_ = update_song_in_database(song_id, edited_column_name, user_input_data)
|
||||||
song_id, edited_column_name, user_input_data)
|
|
||||||
else:
|
else:
|
||||||
error('ERROR: response failed')
|
error('ERROR: response failed')
|
||||||
return
|
return
|
||||||
@ -508,14 +501,11 @@ class MusicTable(QTableView):
|
|||||||
)
|
)
|
||||||
reply = question_dialog.execute()
|
reply = question_dialog.execute()
|
||||||
if reply:
|
if reply:
|
||||||
worker = Worker(batch_delete_filepaths_from_playlist,
|
worker = Worker(batch_delete_filepaths_from_playlist, selected_filepaths, self.selected_playlist_id)
|
||||||
selected_filepaths, self.selected_playlist_id)
|
worker.signals.signal_progress.connect(self.qapp.handle_progress) # type: ignore
|
||||||
worker.signals.signal_progress.connect(
|
worker.signals.signal_finished.connect(self.delete_selected_row_indices)
|
||||||
self.qapp.handle_progress)
|
|
||||||
worker.signals.signal_finished.connect(
|
|
||||||
self.delete_selected_row_indices)
|
|
||||||
if self.qapp:
|
if self.qapp:
|
||||||
threadpool = self.qapp.threadpool
|
threadpool = self.qapp.threadpool # type: ignore
|
||||||
threadpool.start(worker)
|
threadpool.start(worker)
|
||||||
else:
|
else:
|
||||||
question_dialog = QuestionBoxDetails(
|
question_dialog = QuestionBoxDetails(
|
||||||
@ -525,14 +515,11 @@ class MusicTable(QTableView):
|
|||||||
)
|
)
|
||||||
reply = question_dialog.execute()
|
reply = question_dialog.execute()
|
||||||
if reply:
|
if reply:
|
||||||
worker = Worker(
|
worker = Worker(batch_delete_filepaths_from_database, selected_filepaths)
|
||||||
batch_delete_filepaths_from_database, selected_filepaths)
|
worker.signals.signal_progress.connect(self.qapp.handle_progress) # type: ignore
|
||||||
worker.signals.signal_progress.connect(
|
worker.signals.signal_finished.connect(self.delete_selected_row_indices)
|
||||||
self.qapp.handle_progress)
|
|
||||||
worker.signals.signal_finished.connect(
|
|
||||||
self.delete_selected_row_indices)
|
|
||||||
if self.qapp:
|
if self.qapp:
|
||||||
threadpool = self.qapp.threadpool
|
threadpool = self.qapp.threadpool # type: ignore
|
||||||
threadpool.start(worker)
|
threadpool.start(worker)
|
||||||
|
|
||||||
def delete_selected_row_indices(self):
|
def delete_selected_row_indices(self):
|
||||||
@ -555,9 +542,7 @@ class MusicTable(QTableView):
|
|||||||
"""Opens a form with metadata from the selected audio files"""
|
"""Opens a form with metadata from the selected audio files"""
|
||||||
files = self.get_selected_songs_filepaths()
|
files = self.get_selected_songs_filepaths()
|
||||||
song_ids = self.get_selected_songs_db_ids()
|
song_ids = self.get_selected_songs_db_ids()
|
||||||
window = MetadataWindow(
|
window = MetadataWindow(self.refreshMusicTableSignal, self.headers, files, song_ids)
|
||||||
self.refreshMusicTableSignal, self.headers, files, song_ids
|
|
||||||
)
|
|
||||||
window.refreshMusicTableSignal.connect(self.load_music_table)
|
window.refreshMusicTableSignal.connect(self.load_music_table)
|
||||||
window.exec_() # Display the preferences window modally
|
window.exec_() # Display the preferences window modally
|
||||||
|
|
||||||
@ -565,8 +550,7 @@ class MusicTable(QTableView):
|
|||||||
"""Moves screen to the selected song, then selects the row"""
|
"""Moves screen to the selected song, then selects the row"""
|
||||||
debug("jump_to_selected_song")
|
debug("jump_to_selected_song")
|
||||||
# get the proxy model index
|
# get the proxy model index
|
||||||
proxy_index = self.proxymodel.mapFromSource(
|
proxy_index = self.proxymodel.mapFromSource(self.selected_song_qmodel_index)
|
||||||
self.selected_song_qmodel_index)
|
|
||||||
self.scrollTo(proxy_index)
|
self.scrollTo(proxy_index)
|
||||||
self.selectRow(proxy_index.row())
|
self.selectRow(proxy_index.row())
|
||||||
|
|
||||||
@ -576,8 +560,7 @@ class MusicTable(QTableView):
|
|||||||
# get the proxy model index
|
# get the proxy model index
|
||||||
debug(self.current_song_filepath)
|
debug(self.current_song_filepath)
|
||||||
debug(self.current_song_qmodel_index)
|
debug(self.current_song_qmodel_index)
|
||||||
proxy_index = self.proxymodel.mapFromSource(
|
proxy_index = self.proxymodel.mapFromSource(self.current_song_qmodel_index)
|
||||||
self.current_song_qmodel_index)
|
|
||||||
self.scrollTo(proxy_index)
|
self.scrollTo(proxy_index)
|
||||||
self.selectRow(proxy_index.row())
|
self.selectRow(proxy_index.row())
|
||||||
|
|
||||||
@ -648,7 +631,7 @@ class MusicTable(QTableView):
|
|||||||
worker = Worker(self.reorganize_files, filepaths)
|
worker = Worker(self.reorganize_files, filepaths)
|
||||||
worker.signals.signal_progress.connect(self.handle_progress)
|
worker.signals.signal_progress.connect(self.handle_progress)
|
||||||
worker.signals.signal_finished.connect(self.load_music_table)
|
worker.signals.signal_finished.connect(self.load_music_table)
|
||||||
self.qapp.threadpool.start(worker)
|
self.qapp.threadpool.start(worker) # type: ignore
|
||||||
|
|
||||||
def reorganize_files(self, filepaths, progress_callback=None):
|
def reorganize_files(self, filepaths, progress_callback=None):
|
||||||
"""
|
"""
|
||||||
@ -710,7 +693,7 @@ class MusicTable(QTableView):
|
|||||||
"""
|
"""
|
||||||
self.disconnect_data_changed()
|
self.disconnect_data_changed()
|
||||||
self.disconnect_layout_changed()
|
self.disconnect_layout_changed()
|
||||||
self.vertical_scroll_position = self.verticalScrollBar().value()
|
self.save_scroll_position(self.current_playlist_id)
|
||||||
self.model2.clear()
|
self.model2.clear()
|
||||||
self.model2.setHorizontalHeaderLabels(
|
self.model2.setHorizontalHeaderLabels(
|
||||||
self.headers.get_user_gui_headers())
|
self.headers.get_user_gui_headers())
|
||||||
@ -729,84 +712,60 @@ class MusicTable(QTableView):
|
|||||||
else:
|
else:
|
||||||
self.selected_playlist_id = None
|
self.selected_playlist_id = None
|
||||||
|
|
||||||
# Check cache for already loaded QTableView QStandardItemModel
|
# try:
|
||||||
try:
|
# # Check cache for already loaded QTableView QStandardItemModel
|
||||||
new_model = self.cache_models[self.selected_playlist_id]
|
# data = self.data_cache[self.selected_playlist_id]
|
||||||
self.model2 = new_model
|
# self.populate_model(data)
|
||||||
debug('Cached model loaded')
|
# debug('loaded table from cache')
|
||||||
except KeyError:
|
# except KeyError:
|
||||||
# Query for a playlist
|
# # Query for a playlist
|
||||||
if is_playlist:
|
if is_playlist:
|
||||||
debug('load music table a playlist')
|
debug('load music table a playlist')
|
||||||
try:
|
try:
|
||||||
with DBA.DBAccess() as db:
|
with DBA.DBAccess() as db:
|
||||||
query = f"SELECT id, {
|
query = f"SELECT id, {
|
||||||
fields} FROM song JOIN song_playlist sp ON id = sp.song_id WHERE sp.playlist_id = ?"
|
fields} FROM song JOIN song_playlist sp ON id = sp.song_id WHERE sp.playlist_id = ?"
|
||||||
# fulltext search
|
# fulltext search
|
||||||
if self.search_string:
|
if self.search_string:
|
||||||
# params = 3 * [self.search_string]
|
# params = 3 * [self.search_string]
|
||||||
params = ["%" + self.search_string + "%"] * 3
|
params = ["%" + self.search_string + "%"] * 3
|
||||||
if query.find("WHERE") == -1:
|
if query.find("WHERE") == -1:
|
||||||
query = f"{query} WHERE {search_clause};"
|
query = f"{query} WHERE {search_clause};"
|
||||||
else:
|
|
||||||
query = f"{query} AND {search_clause};"
|
|
||||||
data = db.query(
|
|
||||||
query, (self.selected_playlist_id, params))
|
|
||||||
else:
|
else:
|
||||||
data = db.query(query, (self.selected_playlist_id,))
|
query = f"{query} AND {search_clause};"
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error(f"load_music_table() | Unhandled exception 1: {e}")
|
|
||||||
return
|
|
||||||
# Query for the entire library
|
|
||||||
else:
|
|
||||||
debug('load music table a Whole Table')
|
|
||||||
try:
|
|
||||||
with DBA.DBAccess() as db:
|
|
||||||
query = f"SELECT id, {fields} FROM song"
|
|
||||||
# fulltext search
|
|
||||||
if self.search_string:
|
|
||||||
params = ["%" + self.search_string + "%"] * 3
|
|
||||||
if query.find("WHERE") == -1:
|
|
||||||
query = f"{query} WHERE {search_clause};"
|
|
||||||
else:
|
|
||||||
query = f"{query} AND {search_clause};"
|
|
||||||
data = db.query(
|
data = db.query(
|
||||||
query,
|
query, (self.selected_playlist_id, params))
|
||||||
(params),
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
error(f"load_music_table() | Unhandled exception 2: {e}")
|
|
||||||
return
|
|
||||||
# Populate the model
|
|
||||||
# TODO: total time of playlist
|
|
||||||
# but how do i want to do this if user doesn't choose to see length field?
|
|
||||||
# spawn new thread and calculate myself?
|
|
||||||
total_time: int = 0 # total time of all songs in seconds
|
|
||||||
for row_data in data:
|
|
||||||
# print(row_data)
|
|
||||||
# if "length" in fields:
|
|
||||||
id, *rest_of_data = row_data
|
|
||||||
# handle different datatypes
|
|
||||||
items = []
|
|
||||||
for item in rest_of_data:
|
|
||||||
if isinstance(item, int):
|
|
||||||
std_item = QStandardItem()
|
|
||||||
std_item.setData(item, Qt.ItemDataRole.DisplayRole)
|
|
||||||
std_item.setData(item, Qt.ItemDataRole.EditRole)
|
|
||||||
else:
|
else:
|
||||||
std_item = QStandardItem(str(item) if item else "")
|
data = db.query(query, (self.selected_playlist_id,))
|
||||||
items.append(std_item)
|
|
||||||
# store database id in the row object using setData
|
|
||||||
# - useful for fast db fetching and other model operations
|
|
||||||
for item in items:
|
|
||||||
item.setData(id, Qt.ItemDataRole.UserRole)
|
|
||||||
self.model2.appendRow(items)
|
|
||||||
# Store the current loaded model in cache
|
|
||||||
self.cache_models[self.selected_playlist_id] = self.model2
|
|
||||||
debug('Current model stored')
|
|
||||||
|
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error(f"load_music_table() | Unhandled exception 1: {e}")
|
||||||
|
return
|
||||||
|
# Query for the entire library
|
||||||
|
else:
|
||||||
|
debug('load music table a Whole Table')
|
||||||
|
try:
|
||||||
|
with DBA.DBAccess() as db:
|
||||||
|
query = f"SELECT id, {fields} FROM song"
|
||||||
|
# fulltext search
|
||||||
|
if self.search_string:
|
||||||
|
params = ["%" + self.search_string + "%"] * 3
|
||||||
|
if query.find("WHERE") == -1:
|
||||||
|
query = f"{query} WHERE {search_clause};"
|
||||||
|
else:
|
||||||
|
query = f"{query} AND {search_clause};"
|
||||||
|
data = db.query(
|
||||||
|
query,
|
||||||
|
(params),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
error(f"load_music_table() | Unhandled exception 2: {e}")
|
||||||
|
return
|
||||||
|
# cache the data
|
||||||
|
# self.data_cache[self.selected_playlist_id] = data
|
||||||
|
self.populate_model(data)
|
||||||
|
self.current_playlist_id = self.selected_playlist_id
|
||||||
|
self.model2.layoutChanged.emit() # emits a signal that the view should be updated
|
||||||
# reloading the model destroys and makes new indexes
|
# reloading the model destroys and makes new indexes
|
||||||
# so we look for the new index of the current song on load
|
# so we look for the new index of the current song on load
|
||||||
# current_song_filepath = self.get_current_song_filepath()
|
# current_song_filepath = self.get_current_song_filepath()
|
||||||
@ -817,16 +776,52 @@ class MusicTable(QTableView):
|
|||||||
# )
|
# )
|
||||||
# if real_index.data() == current_song_filepath:
|
# if real_index.data() == current_song_filepath:
|
||||||
# self.current_song_qmodel_index = real_index
|
# self.current_song_qmodel_index = real_index
|
||||||
self.model2.layoutChanged.emit() # emits a signal that the view should be updated
|
|
||||||
|
|
||||||
db_name: str = self.config.get("settings", "db").split("/").pop()
|
db_name: str = self.config.get("settings", "db").split("/").pop()
|
||||||
db_filename = self.config.get("settings", "db")
|
db_filename = self.config.get("settings", "db")
|
||||||
# FIXME: total time implementation
|
self.playlistStatsSignal.emit(f"Songs: {self.model2.rowCount()} | {db_name} | {db_filename}")
|
||||||
total_time = 0
|
|
||||||
self.playlistStatsSignal.emit(f"Songs: {self.model2.rowCount()} | Total time: {total_time} | {db_name} | {db_filename}")
|
|
||||||
self.loadMusicTableSignal.emit()
|
self.loadMusicTableSignal.emit()
|
||||||
self.connect_data_changed()
|
self.connect_data_changed()
|
||||||
self.connect_layout_changed()
|
self.connect_layout_changed()
|
||||||
|
# set the current song and such
|
||||||
|
self.find_current_and_selected_bits()
|
||||||
|
# self.restore_scroll_position()
|
||||||
|
|
||||||
|
def find_current_and_selected_bits(self):
|
||||||
|
"""
|
||||||
|
When data changes in the model view, its nice to re-grab the current song.
|
||||||
|
might as well get the selected song too i guess? though nothing should be selected when reloading the table data
|
||||||
|
"""
|
||||||
|
search_col_num = self.headers.user_fields.index("filepath")
|
||||||
|
selected_qmodel_index = self.find_qmodel_index_by_value(self.proxymodel, search_col_num, self.selected_song_filepath)
|
||||||
|
current_qmodel_index = self.find_qmodel_index_by_value(self.proxymodel, search_col_num, self.current_song_filepath)
|
||||||
|
# Update the 2 QModelIndexes that we track
|
||||||
|
self.set_selected_song_qmodel_index(selected_qmodel_index)
|
||||||
|
self.set_current_song_qmodel_index(current_qmodel_index)
|
||||||
|
|
||||||
|
def populate_model(self, data):
|
||||||
|
"""
|
||||||
|
populate the model2 with data... or whatever
|
||||||
|
"""
|
||||||
|
for row_data in data:
|
||||||
|
id, *rest_of_data = row_data
|
||||||
|
# handle different datatypes
|
||||||
|
items = []
|
||||||
|
for item in rest_of_data:
|
||||||
|
if isinstance(item, int):
|
||||||
|
std_item = QStandardItem()
|
||||||
|
std_item.setData(item, Qt.ItemDataRole.DisplayRole)
|
||||||
|
std_item.setData(item, Qt.ItemDataRole.EditRole)
|
||||||
|
else:
|
||||||
|
std_item = QStandardItem(str(item) if item else "")
|
||||||
|
items.append(std_item)
|
||||||
|
# store database id in the row object using setData
|
||||||
|
# - useful for fast db fetching and other model operations
|
||||||
|
for item in items:
|
||||||
|
item.setData(id, Qt.ItemDataRole.UserRole)
|
||||||
|
self.model2.appendRow(items)
|
||||||
|
self.proxymodel.setSourceModel(self.model2)
|
||||||
|
self.setModel(self.proxymodel)
|
||||||
|
|
||||||
def load_header_widths(self):
|
def load_header_widths(self):
|
||||||
"""
|
"""
|
||||||
@ -844,11 +839,9 @@ class MusicTable(QTableView):
|
|||||||
Sorts the data in QTableView (self) by multiple columns
|
Sorts the data in QTableView (self) by multiple columns
|
||||||
as defined in config.ini
|
as defined in config.ini
|
||||||
"""
|
"""
|
||||||
|
self.disconnect_data_changed() # not needed?
|
||||||
# Disconnect these signals to prevent unnecessary reloads
|
self.disconnect_layout_changed() # not needed?
|
||||||
# debug("sort_table_by_multiple_columns()")
|
self.horizontal_header.sortIndicatorChanged.disconnect()
|
||||||
# self.disconnect_data_changed()
|
|
||||||
# self.disconnect_layout_changed()
|
|
||||||
sort_orders = []
|
sort_orders = []
|
||||||
config_sort_orders: list[int] = [
|
config_sort_orders: list[int] = [
|
||||||
int(x) for x in self.config["table"]["sort_orders"].split(",")
|
int(x) for x in self.config["table"]["sort_orders"].split(",")
|
||||||
@ -860,33 +853,35 @@ class MusicTable(QTableView):
|
|||||||
sort_orders.append(Qt.SortOrder.AscendingOrder)
|
sort_orders.append(Qt.SortOrder.AscendingOrder)
|
||||||
elif order == 2:
|
elif order == 2:
|
||||||
sort_orders.append(Qt.SortOrder.DescendingOrder)
|
sort_orders.append(Qt.SortOrder.DescendingOrder)
|
||||||
|
|
||||||
# QTableView sorts need to happen in reverse order
|
# QTableView sorts need to happen in reverse order
|
||||||
# The primary sort column is the last column sorted.
|
# The primary sort column is the last column sorted.
|
||||||
for i in reversed(range(len(sort_orders))):
|
for i in reversed(range(len(sort_orders))):
|
||||||
if sort_orders[i] is not None:
|
if sort_orders[i] is not None:
|
||||||
# debug(f"sorting column {i} by {sort_orders[i]}")
|
debug(f"sorting column {i} by {sort_orders[i]}")
|
||||||
self.sortByColumn(i, sort_orders[i])
|
self.sortByColumn(i, sort_orders[i])
|
||||||
# WARNING:
|
# WARNING:
|
||||||
# sortByColumn calls a SELECT statement,
|
# sortByColumn calls a SELECT statement,
|
||||||
# and will do this for as many sorts that are needed
|
# and will do this for as many sorts that are needed
|
||||||
# maybe not a huge deal for a small music application...?
|
# maybe not a huge deal for a small music application...?
|
||||||
# `len(config_sort_orders)` number of SELECTs
|
# `len(config_sort_orders)` number of SELECTs
|
||||||
|
self.on_sort()
|
||||||
|
self.connect_data_changed() # not needed?
|
||||||
|
self.connect_layout_changed() # not needed?
|
||||||
|
self.model2.layoutChanged.emit()
|
||||||
|
|
||||||
# self.connect_data_changed()
|
def save_scroll_position(self, playlist_id: int | None):
|
||||||
# self.connect_layout_changed()
|
"""Save the current scroll position of the table"""
|
||||||
|
scroll_position = self.verticalScrollBar().value()
|
||||||
|
self.playlist_scroll_positions[playlist_id] = scroll_position
|
||||||
|
debug(f'save scroll position: {playlist_id}:{scroll_position}')
|
||||||
|
|
||||||
# self.model2.layoutChanged.emit()
|
def restore_scroll_position(self):
|
||||||
# TODO: Rewrite this function to use self.load_music_table() with dynamic SQL queries
|
"""Set the scroll position to the given value"""
|
||||||
# in order to sort the data more effectively & have more control over UI refreshes.
|
if self.current_playlist_id in self.playlist_scroll_positions:
|
||||||
|
scroll_position = self.playlist_scroll_positions[self.current_playlist_id]
|
||||||
def restore_scroll_position(self) -> None:
|
# self.restore_scroll_position(scroll_position)
|
||||||
"""Restores the scroll position"""
|
self.verticalScrollBar().setValue(scroll_position)
|
||||||
debug("restore_scroll_position (inactive)")
|
debug(f'restore scroll position: {scroll_position}')
|
||||||
# QTimer.singleShot(
|
|
||||||
# 100,
|
|
||||||
# lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position),
|
|
||||||
# )
|
|
||||||
|
|
||||||
def get_audio_files_recursively(self, directories: list[str], progress_callback=None) -> list[str]:
|
def get_audio_files_recursively(self, directories: list[str], progress_callback=None) -> list[str]:
|
||||||
"""Scans a directories for files"""
|
"""Scans a directories for files"""
|
||||||
@ -918,8 +913,7 @@ class MusicTable(QTableView):
|
|||||||
selected_rows = self.get_selected_rows()
|
selected_rows = self.get_selected_rows()
|
||||||
filepaths = []
|
filepaths = []
|
||||||
for row in selected_rows:
|
for row in selected_rows:
|
||||||
idx = self.proxymodel.index(
|
idx = self.proxymodel.index(row, self.headers.user_fields.index("filepath"))
|
||||||
row, self.headers.user_fields.index("filepath"))
|
|
||||||
filepaths.append(idx.data())
|
filepaths.append(idx.data())
|
||||||
return filepaths
|
return filepaths
|
||||||
|
|
||||||
@ -1040,7 +1034,8 @@ class MusicTable(QTableView):
|
|||||||
def connect_layout_changed(self):
|
def connect_layout_changed(self):
|
||||||
"""Connects the layoutChanged signal from QTableView.model"""
|
"""Connects the layoutChanged signal from QTableView.model"""
|
||||||
try:
|
try:
|
||||||
_ = self.model2.layoutChanged.connect(self.restore_scroll_position)
|
pass
|
||||||
|
# _ = self.model2.layoutChanged.connect(self.restore_scroll_position)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
3
main.py
3
main.py
@ -4,6 +4,7 @@ import logging
|
|||||||
from PyQt5 import QtCore
|
from PyQt5 import QtCore
|
||||||
import typing
|
import typing
|
||||||
import DBA
|
import DBA
|
||||||
|
import qdarktheme
|
||||||
from subprocess import run
|
from subprocess import run
|
||||||
# from pyqtgraph import mkBrush
|
# from pyqtgraph import mkBrush
|
||||||
from mutagen.id3 import ID3
|
from mutagen.id3 import ID3
|
||||||
@ -736,7 +737,7 @@ if __name__ == "__main__":
|
|||||||
clipboard = app.clipboard()
|
clipboard = app.clipboard()
|
||||||
# Dark theme >:3
|
# Dark theme >:3
|
||||||
# qdarktheme.setup_theme()
|
# qdarktheme.setup_theme()
|
||||||
# qdarktheme.setup_theme("auto") # this is supposed to work but doesnt
|
qdarktheme.setup_theme("auto") # this is supposed to work but doesnt
|
||||||
# Show the UI
|
# Show the UI
|
||||||
ui = ApplicationWindow(clipboard)
|
ui = ApplicationWindow(clipboard)
|
||||||
# window size
|
# window size
|
||||||
|
|||||||
@ -4,7 +4,8 @@ appdirs
|
|||||||
pyqt5
|
pyqt5
|
||||||
pydub
|
pydub
|
||||||
audioop-lts
|
audioop-lts
|
||||||
pyqtdarktheme-fork; python_version < '3.11'
|
pyqtdarktheme==2.1.0
|
||||||
pyqtdarktheme; python_version > '3.12'
|
|
||||||
pyqtgraph
|
pyqtgraph
|
||||||
scipy
|
scipy
|
||||||
|
# pyqtdarktheme-fork; python_version < '3.11'
|
||||||
|
# pyqtdarktheme; python_version > '3.12'
|
||||||
|
|||||||
@ -11,7 +11,7 @@ window_size=1152,894
|
|||||||
|
|
||||||
[table]
|
[table]
|
||||||
# Music table user options
|
# Music table user options
|
||||||
columns = title,artist,album,track_number,genre,codec,album_date,filepath
|
columns = title,artist,album,track_number,genre,album_date,codec,length_seconds,filepath
|
||||||
column_widths = 181,116,222,76,74,72,287,150
|
column_widths = 181,116,222,76,74,72,287,150,100
|
||||||
# 0 = no sort, 1 = ascending, 2 = descending
|
# 0 = no sort, 1 = ascending, 2 = descending
|
||||||
sort_orders = 0,1,1,1,0,0,0,0
|
sort_orders = 0,1,1,1,0,0,0,0,0
|
||||||
|
|||||||
@ -4,8 +4,6 @@ from configparser import ConfigParser
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from appdirs import user_config_dir
|
from appdirs import user_config_dir
|
||||||
|
|
||||||
import glob
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
|
|
||||||
|
|
||||||
def scan_for_music(progress_callback=None):
|
def scan_for_music(progress_callback=None):
|
||||||
@ -19,7 +17,7 @@ def scan_for_music(progress_callback=None):
|
|||||||
progress_callback.emit('Scanning libraries...')
|
progress_callback.emit('Scanning libraries...')
|
||||||
config = ConfigParser()
|
config = ConfigParser()
|
||||||
config.read(Path(user_config_dir(appname="musicpom", appauthor="billypom")) / "config.ini")
|
config.read(Path(user_config_dir(appname="musicpom", appauthor="billypom")) / "config.ini")
|
||||||
libraries = [path.strip() for path in config.get("settings", "library_path").split(',')]
|
libraries = [path.strip() for path in config.get("settings", "library").split(',')]
|
||||||
extensions = config.get("settings", "extensions").split(",")
|
extensions = config.get("settings", "extensions").split(",")
|
||||||
|
|
||||||
# Use each library as root dir, walk the dir and find files
|
# Use each library as root dir, walk the dir and find files
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user