This commit is contained in:
billypom on debian 2025-08-03 22:49:04 -04:00
parent 511b118790
commit 69ce4bc763
6 changed files with 131 additions and 113 deletions

View File

@ -7,16 +7,16 @@ from typing import Optional
@dataclass
class SQLiteMap:
title: Optional[str] = None
artist: Optional[str] = None
album: Optional[str] = None
album_artist: Optional[str] = None
track_number: Optional[str] = None
genre: Optional[str] = None
length_seconds: Optional[str] = None
album_date: Optional[str] = None
codec: Optional[str] = None
filepath: Optional[str] = None
title: str | None = None
artist: str | None = None
album: str | None = None
album_artist: str | None = None
track_number: str | None = None
genre: str | None = None
length_seconds: str | None = None
album_date: str | None = None
codec: str | None = None
filepath: str | None = None
"""

View File

@ -118,8 +118,7 @@ class MusicTable(QTableView):
)
self.config = ConfigParser()
self.config.read(cfg_file)
print("music table config:")
print(self.config)
debug(f"music table config: {self.config}")
# Threads
self.threadpool = QThreadPool
@ -138,7 +137,8 @@ class MusicTable(QTableView):
# Properties
self.setAcceptDrops(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setEditTriggers(QAbstractItemView.EditTrigger.EditKeyPressed)
self.setAlternatingRowColors(True)
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
@ -146,7 +146,7 @@ class MusicTable(QTableView):
# header
self.horizontal_header: QHeaderView = self.horizontalHeader()
assert self.horizontal_header is not None # i hate look at linting errors
self.horizontal_header.setStretchLastSection(True)
self.horizontal_header.setStretchLastSection(False)
self.horizontal_header.setSectionResizeMode(QHeaderView.Interactive)
self.horizontal_header.sortIndicatorChanged.connect(self.on_sort)
# dumb vertical estupido
@ -159,7 +159,8 @@ class MusicTable(QTableView):
self.deleteKey.connect(self.delete_songs)
self.doubleClicked.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)
# Final actions
@ -202,11 +203,13 @@ class MusicTable(QTableView):
"""Right-click context menu"""
menu = QMenu(self)
add_to_playlist_action = QAction("Add to playlist", self)
add_to_playlist_action.triggered.connect(self.add_selected_files_to_playlist)
add_to_playlist_action.triggered.connect(
self.add_selected_files_to_playlist)
menu.addAction(add_to_playlist_action)
# edit metadata
edit_metadata_action = QAction("Edit metadata", self)
edit_metadata_action.triggered.connect(self.edit_selected_files_metadata)
edit_metadata_action.triggered.connect(
self.edit_selected_files_metadata)
menu.addAction(edit_metadata_action)
# edit lyrics
edit_lyrics_action = QAction("Lyrics (View/Edit)", self)
@ -214,10 +217,12 @@ class MusicTable(QTableView):
menu.addAction(edit_lyrics_action)
# jump to current song in table
jump_to_current_song_action = QAction("Jump to current song", self)
jump_to_current_song_action.triggered.connect(self.jump_to_current_song)
jump_to_current_song_action.triggered.connect(
self.jump_to_current_song)
menu.addAction(jump_to_current_song_action)
# open in file explorer
open_containing_folder_action = QAction("Open in system file manager", self)
open_containing_folder_action = QAction(
"Open in system file manager", self)
open_containing_folder_action.triggered.connect(self.open_directory)
menu.addAction(open_containing_folder_action)
# view id3 tags (debug)
@ -380,59 +385,58 @@ class MusicTable(QTableView):
self.set_selected_song_qmodel_index()
self.viewport().update() # type: ignore
def on_header_resized(self, logicalIndex, oldSize, newSize):
def on_header_resized(self, logicalIndex: int, oldSize: int, newSize: int):
"""Handles keeping headers inside the viewport"""
# FIXME: how resize good
pass
# https://stackoverflow.com/questions/46775438/how-to-limit-qheaderview-size-when-resizing-sections
col_count = self.model2.columnCount()
qtableview_width = self.size().width()
sum_of_cols = self.horizontal_header.length()
# col_count = self.model2.columnCount()
# qtableview_width = self.size().width()
# sum_of_cols = self.horizontal_header.length()
# debug(f'qtable_width: {qtableview_width}')
# debug(f'sum of cols: {sum_of_cols}')
# check for discrepancy
if sum_of_cols != qtableview_width:
# if not the last header
if logicalIndex < col_count:
next_header_size = self.horizontal_header.sectionSize(logicalIndex + 1)
# If it should shrink
if next_header_size > (sum_of_cols - qtableview_width):
# shrink it
self.horizontal_header.resizeSection(
logicalIndex + 1,
next_header_size - (sum_of_cols - qtableview_width),
)
else:
# block the resize
self.horizontal_header.resizeSection(logicalIndex, oldSize)
# if sum_of_cols != qtableview_width: # check for discrepancy
# if logicalIndex < col_count: # if not the last header
# next_header_size = self.horizontal_header.sectionSize(logicalIndex + 1)
# if next_header_size > (sum_of_cols - qtableview_width): # if it should shrink
# self.horizontal_header.resizeSection(
# logicalIndex + 1,
# next_header_size - (sum_of_cols - qtableview_width),
# ) # shrink it
# else:
# self.horizontal_header.resizeSection(logicalIndex, oldSize) # block the resize
def on_cell_data_changed(self, topLeft: QModelIndex, bottomRight: QModelIndex):
"""Handles updating ID3 tags when data changes in a cell"""
# FIXME: broken
if isinstance(self.model2, QStandardItemModel):
# if isinstance(self.model2, QStandardItemModel):
debug("on_cell_data_changed")
# get the ID of the row that was edited
id_index = self.model2.index(topLeft.row(), 0)
# get the db song_id from the row
song_id = self.model2.data(id_index, Qt.ItemDataRole.UserRole)
song_id: int = self.model2.data(id_index, Qt.ItemDataRole.UserRole)
user_index = self.headers.user_fields.index("filepath")
filepath = self.currentIndex().siblingAtColumn(user_index).data()
# update the ID3 information
user_input_data = topLeft.data()
edited_column_name = self.headers.user_fields[topLeft.column()]
debug(f"on_cell_data_changed | edited column name: {edited_column_name}")
user_input_data: str = topLeft.data()
edited_column_name: str = self.headers.user_fields[topLeft.column()]
debug(
f"on_cell_data_changed | edited column name: {edited_column_name}")
response = set_tag(filepath, edited_column_name, user_input_data)
if response:
# Update the library with new metadata
update_song_in_database(song_id, edited_column_name, user_input_data)
_ = update_song_in_database(
song_id, edited_column_name, user_input_data)
return
def handle_progress(self, data):
def handle_progress(self, data: object):
"""Emits data to main"""
self.handleProgressSignal.emit(data)
def on_get_audio_files_recursively_finished(self, result):
def on_get_audio_files_recursively_finished(self, result: list[str]):
"""file search completion handler"""
if result:
self.add_files_to_library(result)
@ -456,7 +460,8 @@ class MusicTable(QTableView):
except IndexError:
pass
except Exception as e:
debug(f"on_add_files_to_database_finished() | Something went wrong: {e}")
debug(
f"on_add_files_to_database_finished() | Something went wrong: {e}")
# ____________________
# | |
@ -484,8 +489,8 @@ class MusicTable(QTableView):
- File > Open > List of song(s)
"""
worker = Worker(add_files_to_database, files)
worker.signals.signal_progress.connect(self.qapp.handle_progress)
worker.signals.signal_result.connect(self.on_add_files_to_database_finished)
_ = worker.signals.signal_progress.connect(self.qapp.handle_progress)
_ = worker.signals.signal_result.connect(self.on_add_files_to_database_finished)
worker.signals.signal_finished.connect(self.load_music_table)
if self.qapp:
threadpool = self.qapp.threadpool
@ -495,15 +500,15 @@ class MusicTable(QTableView):
def add_selected_files_to_playlist(self):
"""Opens a playlist choice menu and adds the currently selected files to the chosen playlist"""
playlist_choice_window = AddToPlaylistWindow(self.get_selected_songs_db_ids())
playlist_choice_window = AddToPlaylistWindow(
self.get_selected_songs_db_ids())
playlist_choice_window.exec_()
def delete_songs(self):
"""Asks to delete the currently selected songs from the db and music table (not the filesystem)"""
# FIXME: determine if we are in a playlist or not
# then delete songs from only the playlist
# or provide extra questionbox option
# NOTE: provide extra questionbox option?
# | Delete from playlist & lib | Delete from playlist only | Cancel |
# Currently, this just deletes from the playlist, or the main lib & any playlists
selected_filepaths = self.get_selected_songs_filepaths()
if self.selected_playlist_id:
question_dialog = QuestionBoxDetails(
@ -513,9 +518,12 @@ class MusicTable(QTableView):
)
reply = question_dialog.execute()
if reply:
worker = Worker(batch_delete_filepaths_from_playlist, selected_filepaths, self.selected_playlist_id)
worker.signals.signal_progress.connect(self.qapp.handle_progress)
worker.signals.signal_finished.connect(self.delete_selected_row_indices)
worker = Worker(batch_delete_filepaths_from_playlist,
selected_filepaths, self.selected_playlist_id)
worker.signals.signal_progress.connect(
self.qapp.handle_progress)
worker.signals.signal_finished.connect(
self.delete_selected_row_indices)
if self.qapp:
threadpool = self.qapp.threadpool
threadpool.start(worker)
@ -527,9 +535,12 @@ class MusicTable(QTableView):
)
reply = question_dialog.execute()
if reply:
worker = Worker(batch_delete_filepaths_from_database, selected_filepaths)
worker.signals.signal_progress.connect(self.qapp.handle_progress)
worker.signals.signal_finished.connect(self.delete_selected_row_indices)
worker = Worker(
batch_delete_filepaths_from_database, selected_filepaths)
worker.signals.signal_progress.connect(
self.qapp.handle_progress)
worker.signals.signal_finished.connect(
self.delete_selected_row_indices)
if self.qapp:
threadpool = self.qapp.threadpool
threadpool.start(worker)
@ -563,7 +574,8 @@ class MusicTable(QTableView):
"""Moves screen to the selected song, then selects the row"""
debug("jump_to_selected_song")
# get the proxy model index
proxy_index = self.proxymodel.mapFromSource(self.selected_song_qmodel_index)
proxy_index = self.proxymodel.mapFromSource(
self.selected_song_qmodel_index)
self.scrollTo(proxy_index)
self.selectRow(proxy_index.row())
@ -573,7 +585,8 @@ class MusicTable(QTableView):
# get the proxy model index
debug(self.current_song_filepath)
debug(self.current_song_qmodel_index)
proxy_index = self.proxymodel.mapFromSource(self.current_song_qmodel_index)
proxy_index = self.proxymodel.mapFromSource(
self.current_song_qmodel_index)
self.scrollTo(proxy_index)
self.selectRow(proxy_index.row())
@ -684,7 +697,8 @@ class MusicTable(QTableView):
)
# debug(f"reorganize_files() | Moved: {filepath} -> {new_path}")
except Exception as e:
error(f"reorganize_files() | Error moving file: {filepath} | {e}")
error(
f"reorganize_files() | Error moving file: {filepath} | {e}")
# Draw the rest of the owl
# QMessageBox.information(
# self, "Reorganization complete", "Files successfully reorganized"
@ -709,7 +723,8 @@ class MusicTable(QTableView):
self.disconnect_layout_changed()
self.vertical_scroll_position = self.verticalScrollBar().value() # type: ignore
self.model2.clear()
self.model2.setHorizontalHeaderLabels(self.headers.get_user_gui_headers())
self.model2.setHorizontalHeaderLabels(
self.headers.get_user_gui_headers())
fields = ", ".join(self.headers.user_fields)
search_clause = (
"title LIKE ? OR artist LIKE ? OR album LIKE ?"
@ -731,7 +746,8 @@ class MusicTable(QTableView):
query = f"{query} WHERE {search_clause};"
else:
query = f"{query} AND {search_clause};"
data = db.query(query, (self.selected_playlist_id, params))
data = db.query(
query, (self.selected_playlist_id, params))
else:
data = db.query(query, (self.selected_playlist_id,))
@ -788,7 +804,8 @@ class MusicTable(QTableView):
# reloading the model destroys and makes new indexes
# so we look for the new index of the current song on load
current_song_filepath = self.get_current_song_filepath()
debug(f"load_music_table() | current filepath: {current_song_filepath}")
debug(
f"load_music_table() | current filepath: {current_song_filepath}")
for row in range(self.model2.rowCount()):
real_index = self.model2.index(
row, self.headers.user_fields.index("filepath")
@ -814,7 +831,8 @@ class MusicTable(QTableView):
"""
Loads the header widths from the last application close.
"""
table_view_column_widths = str(self.config["table"]["column_widths"]).split(",")
table_view_column_widths = str(
self.config["table"]["column_widths"]).split(",")
debug(f"loaded header widths: {table_view_column_widths}")
if not isinstance(table_view_column_widths, list):
for i in range(self.model2.columnCount() - 1):
@ -898,7 +916,8 @@ class MusicTable(QTableView):
selected_rows = self.get_selected_rows()
filepaths = []
for row in selected_rows:
idx = self.proxymodel.index(row, self.headers.user_fields.index("filepath"))
idx = self.proxymodel.index(
row, self.headers.user_fields.index("filepath"))
filepaths.append(idx.data())
return filepaths

View File

@ -1,7 +1,6 @@
from PyQt5.QtWidgets import (
QAction,
QInputDialog,
QListWidget,
QMenu,
QMessageBox,
QTreeWidget,
@ -10,6 +9,7 @@ from PyQt5.QtWidgets import (
from PyQt5.QtCore import QThreadPool, pyqtSignal, Qt, QPoint
import DBA
from logging import debug
from components.ErrorDialog import ErrorDialog
from utils import Worker
from components import CreatePlaylistWindow
@ -43,10 +43,6 @@ class PlaylistsPane(QTreeWidget):
self.customContextMenuRequested.connect(self.showContextMenu)
self.currentItemChanged.connect(self.playlist_clicked)
self.playlist_db_id_choice: int | None = None
self.threadpool: QThreadPool | None = None
def set_threadpool(self, threadpool: QThreadPool):
self.threadpool = threadpool
def reload_playlists(self, progress_callback=None):
"""
@ -60,7 +56,7 @@ class PlaylistsPane(QTreeWidget):
playlists = db.query(
"SELECT id, name FROM playlist ORDER BY date_created DESC;", ()
)
debug(f'PlaylistsPane: | playlists = {playlists}')
# debug(f'PlaylistsPane: | playlists = {playlists}')
for playlist in playlists:
branch = PlaylistWidgetItem(self, playlist[0], playlist[1])
self._playlists_root.addChild(branch)
@ -86,7 +82,7 @@ class PlaylistsPane(QTreeWidget):
def create_playlist(self):
"""Creates a database record for a playlist, given a name"""
window = CreatePlaylistWindow(self.playlistCreatedSignal)
window.playlistCreatedSignal.connect(self.reload_playlists) # type: ignore
window.playlistCreatedSignal.connect(self.reload_playlists)
window.exec_()
def rename_playlist(self, *args):
@ -110,8 +106,9 @@ class PlaylistsPane(QTreeWidget):
(text, self.playlist_db_id_choice),
)
worker = Worker(self.reload_playlists)
if self.threadpool:
self.threadpool.start(worker)
if self.qapp:
threadpool = self.qapp.threadpool
threadpool.start(worker)
def delete_playlist(self, *args):
"""Deletes a playlist"""
@ -123,14 +120,18 @@ class PlaylistsPane(QTreeWidget):
QMessageBox.Yes,
)
if reply == QMessageBox.Yes:
if self.qapp:
with DBA.DBAccess() as db:
db.execute(
"DELETE FROM playlist WHERE id = ?;", (self.playlist_db_id_choice,)
)
# reload
worker = Worker(self.reload_playlists)
if self.threadpool:
self.threadpool.start(worker)
threadpool = self.qapp.threadpool
threadpool.start(worker)
else:
error_dialog = ErrorDialog("Main application [qapp] not loaded - aborting operation")
error_dialog.exec()
def playlist_clicked(self, item):
"""Specific playlist pane index was clicked"""
@ -139,10 +140,14 @@ class PlaylistsPane(QTreeWidget):
# self.all_songs_selected()
self.allSongsSignal.emit()
elif isinstance(item, PlaylistWidgetItem):
debug(f"ID: {item.id}, name: {item.text(0)}")
# debug(f"ID: {item.id}, name: {item.text(0)}")
self.playlist_db_id_choice = item.id
self.playlistChoiceSignal.emit(int(item.id))
def load_qapp(self, qapp) -> None:
"""Necessary for using members and methods of main application window"""
self.qapp = qapp
# def all_songs_selected(self):
# """Emits a signal to display all songs in the library"""
# # I have no idea why this has to be in its own function, but it does

View File

@ -42,7 +42,7 @@ class SearchLineEdit(QLineEdit):
def on_text_changed(self):
"""Reset a timer each time text is changed"""
self.timer.start(300)
self.timer.start(1500)
def on_typing_stopped(self):
"""When timer reaches end, emit the text that is currently entered"""

12
main.py
View File

@ -92,7 +92,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
/ "config.ini"
)
self.config.read(self.cfg_file)
debug(f"\tmain config: {self.config}")
self.threadpool: QThreadPool = QThreadPool()
# UI
self.setupUi(self)
@ -105,6 +104,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.status_bar.addPermanentWidget(self.permanent_status_label)
self.setStatusBar(self.status_bar)
# table
self.selected_song_filepath: str | None = None
self.current_song_filepath: str | None = None
self.current_song_metadata: ID3 | dict | None = None
@ -113,7 +113,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
# widget bits
self.tableView: MusicTable
self.album_art_scene: QGraphicsScene = QGraphicsScene()
# self.player: QMediaPlayer = QMediaPlayer() # Audio player object
self.player: QMediaPlayer = MediaPlayer()
# set index on choose song
# index is the model2's row number? i guess?
@ -143,6 +142,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
# sharing functions with other classes and that
self.tableView.load_qapp(self)
self.albumGraphicsView.load_qapp(self)
self.playlistTreeView.load_qapp(self)
self.headers = HeaderTags()
# Settings init
@ -214,7 +214,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.tableView.load_music_table
)
self.playlistTreeView.allSongsSignal.connect(self.tableView.load_music_table)
self.playlistTreeView.set_threadpool(self.threadpool)
# albumGraphicsView
self.albumGraphicsView.albumArtDropped.connect(
@ -393,16 +392,11 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
def load_config(self) -> None:
"""does what it says"""
cfg_file = (
Path(user_config_dir(appname="musicpom", appauthor="billypom"))
/ "config.ini"
Path(user_config_dir(appname="musicpom", appauthor="billypom")) / "config.ini"
)
self.config.read(cfg_file)
debug("load_config()")
def get_thread_pool(self) -> QThreadPool:
"""Returns the threadpool instance"""
return self.threadpool
def set_permanent_status_bar_message(self, message: str) -> None:
"""
Sets the permanent message label in the status bar

View File

@ -27,10 +27,10 @@ class WorkerSignals(QObject):
# pandas DataFrames or numpy arrays. Instead, pass the minimum amount of information needed
# (i.e. lists of filepaths)
signal_started = pyqtSignal()
signal_result = pyqtSignal(object)
signal_finished = pyqtSignal()
signal_progress = pyqtSignal(str)
signal_started: pyqtSignal = pyqtSignal()
signal_result: pyqtSignal = pyqtSignal(object)
signal_finished: pyqtSignal = pyqtSignal()
signal_progress: pyqtSignal = pyqtSignal(str)
class Worker(QRunnable):
@ -46,12 +46,12 @@ class Worker(QRunnable):
:param kwargs: Keywords to pass to the callback function
"""
def __init__(self, fn, *args, **kwargs):
def __init__(self, fn, *args: object, **kwargs: object):
super(Worker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.args: object = args
self.kwargs: object = kwargs
self.signals: WorkerSignals = WorkerSignals()
# Add a callback to our kwargs