working on stuff and fixing nvim config testing stuff
This commit is contained in:
parent
d01a743c45
commit
dbfab01b53
@ -1,4 +1,3 @@
|
|||||||
from mutagen.id3 import ID3
|
|
||||||
import DBA
|
import DBA
|
||||||
from PyQt5.QtGui import (
|
from PyQt5.QtGui import (
|
||||||
QColor,
|
QColor,
|
||||||
@ -14,6 +13,7 @@ from PyQt5.QtGui import (
|
|||||||
)
|
)
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QAction,
|
QAction,
|
||||||
|
QApplication,
|
||||||
QHeaderView,
|
QHeaderView,
|
||||||
QMenu,
|
QMenu,
|
||||||
QTableView,
|
QTableView,
|
||||||
@ -26,11 +26,9 @@ from PyQt5.QtCore import (
|
|||||||
QSortFilterProxyModel,
|
QSortFilterProxyModel,
|
||||||
Qt,
|
Qt,
|
||||||
QModelIndex,
|
QModelIndex,
|
||||||
QThreadPool,
|
|
||||||
pyqtSignal,
|
pyqtSignal,
|
||||||
)
|
)
|
||||||
from components.DebugWindow import DebugWindow
|
from components.DebugWindow import DebugWindow
|
||||||
from components.ErrorDialog import ErrorDialog
|
|
||||||
from components.LyricsWindow import LyricsWindow
|
from components.LyricsWindow import LyricsWindow
|
||||||
from components.AddToPlaylistWindow import AddToPlaylistWindow
|
from components.AddToPlaylistWindow import AddToPlaylistWindow
|
||||||
from components.MetadataWindow import MetadataWindow
|
from components.MetadataWindow import MetadataWindow
|
||||||
@ -40,11 +38,9 @@ from components.HeaderTags import HeaderTags
|
|||||||
from utils import (
|
from utils import (
|
||||||
batch_delete_filepaths_from_database,
|
batch_delete_filepaths_from_database,
|
||||||
batch_delete_filepaths_from_playlist,
|
batch_delete_filepaths_from_playlist,
|
||||||
delete_song_id_from_database,
|
|
||||||
add_files_to_database,
|
add_files_to_database,
|
||||||
get_reorganize_vars,
|
get_reorganize_vars,
|
||||||
update_song_in_database,
|
update_song_in_database,
|
||||||
get_album_art,
|
|
||||||
id3_remap,
|
id3_remap,
|
||||||
get_tags,
|
get_tags,
|
||||||
set_tag,
|
set_tag,
|
||||||
@ -59,21 +55,20 @@ from pathlib import Path
|
|||||||
from appdirs import user_config_dir
|
from appdirs import user_config_dir
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
|
|
||||||
|
|
||||||
class MusicTable(QTableView):
|
class MusicTable(QTableView):
|
||||||
playlistStatsSignal = pyqtSignal(str)
|
playlistStatsSignal: pyqtSignal = pyqtSignal(str)
|
||||||
loadMusicTableSignal = pyqtSignal()
|
loadMusicTableSignal: pyqtSignal = pyqtSignal()
|
||||||
sortSignal = pyqtSignal()
|
sortSignal: pyqtSignal = pyqtSignal()
|
||||||
playPauseSignal = pyqtSignal()
|
playPauseSignal: pyqtSignal = pyqtSignal()
|
||||||
playSignal = pyqtSignal(str)
|
playSignal: pyqtSignal = pyqtSignal(str)
|
||||||
enterKey = pyqtSignal()
|
enterKey: pyqtSignal = pyqtSignal()
|
||||||
deleteKey = pyqtSignal()
|
deleteKey: pyqtSignal = pyqtSignal()
|
||||||
refreshMusicTableSignal = pyqtSignal()
|
refreshMusicTableSignal: pyqtSignal = pyqtSignal()
|
||||||
handleProgressSignal = pyqtSignal(str)
|
handleProgressSignal: pyqtSignal = pyqtSignal(str)
|
||||||
getThreadPoolSignal = pyqtSignal()
|
getThreadPoolSignal: pyqtSignal = pyqtSignal()
|
||||||
searchBoxSignal = pyqtSignal()
|
searchBoxSignal: pyqtSignal = pyqtSignal()
|
||||||
focusEnterSignal = pyqtSignal()
|
focusEnterSignal: pyqtSignal = pyqtSignal()
|
||||||
focusLeaveSignal = pyqtSignal()
|
focusLeaveSignal: pyqtSignal = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent=None, application_window=None):
|
def __init__(self, parent=None, application_window=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@ -85,7 +80,7 @@ class MusicTable(QTableView):
|
|||||||
Path(user_config_dir(appname="musicpom", appauthor="billypom"))
|
Path(user_config_dir(appname="musicpom", appauthor="billypom"))
|
||||||
/ "config.ini"
|
/ "config.ini"
|
||||||
)
|
)
|
||||||
self.config.read(cfg_file)
|
_ = self.config.read(cfg_file)
|
||||||
debug(f"music table config: {self.config}")
|
debug(f"music table config: {self.config}")
|
||||||
|
|
||||||
# NOTE:
|
# NOTE:
|
||||||
@ -96,13 +91,12 @@ class MusicTable(QTableView):
|
|||||||
# Create QSortFilterProxyModel
|
# Create QSortFilterProxyModel
|
||||||
# Set QSortFilterProxyModel source to QStandardItemModel
|
# Set QSortFilterProxyModel source to QStandardItemModel
|
||||||
# Set QTableView model to the Proxy model
|
# Set QTableView model to the Proxy model
|
||||||
# so it looks like that, i guess
|
# so it looks like the above note, i guess
|
||||||
|
|
||||||
# need a QStandardItemModel to do actions on cells
|
# need a QStandardItemModel to do actions on cells
|
||||||
self.model2: QStandardItemModel = QStandardItemModel()
|
self.model2: QStandardItemModel = QStandardItemModel()
|
||||||
self.proxymodel = QSortFilterProxyModel()
|
self.proxymodel: QSortFilterProxyModel = QSortFilterProxyModel()
|
||||||
self.search_string = None
|
self.search_string: str | None = None
|
||||||
self.threadpool = QThreadPool
|
|
||||||
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(
|
||||||
@ -144,8 +138,7 @@ class MusicTable(QTableView):
|
|||||||
self.deleteKey.connect(self.delete_songs)
|
self.deleteKey.connect(self.delete_songs)
|
||||||
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.model2.dataChanged.connect(self.on_cell_data_changed) # editing cells
|
||||||
self.on_cell_data_changed) # editing cells
|
|
||||||
# self.model2.layoutChanged.connect(self.restore_scroll_position)
|
# 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
|
||||||
@ -202,35 +195,32 @@ class MusicTable(QTableView):
|
|||||||
"""Right-click context menu"""
|
"""Right-click context menu"""
|
||||||
menu = QMenu(self)
|
menu = QMenu(self)
|
||||||
add_to_playlist_action = QAction("Add to playlist", self)
|
add_to_playlist_action = QAction("Add to playlist", self)
|
||||||
add_to_playlist_action.triggered.connect(
|
_ = add_to_playlist_action.triggered.connect(self.add_selected_files_to_playlist)
|
||||||
self.add_selected_files_to_playlist)
|
|
||||||
menu.addAction(add_to_playlist_action)
|
menu.addAction(add_to_playlist_action)
|
||||||
# edit metadata
|
# edit metadata
|
||||||
edit_metadata_action = QAction("Edit metadata", self)
|
edit_metadata_action = QAction("Edit metadata", self)
|
||||||
edit_metadata_action.triggered.connect(
|
_ = edit_metadata_action.triggered.connect(self.edit_selected_files_metadata)
|
||||||
self.edit_selected_files_metadata)
|
|
||||||
menu.addAction(edit_metadata_action)
|
menu.addAction(edit_metadata_action)
|
||||||
# edit lyrics
|
# edit lyrics
|
||||||
edit_lyrics_action = QAction("Lyrics (View/Edit)", self)
|
edit_lyrics_action = QAction("Lyrics (View/Edit)", self)
|
||||||
edit_lyrics_action.triggered.connect(self.show_lyrics_menu)
|
_ = edit_lyrics_action.triggered.connect(self.show_lyrics_menu)
|
||||||
menu.addAction(edit_lyrics_action)
|
menu.addAction(edit_lyrics_action)
|
||||||
# jump to current song in table
|
# jump to current song in table
|
||||||
jump_to_current_song_action = QAction("Jump to current song", self)
|
jump_to_current_song_action = QAction("Jump to current song", self)
|
||||||
jump_to_current_song_action.triggered.connect(
|
_ = jump_to_current_song_action.triggered.connect(self.jump_to_current_song)
|
||||||
self.jump_to_current_song)
|
|
||||||
menu.addAction(jump_to_current_song_action)
|
menu.addAction(jump_to_current_song_action)
|
||||||
# open in file explorer
|
# open in file explorer
|
||||||
open_containing_folder_action = QAction(
|
open_containing_folder_action = QAction(
|
||||||
"Open in system file manager", self)
|
"Open in system file manager", self)
|
||||||
open_containing_folder_action.triggered.connect(self.open_directory)
|
_ = open_containing_folder_action.triggered.connect(self.open_directory)
|
||||||
menu.addAction(open_containing_folder_action)
|
menu.addAction(open_containing_folder_action)
|
||||||
# view id3 tags (debug)
|
# view id3 tags (debug)
|
||||||
view_id3_tags_debug = QAction("View ID3 tags (debug)", self)
|
view_id3_tags_debug = QAction("View ID3 tags (debug)", self)
|
||||||
view_id3_tags_debug.triggered.connect(self.view_id3_tags_debug_menu)
|
_ = view_id3_tags_debug.triggered.connect(self.view_id3_tags_debug_menu)
|
||||||
menu.addAction(view_id3_tags_debug)
|
menu.addAction(view_id3_tags_debug)
|
||||||
# delete song
|
# delete song
|
||||||
delete_action = QAction("Delete", self)
|
delete_action = QAction("Delete", self)
|
||||||
delete_action.triggered.connect(self.delete_songs)
|
_ = delete_action.triggered.connect(self.delete_songs)
|
||||||
menu.addAction(delete_action)
|
menu.addAction(delete_action)
|
||||||
# show
|
# show
|
||||||
self.set_selected_song_filepath()
|
self.set_selected_song_filepath()
|
||||||
@ -261,8 +251,8 @@ class MusicTable(QTableView):
|
|||||||
data = e.mimeData()
|
data = e.mimeData()
|
||||||
debug("dropEvent")
|
debug("dropEvent")
|
||||||
if data and data.hasUrls():
|
if data and data.hasUrls():
|
||||||
directories = []
|
directories: list[str] = []
|
||||||
files = []
|
files: list[str] = []
|
||||||
for url in data.urls():
|
for url in data.urls():
|
||||||
if url.isLocalFile():
|
if url.isLocalFile():
|
||||||
path = url.toLocalFile()
|
path = url.toLocalFile()
|
||||||
@ -275,11 +265,9 @@ class MusicTable(QTableView):
|
|||||||
e.accept()
|
e.accept()
|
||||||
if directories:
|
if directories:
|
||||||
worker = Worker(self.get_audio_files_recursively, directories)
|
worker = Worker(self.get_audio_files_recursively, directories)
|
||||||
worker.signals.signal_progress.connect(self.handle_progress)
|
_ = worker.signals.signal_progress.connect(self.handle_progress)
|
||||||
worker.signals.signal_result.connect(
|
_ = worker.signals.signal_result.connect(self.on_get_audio_files_recursively_finished)
|
||||||
self.on_get_audio_files_recursively_finished
|
_ = worker.signals.signal_finished.connect(self.load_music_table)
|
||||||
)
|
|
||||||
worker.signals.signal_finished.connect(self.load_music_table)
|
|
||||||
if self.qapp:
|
if self.qapp:
|
||||||
threadpool = self.qapp.threadpool
|
threadpool = self.qapp.threadpool
|
||||||
threadpool.start(worker)
|
threadpool.start(worker)
|
||||||
@ -301,9 +289,8 @@ class MusicTable(QTableView):
|
|||||||
index = self.currentIndex()
|
index = self.currentIndex()
|
||||||
new_index = self.model2.index(index.row(), index.column() + 1)
|
new_index = self.model2.index(index.row(), index.column() + 1)
|
||||||
if new_index.isValid():
|
if new_index.isValid():
|
||||||
# print(f"right -> ({new_index.row()},{new_index.column()})")
|
|
||||||
self.setCurrentIndex(new_index)
|
self.setCurrentIndex(new_index)
|
||||||
self.viewport().update() # type: ignore
|
self.viewport().update()
|
||||||
super().keyPressEvent(e)
|
super().keyPressEvent(e)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -311,9 +298,8 @@ class MusicTable(QTableView):
|
|||||||
index = self.currentIndex()
|
index = self.currentIndex()
|
||||||
new_index = self.model2.index(index.row(), index.column() - 1)
|
new_index = self.model2.index(index.row(), index.column() - 1)
|
||||||
if new_index.isValid():
|
if new_index.isValid():
|
||||||
# print(f"left -> ({new_index.row()},{new_index.column()})")
|
|
||||||
self.setCurrentIndex(new_index)
|
self.setCurrentIndex(new_index)
|
||||||
self.viewport().update() # type: ignore
|
self.viewport().update()
|
||||||
super().keyPressEvent(e)
|
super().keyPressEvent(e)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -321,9 +307,8 @@ class MusicTable(QTableView):
|
|||||||
index = self.currentIndex()
|
index = self.currentIndex()
|
||||||
new_index = self.model2.index(index.row() - 1, index.column())
|
new_index = self.model2.index(index.row() - 1, index.column())
|
||||||
if new_index.isValid():
|
if new_index.isValid():
|
||||||
# print(f"up -> ({new_index.row()},{new_index.column()})")
|
|
||||||
self.setCurrentIndex(new_index)
|
self.setCurrentIndex(new_index)
|
||||||
self.viewport().update() # type: ignore
|
self.viewport().update()
|
||||||
super().keyPressEvent(e)
|
super().keyPressEvent(e)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -331,15 +316,14 @@ class MusicTable(QTableView):
|
|||||||
index = self.currentIndex()
|
index = self.currentIndex()
|
||||||
new_index = self.model2.index(index.row() + 1, index.column())
|
new_index = self.model2.index(index.row() + 1, index.column())
|
||||||
if new_index.isValid():
|
if new_index.isValid():
|
||||||
# print(f"down -> ({new_index.row()},{new_index.column()})")
|
|
||||||
self.setCurrentIndex(new_index)
|
self.setCurrentIndex(new_index)
|
||||||
self.viewport().update() # type: ignore
|
self.viewport().update()
|
||||||
super().keyPressEvent(e)
|
super().keyPressEvent(e)
|
||||||
return
|
return
|
||||||
|
|
||||||
elif key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
elif key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
||||||
if self.state() != QAbstractItemView.EditingState:
|
if self.state() != QAbstractItemView.EditingState:
|
||||||
self.enterKey.emit() # Enter key detected
|
self.enterKey.emit()
|
||||||
else:
|
else:
|
||||||
super().keyPressEvent(e)
|
super().keyPressEvent(e)
|
||||||
else: # Default behavior
|
else: # Default behavior
|
||||||
@ -450,8 +434,10 @@ class MusicTable(QTableView):
|
|||||||
- data returned from the original worker process function are returned here
|
- data returned from the original worker process function are returned here
|
||||||
as the first item in a tuple
|
as the first item in a tuple
|
||||||
"""
|
"""
|
||||||
_, details = args[0][:2]
|
print('hello?')
|
||||||
|
print(args)
|
||||||
try:
|
try:
|
||||||
|
_, details = args[0][:2]
|
||||||
details = dict(tuple(details)[0])
|
details = dict(tuple(details)[0])
|
||||||
if details:
|
if details:
|
||||||
window = DebugWindow(details)
|
window = DebugWindow(details)
|
||||||
@ -484,14 +470,15 @@ class MusicTable(QTableView):
|
|||||||
def add_files_to_library(self, files: list[str]) -> None:
|
def add_files_to_library(self, files: list[str]) -> None:
|
||||||
"""
|
"""
|
||||||
Spawns a worker thread - adds a list of filepaths to the library
|
Spawns a worker thread - adds a list of filepaths to the library
|
||||||
|
|
||||||
- Drag & Drop song(s) on tableView
|
- Drag & Drop song(s) on tableView
|
||||||
- File > Open > List of song(s)
|
- File > Open > List of song(s)
|
||||||
"""
|
"""
|
||||||
worker = Worker(add_files_to_database, files)
|
debug('add_files_to_library()')
|
||||||
|
worker = Worker(add_files_to_database, files, None)
|
||||||
_ = worker.signals.signal_progress.connect(self.qapp.handle_progress)
|
_ = worker.signals.signal_progress.connect(self.qapp.handle_progress)
|
||||||
_ = worker.signals.signal_result.connect(
|
_ = worker.signals.signal_result.connect(self.on_add_files_to_database_finished)
|
||||||
self.on_add_files_to_database_finished)
|
_ = worker.signals.signal_finished.connect(self.load_music_table)
|
||||||
worker.signals.signal_finished.connect(self.load_music_table)
|
|
||||||
if self.qapp:
|
if self.qapp:
|
||||||
threadpool = self.qapp.threadpool
|
threadpool = self.qapp.threadpool
|
||||||
threadpool.start(worker)
|
threadpool.start(worker)
|
||||||
@ -711,7 +698,7 @@ class MusicTable(QTableView):
|
|||||||
self.set_current_song_filepath()
|
self.set_current_song_filepath()
|
||||||
self.playPauseSignal.emit()
|
self.playPauseSignal.emit()
|
||||||
|
|
||||||
def load_music_table(self, *playlist_id):
|
def load_music_table(self, *playlist_id: int):
|
||||||
"""
|
"""
|
||||||
Loads data into self (QTableView)
|
Loads data into self (QTableView)
|
||||||
Loads all songs in library, by default
|
Loads all songs in library, by default
|
||||||
@ -721,7 +708,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() # type: ignore
|
self.vertical_scroll_position = self.verticalScrollBar().value()
|
||||||
self.model2.clear()
|
self.model2.clear()
|
||||||
self.model2.setHorizontalHeaderLabels(
|
self.model2.setHorizontalHeaderLabels(
|
||||||
self.headers.get_user_gui_headers())
|
self.headers.get_user_gui_headers())
|
||||||
@ -845,10 +832,8 @@ 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
|
||||||
"""
|
"""
|
||||||
# TODO: Rewrite this function to use self.load_music_table() with dynamic SQL queries
|
|
||||||
# in order to sort the data more effectively & have more control over UI refreshes.
|
|
||||||
|
|
||||||
# Disconnect these signals to prevent unnecessary loads
|
# Disconnect these signals to prevent unnecessary reloads
|
||||||
debug("sort_table_by_multiple_columns()")
|
debug("sort_table_by_multiple_columns()")
|
||||||
self.disconnect_data_changed()
|
self.disconnect_data_changed()
|
||||||
self.disconnect_layout_changed()
|
self.disconnect_layout_changed()
|
||||||
@ -879,6 +864,8 @@ class MusicTable(QTableView):
|
|||||||
self.connect_data_changed()
|
self.connect_data_changed()
|
||||||
self.connect_layout_changed()
|
self.connect_layout_changed()
|
||||||
# self.model2.layoutChanged.emit()
|
# self.model2.layoutChanged.emit()
|
||||||
|
# TODO: Rewrite this function to use self.load_music_table() with dynamic SQL queries
|
||||||
|
# in order to sort the data more effectively & have more control over UI refreshes.
|
||||||
|
|
||||||
def restore_scroll_position(self) -> None:
|
def restore_scroll_position(self) -> None:
|
||||||
"""Restores the scroll position"""
|
"""Restores the scroll position"""
|
||||||
@ -888,10 +875,10 @@ class MusicTable(QTableView):
|
|||||||
# lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position),
|
# lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position),
|
||||||
# )
|
# )
|
||||||
|
|
||||||
def get_audio_files_recursively(self, directories, progress_callback=None):
|
def get_audio_files_recursively(self, directories: list[str], progress_callback=None) -> list[str]:
|
||||||
"""Scans a directories for files"""
|
"""Scans a directories for files"""
|
||||||
extensions = self.config.get("settings", "extensions").split(",")
|
extensions = self.config.get("settings", "extensions").split(",")
|
||||||
audio_files = []
|
audio_files: list[str] = []
|
||||||
for directory in directories:
|
for directory in directories:
|
||||||
for root, _, files in os.walk(directory):
|
for root, _, files in os.walk(directory):
|
||||||
for file in files:
|
for file in files:
|
||||||
@ -965,9 +952,7 @@ class MusicTable(QTableView):
|
|||||||
self.proxymodel.index(row, 0), Qt.ItemDataRole.UserRole
|
self.proxymodel.index(row, 0), Qt.ItemDataRole.UserRole
|
||||||
)
|
)
|
||||||
with DBA.DBAccess() as db:
|
with DBA.DBAccess() as db:
|
||||||
filepath = db.query("SELECT filepath FROM song WHERE id = ?", (id,))[0][
|
filepath = db.query("SELECT filepath FROM song WHERE id = ?", (id,))[0][0]
|
||||||
0
|
|
||||||
]
|
|
||||||
self.selected_song_filepath = filepath
|
self.selected_song_filepath = filepath
|
||||||
|
|
||||||
def set_current_song_filepath(self, filepath=None) -> None:
|
def set_current_song_filepath(self, filepath=None) -> None:
|
||||||
@ -984,7 +969,7 @@ class MusicTable(QTableView):
|
|||||||
else:
|
else:
|
||||||
self.current_song_filepath = filepath
|
self.current_song_filepath = filepath
|
||||||
|
|
||||||
def set_current_song_qmodel_index(self, index=None):
|
def set_current_song_qmodel_index(self, index: QModelIndex | None = None):
|
||||||
"""
|
"""
|
||||||
Takes in the proxy model index for current song - QModelIndex
|
Takes in the proxy model index for current song - QModelIndex
|
||||||
converts to model2 index
|
converts to model2 index
|
||||||
@ -994,9 +979,9 @@ class MusicTable(QTableView):
|
|||||||
index = self.currentIndex()
|
index = self.currentIndex()
|
||||||
# map proxy (sortable) model to the original model (used for interactions)
|
# map proxy (sortable) model to the original model (used for interactions)
|
||||||
real_index: QModelIndex = self.proxymodel.mapToSource(index)
|
real_index: QModelIndex = self.proxymodel.mapToSource(index)
|
||||||
self.current_song_qmodel_index: QModelIndex = real_index
|
self.current_song_qmodel_index = real_index
|
||||||
|
|
||||||
def set_selected_song_qmodel_index(self, index=None):
|
def set_selected_song_qmodel_index(self, index: QModelIndex | None = None):
|
||||||
"""
|
"""
|
||||||
Takes in the proxy model index for current song - QModelIndex
|
Takes in the proxy model index for current song - QModelIndex
|
||||||
converts to model2 index
|
converts to model2 index
|
||||||
@ -1006,15 +991,15 @@ class MusicTable(QTableView):
|
|||||||
index = self.currentIndex()
|
index = self.currentIndex()
|
||||||
# map proxy (sortable) model to the original model (used for interactions)
|
# map proxy (sortable) model to the original model (used for interactions)
|
||||||
real_index: QModelIndex = self.proxymodel.mapToSource(index)
|
real_index: QModelIndex = self.proxymodel.mapToSource(index)
|
||||||
self.selected_song_qmodel_index: QModelIndex = real_index
|
self.selected_song_qmodel_index = real_index
|
||||||
|
|
||||||
def set_search_string(self, text: str):
|
def set_search_string(self, text: str):
|
||||||
"""set the search string"""
|
"""set the search string"""
|
||||||
self.search_string = text
|
self.search_string = text
|
||||||
|
|
||||||
def load_qapp(self, qapp) -> None:
|
def load_qapp(self, qapp: QApplication) -> None:
|
||||||
"""Necessary for using members and methods of main application window"""
|
"""Necessary for using members and methods of main application window"""
|
||||||
self.qapp = qapp
|
self.qapp: QApplication = qapp
|
||||||
|
|
||||||
# ____________________
|
# ____________________
|
||||||
# | |
|
# | |
|
||||||
@ -1033,7 +1018,7 @@ class MusicTable(QTableView):
|
|||||||
def connect_data_changed(self):
|
def connect_data_changed(self):
|
||||||
"""Connects the dataChanged signal from QTableView.model"""
|
"""Connects the dataChanged signal from QTableView.model"""
|
||||||
try:
|
try:
|
||||||
self.model2.dataChanged.connect(self.on_cell_data_changed)
|
_ = self.model2.dataChanged.connect(self.on_cell_data_changed)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -1047,7 +1032,7 @@ 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)
|
_ = self.model2.layoutChanged.connect(self.restore_scroll_position)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
52
main.py
52
main.py
@ -12,7 +12,7 @@ from mutagen.id3 import ID3
|
|||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from appdirs import user_config_dir
|
from appdirs import user_config_dir
|
||||||
from logging import debug, error, warning, basicConfig, INFO, DEBUG
|
from logging import debug, error, basicConfig, DEBUG
|
||||||
from ui import Ui_MainWindow
|
from ui import Ui_MainWindow
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QFileDialog,
|
QFileDialog,
|
||||||
@ -21,34 +21,25 @@ from PyQt5.QtWidgets import (
|
|||||||
QMainWindow,
|
QMainWindow,
|
||||||
QApplication,
|
QApplication,
|
||||||
QGraphicsScene,
|
QGraphicsScene,
|
||||||
QGraphicsPixmapItem,
|
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
QStatusBar,
|
QStatusBar,
|
||||||
QStyle,
|
QStyle,
|
||||||
QTableView,
|
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import (
|
from PyQt5.QtCore import (
|
||||||
QModelIndex,
|
QModelIndex,
|
||||||
QSize,
|
QSize,
|
||||||
QThread,
|
|
||||||
QUrl,
|
QUrl,
|
||||||
QTimer,
|
QTimer,
|
||||||
Qt,
|
|
||||||
pyqtSignal,
|
pyqtSignal,
|
||||||
QObject,
|
|
||||||
pyqtSlot,
|
|
||||||
QThreadPool,
|
QThreadPool,
|
||||||
QRunnable,
|
|
||||||
)
|
)
|
||||||
from PyQt5.QtMultimedia import (
|
from PyQt5.QtMultimedia import (
|
||||||
QMediaPlayer,
|
QMediaPlayer,
|
||||||
QMediaContent,
|
QMediaContent,
|
||||||
QAudioProbe,
|
QAudioProbe,
|
||||||
QMediaPlaylist,
|
|
||||||
QMultimedia,
|
|
||||||
)
|
)
|
||||||
from PyQt5.QtGui import QClipboard, QCloseEvent, QFont, QPixmap, QResizeEvent
|
from PyQt5.QtGui import QCloseEvent, QFont, QResizeEvent
|
||||||
from utils import (
|
from utils import (
|
||||||
delete_album_art,
|
delete_album_art,
|
||||||
get_tags,
|
get_tags,
|
||||||
@ -68,7 +59,6 @@ from components import (
|
|||||||
AudioVisualizer,
|
AudioVisualizer,
|
||||||
CreatePlaylistWindow,
|
CreatePlaylistWindow,
|
||||||
ExportPlaylistWindow,
|
ExportPlaylistWindow,
|
||||||
SearchLineEdit,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# good help with signals slots in threads
|
# good help with signals slots in threads
|
||||||
@ -79,14 +69,14 @@ from components import (
|
|||||||
|
|
||||||
|
|
||||||
class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||||
reloadConfigSignal = pyqtSignal()
|
reloadConfigSignal: pyqtSignal = pyqtSignal()
|
||||||
reloadDatabaseSignal = pyqtSignal()
|
reloadDatabaseSignal: pyqtSignal = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, clipboard):
|
def __init__(self, clipboard):
|
||||||
super(ApplicationWindow, self).__init__()
|
super(ApplicationWindow, self).__init__()
|
||||||
self.clipboard = clipboard
|
self.clipboard = clipboard
|
||||||
self.config: ConfigParser = ConfigParser()
|
self.config: ConfigParser = ConfigParser()
|
||||||
self.cfg_file = (
|
self.cfg_file: Path = (
|
||||||
Path(user_config_dir(appname="musicpom", appauthor="billypom"))
|
Path(user_config_dir(appname="musicpom", appauthor="billypom"))
|
||||||
/ "config.ini"
|
/ "config.ini"
|
||||||
)
|
)
|
||||||
@ -119,14 +109,15 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
self.audio_visualizer: AudioVisualizer = AudioVisualizer(
|
self.audio_visualizer: AudioVisualizer = AudioVisualizer(
|
||||||
self.player, self.probe, self.PlotWidget
|
self.player, self.probe, self.PlotWidget
|
||||||
)
|
)
|
||||||
self.timer = QTimer(self) # for playback slider and such
|
self.timer: QTimer = QTimer(parent=self) # for playback slider and such
|
||||||
|
|
||||||
# Button styles
|
# Button styles
|
||||||
|
style: QStyle | None
|
||||||
if not self.style():
|
if not self.style():
|
||||||
style = QStyle()
|
style = QStyle()
|
||||||
else:
|
else:
|
||||||
style = self.style()
|
style = self.style()
|
||||||
assert style is not None # i hate linting errors
|
assert style is not None
|
||||||
pixmapi = QStyle.StandardPixmap.SP_MediaSkipForward
|
pixmapi = QStyle.StandardPixmap.SP_MediaSkipForward
|
||||||
icon = style.standardIcon(pixmapi)
|
icon = style.standardIcon(pixmapi)
|
||||||
self.nextButton.setIcon(icon)
|
self.nextButton.setIcon(icon)
|
||||||
@ -380,7 +371,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
QtCore.Qt.TextInteractionFlag.TextSelectableByMouse
|
QtCore.Qt.TextInteractionFlag.TextSelectableByMouse
|
||||||
)
|
)
|
||||||
|
|
||||||
font: QFont = QFont()
|
|
||||||
font.setPointSize(12)
|
font.setPointSize(12)
|
||||||
font.setBold(False)
|
font.setBold(False)
|
||||||
self.titleLabel.setFont(font)
|
self.titleLabel.setFont(font)
|
||||||
@ -388,7 +378,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
QtCore.Qt.TextInteractionFlag.TextSelectableByMouse
|
QtCore.Qt.TextInteractionFlag.TextSelectableByMouse
|
||||||
)
|
)
|
||||||
|
|
||||||
font: QFont = QFont()
|
|
||||||
font.setPointSize(12)
|
font.setPointSize(12)
|
||||||
font.setItalic(True)
|
font.setItalic(True)
|
||||||
self.albumLabel.setFont(font)
|
self.albumLabel.setFont(font)
|
||||||
@ -585,10 +574,8 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
preferences_window = PreferencesWindow(
|
preferences_window = PreferencesWindow(
|
||||||
self.reloadConfigSignal, self.reloadDatabaseSignal
|
self.reloadConfigSignal, self.reloadDatabaseSignal
|
||||||
)
|
)
|
||||||
preferences_window.reloadConfigSignal.connect(
|
preferences_window.reloadConfigSignal.connect(self.load_config)
|
||||||
self.load_config) # type: ignore
|
preferences_window.reloadDatabaseSignal.connect(self.tableView.load_music_table)
|
||||||
preferences_window.reloadDatabaseSignal.connect(
|
|
||||||
self.tableView.load_music_table) # type: ignore
|
|
||||||
preferences_window.exec_() # Display the preferences window modally
|
preferences_window.exec_() # Display the preferences window modally
|
||||||
|
|
||||||
# Quick Actions
|
# Quick Actions
|
||||||
@ -606,11 +593,11 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
def delete_database(self) -> None:
|
def delete_database(self) -> None:
|
||||||
"""Deletes the entire database"""
|
"""Deletes the entire database"""
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
self,
|
parent=self,
|
||||||
"Confirmation",
|
title="Confirmation",
|
||||||
"Delete database?",
|
text="Delete database?",
|
||||||
QMessageBox.Yes | QMessageBox.No,
|
buttons=QMessageBox.Yes | QMessageBox.No,
|
||||||
QMessageBox.Yes,
|
defaultButton=QMessageBox.Yes,
|
||||||
)
|
)
|
||||||
if reply == QMessageBox.Yes:
|
if reply == QMessageBox.Yes:
|
||||||
initialize_db()
|
initialize_db()
|
||||||
@ -629,7 +616,8 @@ def update_database_file() -> bool:
|
|||||||
appname="musicpom", appauthor="billypom")))
|
appname="musicpom", appauthor="billypom")))
|
||||||
config = ConfigParser()
|
config = ConfigParser()
|
||||||
config.read(cfg_file)
|
config.read(cfg_file)
|
||||||
db_filepath: str = config.get("settings", "db")
|
db_filepath: str
|
||||||
|
db_filepath= config.get("settings", "db")
|
||||||
|
|
||||||
# If the database location isnt set at the config location, move it
|
# If the database location isnt set at the config location, move it
|
||||||
if not db_filepath.startswith(cfg_path):
|
if not db_filepath.startswith(cfg_path):
|
||||||
@ -643,7 +631,7 @@ def update_database_file() -> bool:
|
|||||||
with open(cfg_file, "w") as configfile:
|
with open(cfg_file, "w") as configfile:
|
||||||
config.write(configfile)
|
config.write(configfile)
|
||||||
config.read(cfg_file)
|
config.read(cfg_file)
|
||||||
db_filepath: str = config.get("settings", "db")
|
db_filepath = config.get("settings", "db")
|
||||||
|
|
||||||
db_path = db_filepath.split("/")
|
db_path = db_filepath.split("/")
|
||||||
db_path.pop()
|
db_path.pop()
|
||||||
@ -722,8 +710,8 @@ if __name__ == "__main__":
|
|||||||
format="{%(filename)s:%(lineno)d} %(levelname)s - %(message)s",
|
format="{%(filename)s:%(lineno)d} %(levelname)s - %(message)s",
|
||||||
handlers=handlers,
|
handlers=handlers,
|
||||||
)
|
)
|
||||||
debug(f'--------- musicpom debug started')
|
debug('--------- musicpom debug started')
|
||||||
debug(f'---------------------| ')
|
debug('---------------------| ')
|
||||||
debug(f'----------------------> {handlers} ')
|
debug(f'----------------------> {handlers} ')
|
||||||
# Initialization
|
# Initialization
|
||||||
config: ConfigParser = update_config_file()
|
config: ConfigParser = update_config_file()
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from PyQt5.QtWidgets import QMessageBox
|
|
||||||
from mutagen.id3 import ID3
|
|
||||||
import DBA
|
import DBA
|
||||||
from logging import debug
|
from logging import debug
|
||||||
from utils import get_tags, convert_id3_timestamp_to_datetime, id3_remap
|
from utils import get_tags, convert_id3_timestamp_to_datetime, id3_remap
|
||||||
@ -8,7 +6,7 @@ from pathlib import Path
|
|||||||
from appdirs import user_config_dir
|
from appdirs import user_config_dir
|
||||||
|
|
||||||
|
|
||||||
def add_files_to_database(files, progress_callback=None):
|
def add_files_to_database(files: list[str], playlist_id: int | None = None, progress_callback=None) -> tuple[bool, dict[str, str]]:
|
||||||
"""
|
"""
|
||||||
Adds audio file(s) to the sqllite db "song" table
|
Adds audio file(s) to the sqllite db "song" table
|
||||||
Args:
|
Args:
|
||||||
@ -21,26 +19,40 @@ def add_files_to_database(files, progress_callback=None):
|
|||||||
(True, {"filename.mp3":"failed because i said so"})
|
(True, {"filename.mp3":"failed because i said so"})
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
# yea
|
||||||
|
if playlist_id:
|
||||||
|
pass
|
||||||
|
|
||||||
config = ConfigParser()
|
config = ConfigParser()
|
||||||
cfg_file = (
|
cfg_file = (
|
||||||
Path(user_config_dir(appname="musicpom", appauthor="billypom")) / "config.ini"
|
Path(user_config_dir(appname="musicpom", appauthor="billypom")) / "config.ini"
|
||||||
)
|
)
|
||||||
config.read(cfg_file)
|
_ = config.read(cfg_file)
|
||||||
if not files:
|
if not files:
|
||||||
return False, {"Failure": "All operations failed in add_files_to_database()"}
|
return False, {"Failure": "All operations failed in add_files_to_database()"}
|
||||||
failed_dict = {}
|
failed_dict: dict[str, str] = {}
|
||||||
insert_data = [] # To store data for batch insert
|
insert_data: list[tuple[
|
||||||
|
str,
|
||||||
|
str | int | None,
|
||||||
|
str | int | None,
|
||||||
|
str | int | None,
|
||||||
|
str | int | None,
|
||||||
|
str | int | None,
|
||||||
|
str,
|
||||||
|
str | int | None,
|
||||||
|
str | int | None,
|
||||||
|
str | int | None]] = [] # To store data for batch insert
|
||||||
for filepath in files:
|
for filepath in files:
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
progress_callback.emit(filepath)
|
progress_callback.emit(filepath)
|
||||||
filename = filepath.split("/")[-1]
|
filename = filepath.split("/")[-1]
|
||||||
|
tags, fail_reason = get_tags(filepath)
|
||||||
tags, details = get_tags(filepath)
|
if fail_reason:
|
||||||
if details:
|
# if we fail to get audio tags, skip to next song
|
||||||
failed_dict[filepath] = details
|
failed_dict[filepath] = fail_reason
|
||||||
continue
|
continue
|
||||||
audio = id3_remap(tags)
|
# remap tags from ID3 to database tags
|
||||||
|
audio: dict[str, str | int | None] = id3_remap(tags)
|
||||||
# Append data tuple to insert_data list
|
# Append data tuple to insert_data list
|
||||||
insert_data.append(
|
insert_data.append(
|
||||||
(
|
(
|
||||||
@ -60,21 +72,22 @@ def add_files_to_database(files, progress_callback=None):
|
|||||||
if len(insert_data) >= 1000:
|
if len(insert_data) >= 1000:
|
||||||
debug(f"inserting a LOT of songs: {len(insert_data)}")
|
debug(f"inserting a LOT of songs: {len(insert_data)}")
|
||||||
with DBA.DBAccess() as db:
|
with DBA.DBAccess() as db:
|
||||||
db.executemany(
|
result = db.executemany(
|
||||||
"INSERT OR IGNORE INTO song (filepath, title, album, artist, track_number, genre, codec, album_date, bitrate, length_seconds) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
"INSERT OR IGNORE INTO song (filepath, title, album, artist, track_number, genre, codec, album_date, bitrate, length_seconds) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
|
||||||
insert_data,
|
insert_data,
|
||||||
)
|
)
|
||||||
|
debug('IMPORTANT')
|
||||||
|
debug(f'batch insert result: {result}')
|
||||||
|
debug('IMPORTANT')
|
||||||
insert_data = [] # Reset the insert_data list
|
insert_data = [] # Reset the insert_data list
|
||||||
else:
|
# Insert any remaining data after reading every file
|
||||||
# continue adding files if we havent reached big length
|
|
||||||
continue
|
|
||||||
# Insert any remaining data
|
|
||||||
debug("i check for insert data")
|
|
||||||
if insert_data:
|
if insert_data:
|
||||||
debug(f"inserting some songs: {len(insert_data)}")
|
|
||||||
with DBA.DBAccess() as db:
|
with DBA.DBAccess() as db:
|
||||||
db.executemany(
|
result = db.executemany(
|
||||||
"INSERT OR IGNORE INTO song (filepath, title, album, artist, track_number, genre, codec, album_date, bitrate, length_seconds) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
"INSERT OR IGNORE INTO song (filepath, title, album, artist, track_number, genre, codec, album_date, bitrate, length_seconds) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
|
||||||
insert_data,
|
insert_data,
|
||||||
)
|
)
|
||||||
|
debug('IMPORTANT')
|
||||||
|
debug(f'batch insert result: {result}')
|
||||||
|
debug('IMPORTANT')
|
||||||
return True, failed_dict
|
return True, failed_dict
|
||||||
|
|||||||
@ -34,7 +34,7 @@ def get_mp3_tags(filename: str) -> tuple[MP3 | ID3 | FLAC, str]:
|
|||||||
return MP3(), f"Could not assign ID3 tag to file: {e}"
|
return MP3(), f"Could not assign ID3 tag to file: {e}"
|
||||||
|
|
||||||
|
|
||||||
def id3_remap(audio: MP3 | ID3 | FLAC) -> dict:
|
def id3_remap(audio: MP3 | ID3 | FLAC) -> dict[str, str | int | None]:
|
||||||
"""
|
"""
|
||||||
Turns the ID3 dict of an audio file into a normal dict that I, the human, can use.
|
Turns the ID3 dict of an audio file into a normal dict that I, the human, can use.
|
||||||
Add extra fields too :D yahooo
|
Add extra fields too :D yahooo
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user