multithreading working with add files to library
This commit is contained in:
parent
3bd2c539cf
commit
d351f3515e
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
|||||||
/references
|
/references
|
||||||
/db
|
/db
|
||||||
config.ini
|
config.ini
|
||||||
|
log
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
@ -31,6 +31,7 @@ from components.AddToPlaylistWindow import AddToPlaylistWindow
|
|||||||
from components.MetadataWindow import MetadataWindow
|
from components.MetadataWindow import MetadataWindow
|
||||||
|
|
||||||
# from main import WorkerThread
|
# from main import WorkerThread
|
||||||
|
from main import Worker
|
||||||
from utils.delete_song_id_from_database import delete_song_id_from_database
|
from utils.delete_song_id_from_database import delete_song_id_from_database
|
||||||
from utils.add_files_to_library import add_files_to_library
|
from utils.add_files_to_library import add_files_to_library
|
||||||
from utils.get_reorganize_vars import get_reorganize_vars
|
from utils.get_reorganize_vars import get_reorganize_vars
|
||||||
@ -45,29 +46,6 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
class DropAddFilesThread(QThread):
|
|
||||||
signalStarted = pyqtSignal()
|
|
||||||
signalProgress = pyqtSignal(str)
|
|
||||||
signalFinished = pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self, files, parent=None):
|
|
||||||
QThread.__init__(self, parent)
|
|
||||||
self.files = files
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
self.add_files()
|
|
||||||
return
|
|
||||||
|
|
||||||
def add_files(self) -> None:
|
|
||||||
"""When song(s) added to the library, update the tableview model
|
|
||||||
- Drag & Drop song(s) on tableView
|
|
||||||
- File > Open > List of song(s)
|
|
||||||
"""
|
|
||||||
add_files_to_library(self.files)
|
|
||||||
self.signalFinished.emit()
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
class MusicTable(QTableView):
|
class MusicTable(QTableView):
|
||||||
playPauseSignal = pyqtSignal()
|
playPauseSignal = pyqtSignal()
|
||||||
enterKey = pyqtSignal()
|
enterKey = pyqtSignal()
|
||||||
@ -191,7 +169,7 @@ class MusicTable(QTableView):
|
|||||||
try:
|
try:
|
||||||
model.removeRow(index)
|
model.removeRow(index)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.info(f"MusicTable.py delete_songs() failed | {e}")
|
logging.info(f" delete_songs() failed | {e}")
|
||||||
self.load_music_table()
|
self.load_music_table()
|
||||||
self.model.dataChanged.connect(self.on_cell_data_changed)
|
self.model.dataChanged.connect(self.on_cell_data_changed)
|
||||||
|
|
||||||
@ -237,7 +215,7 @@ class MusicTable(QTableView):
|
|||||||
else:
|
else:
|
||||||
raise RuntimeError("No USLT tags found in song metadata")
|
raise RuntimeError("No USLT tags found in song metadata")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"MusicTable.py | show_lyrics_menu | could not retrieve lyrics | {e}")
|
logging.error(f"show_lyrics_menu() | could not retrieve lyrics | {e}")
|
||||||
lyrics = ""
|
lyrics = ""
|
||||||
lyrics_window = LyricsWindow(selected_song_filepath, lyrics)
|
lyrics_window = LyricsWindow(selected_song_filepath, lyrics)
|
||||||
lyrics_window.exec_()
|
lyrics_window.exec_()
|
||||||
@ -261,6 +239,7 @@ class MusicTable(QTableView):
|
|||||||
e.ignore()
|
e.ignore()
|
||||||
|
|
||||||
def dropEvent(self, e: QDropEvent | None):
|
def dropEvent(self, e: QDropEvent | None):
|
||||||
|
self.model.dataChanged.disconnect(self.on_cell_data_changed)
|
||||||
if e is None:
|
if e is None:
|
||||||
return
|
return
|
||||||
data = e.mimeData()
|
data = e.mimeData()
|
||||||
@ -270,14 +249,13 @@ class MusicTable(QTableView):
|
|||||||
if url.isLocalFile():
|
if url.isLocalFile():
|
||||||
files.append(url.path())
|
files.append(url.path())
|
||||||
e.accept()
|
e.accept()
|
||||||
self.worker = DropAddFilesThread(files=files)
|
worker = Worker(add_files_to_library, files)
|
||||||
# self.model.dataChanged.disconnect(self.on_cell_data_changed)
|
worker.signals.signal_progress.connect(self.handle_progress)
|
||||||
# self.model.dataChanged.connect(self.on_cell_data_changed)
|
worker.signals.signal_finished.connect(self.load_music_table)
|
||||||
self.worker.signalFinished.connect(self.load_music_table)
|
self.threadpool.start(worker)
|
||||||
self.worker.start()
|
|
||||||
# self.add_files(files)
|
|
||||||
else:
|
else:
|
||||||
e.ignore()
|
e.ignore()
|
||||||
|
self.model.dataChanged.connect(self.on_cell_data_changed)
|
||||||
|
|
||||||
def keyPressEvent(self, e):
|
def keyPressEvent(self, e):
|
||||||
"""Press a key. Do a thing"""
|
"""Press a key. Do a thing"""
|
||||||
@ -315,7 +293,7 @@ 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")
|
logging.info("on_cell_data_changed")
|
||||||
id_index = self.model.index(topLeft.row(), 0) # ID is column 0, always
|
id_index = self.model.index(topLeft.row(), 0) # ID is column 0, always
|
||||||
song_id = self.model.data(id_index, Qt.UserRole)
|
song_id = self.model.data(id_index, Qt.UserRole)
|
||||||
# filepath is always the last column
|
# filepath is always the last column
|
||||||
@ -326,7 +304,7 @@ class MusicTable(QTableView):
|
|||||||
# 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()]
|
||||||
print(f"edited column name: {edited_column_name}")
|
logging.info(f"edited column name: {edited_column_name}")
|
||||||
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 the library with new metadata
|
# Update the library with new metadata
|
||||||
@ -366,13 +344,12 @@ class MusicTable(QTableView):
|
|||||||
"UPDATE song SET filepath = ? WHERE filepath = ?",
|
"UPDATE song SET filepath = ? WHERE filepath = ?",
|
||||||
(new_path, filepath),
|
(new_path, filepath),
|
||||||
)
|
)
|
||||||
print(f"Moved: {filepath} -> {new_path}")
|
logging.info(
|
||||||
|
f"reorganize_selected_files() | Moved: {filepath} -> {new_path}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f"MusicTable.py reorganize_selected_files() | Error moving file: {filepath} | {e}"
|
f"reorganize_selected_files() | Error moving file: {filepath} | {e}"
|
||||||
)
|
|
||||||
print(
|
|
||||||
f"MusicTable.py reorganize_selected_files() | Error moving file: {filepath} | {e}"
|
|
||||||
)
|
)
|
||||||
# Draw the rest of the owl
|
# Draw the rest of the owl
|
||||||
# self.model.dataChanged.disconnect(self.on_cell_data_changed)
|
# self.model.dataChanged.disconnect(self.on_cell_data_changed)
|
||||||
@ -388,6 +365,14 @@ class MusicTable(QTableView):
|
|||||||
self.set_current_song_filepath()
|
self.set_current_song_filepath()
|
||||||
self.playPauseSignal.emit()
|
self.playPauseSignal.emit()
|
||||||
|
|
||||||
|
def add_files(self, files, progress_callback) -> None:
|
||||||
|
"""When song(s) added to the library, update the tableview model
|
||||||
|
- Drag & Drop song(s) on tableView
|
||||||
|
- File > Open > List of song(s)
|
||||||
|
"""
|
||||||
|
add_files_to_library(files, progress_callback)
|
||||||
|
return
|
||||||
|
|
||||||
def load_music_table(self, *playlist_id):
|
def load_music_table(self, *playlist_id):
|
||||||
"""
|
"""
|
||||||
Loads data into self (QTableView)
|
Loads data into self (QTableView)
|
||||||
@ -401,8 +386,8 @@ class MusicTable(QTableView):
|
|||||||
# then re-enable after we are done loading
|
# then re-enable after we are done loading
|
||||||
self.model.dataChanged.disconnect(self.on_cell_data_changed)
|
self.model.dataChanged.disconnect(self.on_cell_data_changed)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
logging.info(
|
||||||
f"MusicTable.py load_music_table() | could not disconnect on_cell_data_changed trigger: {e}"
|
f"load_music_table() | could not disconnect on_cell_data_changed trigger: {e}"
|
||||||
)
|
)
|
||||||
pass
|
pass
|
||||||
self.vertical_scroll_position = (
|
self.vertical_scroll_position = (
|
||||||
@ -413,7 +398,9 @@ class MusicTable(QTableView):
|
|||||||
self.model.setHorizontalHeaderLabels(self.table_headers)
|
self.model.setHorizontalHeaderLabels(self.table_headers)
|
||||||
if playlist_id:
|
if playlist_id:
|
||||||
selected_playlist_id = playlist_id[0]
|
selected_playlist_id = playlist_id[0]
|
||||||
print(f"selected_playlist_id: {selected_playlist_id}")
|
logging.info(
|
||||||
|
f"load_music_table() | selected_playlist_id: {selected_playlist_id}"
|
||||||
|
)
|
||||||
# Fetch playlist data
|
# Fetch playlist data
|
||||||
try:
|
try:
|
||||||
with DBA.DBAccess() as db:
|
with DBA.DBAccess() as db:
|
||||||
@ -422,10 +409,7 @@ class MusicTable(QTableView):
|
|||||||
(selected_playlist_id,),
|
(selected_playlist_id,),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(
|
logging.warning(f"load_music_table() | Unhandled exception: {e}")
|
||||||
f"MusicTable.py | load_music_table | Unhandled exception: {e}"
|
|
||||||
)
|
|
||||||
print(f"MusicTable.py | load_music_table | Unhandled exception: {e}")
|
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
# Fetch library data
|
# Fetch library data
|
||||||
@ -436,9 +420,7 @@ class MusicTable(QTableView):
|
|||||||
(),
|
(),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(
|
logging.warning(f"load_music_table() | Unhandled exception: {e}")
|
||||||
f"MusicTable.py | load_music_table | Unhandled exception: {e}"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
# Populate the model
|
# Populate the model
|
||||||
for row_data in data:
|
for row_data in data:
|
||||||
@ -458,7 +440,7 @@ class MusicTable(QTableView):
|
|||||||
|
|
||||||
def restore_scroll_position(self) -> None:
|
def restore_scroll_position(self) -> None:
|
||||||
"""Restores the scroll position"""
|
"""Restores the scroll position"""
|
||||||
print("restore_scroll_position")
|
logging.info("restore_scroll_position")
|
||||||
QTimer.singleShot(
|
QTimer.singleShot(
|
||||||
100,
|
100,
|
||||||
lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position),
|
lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position),
|
||||||
@ -520,7 +502,7 @@ class MusicTable(QTableView):
|
|||||||
self.selected_song_filepath = (
|
self.selected_song_filepath = (
|
||||||
self.currentIndex().siblingAtColumn(self.table_headers.index("path")).data()
|
self.currentIndex().siblingAtColumn(self.table_headers.index("path")).data()
|
||||||
)
|
)
|
||||||
print(self.selected_song_filepath)
|
logging.info(self.selected_song_filepath)
|
||||||
|
|
||||||
def set_current_song_filepath(self) -> None:
|
def set_current_song_filepath(self) -> None:
|
||||||
"""Sets the filepath of the currently playing song"""
|
"""Sets the filepath of the currently playing song"""
|
||||||
|
|||||||
140
main.py
140
main.py
@ -9,6 +9,7 @@ from pyqtgraph import mkBrush
|
|||||||
from mutagen.id3 import ID3
|
from mutagen.id3 import ID3
|
||||||
from mutagen.id3._frames import APIC
|
from mutagen.id3._frames import APIC
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
|
import traceback
|
||||||
import DBA
|
import DBA
|
||||||
from ui import Ui_MainWindow
|
from ui import Ui_MainWindow
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
@ -28,6 +29,7 @@ from PyQt5.QtCore import (
|
|||||||
Qt,
|
Qt,
|
||||||
pyqtSignal,
|
pyqtSignal,
|
||||||
QObject,
|
QObject,
|
||||||
|
QThread,
|
||||||
pyqtSlot,
|
pyqtSlot,
|
||||||
QThreadPool,
|
QThreadPool,
|
||||||
QRunnable,
|
QRunnable,
|
||||||
@ -35,7 +37,12 @@ from PyQt5.QtCore import (
|
|||||||
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe
|
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe
|
||||||
from PyQt5.QtGui import QCloseEvent, QPixmap
|
from PyQt5.QtGui import QCloseEvent, QPixmap
|
||||||
from uuid import uuid4, UUID
|
from uuid import uuid4, UUID
|
||||||
from utils import scan_for_music, delete_and_create_library_database, initialize_db
|
from utils import (
|
||||||
|
scan_for_music,
|
||||||
|
delete_and_create_library_database,
|
||||||
|
initialize_db,
|
||||||
|
add_files_to_library,
|
||||||
|
)
|
||||||
from components import (
|
from components import (
|
||||||
PreferencesWindow,
|
PreferencesWindow,
|
||||||
AudioVisualizer,
|
AudioVisualizer,
|
||||||
@ -49,6 +56,11 @@ from components import (
|
|||||||
|
|
||||||
# good help with signals slots in threads
|
# good help with signals slots in threads
|
||||||
# https://stackoverflow.com/questions/52993677/how-do-i-setup-signals-and-slots-in-pyqt-with-qthreads-in-both-directions
|
# https://stackoverflow.com/questions/52993677/how-do-i-setup-signals-and-slots-in-pyqt-with-qthreads-in-both-directions
|
||||||
|
|
||||||
|
# GOOD
|
||||||
|
# https://www.pythonguis.com/tutorials/multithreading-pyqt-applications-qthreadpool/
|
||||||
|
|
||||||
|
|
||||||
class WorkerSignals(QObject):
|
class WorkerSignals(QObject):
|
||||||
"""
|
"""
|
||||||
How to use signals for a QRunnable class; unlike most cases where signals
|
How to use signals for a QRunnable class; unlike most cases where signals
|
||||||
@ -67,31 +79,52 @@ class WorkerSignals(QObject):
|
|||||||
# pandas DataFrames or numpy arrays. Instead, pass the minimum amount of information needed
|
# pandas DataFrames or numpy arrays. Instead, pass the minimum amount of information needed
|
||||||
# (i.e. lists of filepaths)
|
# (i.e. lists of filepaths)
|
||||||
|
|
||||||
signal_started = pyqtSignal(list)
|
signal_started = pyqtSignal()
|
||||||
signal_finished = pyqtSignal(UUID)
|
signal_finished = pyqtSignal()
|
||||||
signal_progress = pyqtSignal(str)
|
signal_progress = pyqtSignal(str)
|
||||||
|
|
||||||
|
|
||||||
class WorkerThread(QRunnable):
|
class Worker(QRunnable):
|
||||||
"""
|
"""
|
||||||
This is the thread that is going to do the work so that the
|
This is the thread that is going to do the work so that the
|
||||||
application doesn't freeze
|
application doesn't freeze
|
||||||
|
|
||||||
|
Inherits from QRunnable to handle worker thread setup, signals, and tear down
|
||||||
|
:param callback: the function callback to run on this worker thread. Supplied
|
||||||
|
arg and kwargs will be passed through to the runner
|
||||||
|
:type callback: function
|
||||||
|
:param args: Arguments to pass to the callback function
|
||||||
|
:param kwargs: Keywords to pass to the callback function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, worker_id: UUID) -> None:
|
def __init__(self, fn, *args, **kwargs):
|
||||||
super().__init__()
|
super(Worker, self).__init__()
|
||||||
# allow for signals to be used
|
# Store constructor arguments (re-used for processing)
|
||||||
self.worker_id = worker_id
|
self.fn = fn
|
||||||
|
self.args = args
|
||||||
|
self.kwargs = kwargs
|
||||||
self.signals: WorkerSignals = WorkerSignals()
|
self.signals: WorkerSignals = WorkerSignals()
|
||||||
|
|
||||||
|
# Add a callback to our kwargs
|
||||||
|
self.kwargs["progress_callback"] = self.signals.signal_progress
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
def run(self):
|
def run(self):
|
||||||
"""
|
"""
|
||||||
This is where the work is done.
|
This is where the work is done.
|
||||||
MUST be called run() in order for QRunnable to work
|
MUST be called run() in order for QRunnable to work
|
||||||
|
|
||||||
|
Initialize the runner function with passed args & kwargs
|
||||||
"""
|
"""
|
||||||
self.signals.signal_started.emit("start worker")
|
self.signals.signal_started.emit()
|
||||||
self.signals.signal_progress.emit("worker progress")
|
try:
|
||||||
self.signals.signal_finished.emit(self.worker_id)
|
self.fn(*self.args, **self.kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
exctype, value = sys.exc_info()[:2]
|
||||||
|
self.signals.signal_finished.emit((exctype, value, traceback.format_exc()))
|
||||||
|
finally:
|
||||||
|
self.signals.signal_finished.emit()
|
||||||
|
|
||||||
|
|
||||||
class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||||
@ -122,7 +155,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
self.audio_visualizer: AudioVisualizer = AudioVisualizer(self.player)
|
self.audio_visualizer: AudioVisualizer = AudioVisualizer(self.player)
|
||||||
self.current_volume: int = 50
|
self.current_volume: int = 50
|
||||||
self.qapp = qapp
|
self.qapp = qapp
|
||||||
# print(f'ApplicationWindow self.qapp: {self.qapp}')
|
|
||||||
self.tableView.load_qapp(self.qapp)
|
self.tableView.load_qapp(self.qapp)
|
||||||
self.albumGraphicsView.load_qapp(self.qapp)
|
self.albumGraphicsView.load_qapp(self.qapp)
|
||||||
self.config.read("config.ini")
|
self.config.read("config.ini")
|
||||||
@ -154,7 +186,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
self.playbackSlider.sliderReleased.connect(
|
self.playbackSlider.sliderReleased.connect(
|
||||||
lambda: self.player.setPosition(self.playbackSlider.value())
|
lambda: self.player.setPosition(self.playbackSlider.value())
|
||||||
) # maybe sliderReleased works better than sliderMoved
|
) # maybe sliderReleased works better than sliderMoved
|
||||||
self
|
|
||||||
self.volumeSlider.sliderMoved[int].connect(
|
self.volumeSlider.sliderMoved[int].connect(
|
||||||
lambda: self.volume_changed()
|
lambda: self.volume_changed()
|
||||||
) # Move slider to adjust volume
|
) # Move slider to adjust volume
|
||||||
@ -213,45 +244,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||||
self.tableView.horizontalHeader().setStretchLastSection(False)
|
self.tableView.horizontalHeader().setStretchLastSection(False)
|
||||||
|
|
||||||
def start_worker(self):
|
|
||||||
worker_id = uuid4() # generate an unique id for the worker
|
|
||||||
worker = WorkerThread(worker_id=worker_id)
|
|
||||||
|
|
||||||
# Connect the signals to the slots
|
|
||||||
worker.signals.signal_started.connect(self.handle_started)
|
|
||||||
worker.signals.signal_progress.connect(self.handle_progress)
|
|
||||||
worker.signals.signal_finished.connect(self.handle_finished)
|
|
||||||
|
|
||||||
# Store the worker in a dict so we can keep track of it
|
|
||||||
self.workers[worker_id] = worker
|
|
||||||
|
|
||||||
# Start the worker
|
|
||||||
self.threadpool.start(worker)
|
|
||||||
|
|
||||||
# Slots should have decorators whose arguments match the signals they are connected to
|
|
||||||
@pyqtSlot(list)
|
|
||||||
def handle_files_from_child_window(self, files: list[str]):
|
|
||||||
self.files_to_process = files
|
|
||||||
print(f"Files to process: {self.files_to_process}")
|
|
||||||
self.show_status_bar_message(f"Files to process: {self.files_to_process}")
|
|
||||||
|
|
||||||
@pyqtSlot(int)
|
|
||||||
def handle_progress(self, n):
|
|
||||||
print(f"progress: {n}%")
|
|
||||||
self.show_status_bar_message(f"progress: {n}%")
|
|
||||||
|
|
||||||
@pyqtSlot(UUID)
|
|
||||||
def handle_finished(self, worker_id):
|
|
||||||
print(f"worker {worker_id} finished")
|
|
||||||
self.show_status_bar_message(f"worker {worker_id} finished")
|
|
||||||
# remove the worker from the dict
|
|
||||||
self.workers.pop(worker_id)
|
|
||||||
assert worker_id not in self.workers
|
|
||||||
|
|
||||||
@pyqtSlot(list)
|
|
||||||
def handle_started(self, files: list[str]):
|
|
||||||
print(f"Started processing files: {files}")
|
|
||||||
|
|
||||||
def closeEvent(self, a0: QCloseEvent | None) -> None:
|
def closeEvent(self, a0: QCloseEvent | None) -> None:
|
||||||
"""Save settings when closing the application"""
|
"""Save settings when closing the application"""
|
||||||
# MusicTable/tableView column widths
|
# MusicTable/tableView column widths
|
||||||
@ -355,7 +347,9 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
"""Sets the ID3 tag APIC (album art) for all selected song filepaths"""
|
"""Sets the ID3 tag APIC (album art) for all selected song filepaths"""
|
||||||
selected_songs = self.tableView.get_selected_songs_filepaths()
|
selected_songs = self.tableView.get_selected_songs_filepaths()
|
||||||
for song in selected_songs:
|
for song in selected_songs:
|
||||||
print(f"updating album art for {song}")
|
logging.info(
|
||||||
|
f"main.py set_album_art_for_selected_songs() | updating album art for {song}"
|
||||||
|
)
|
||||||
self.update_album_art_for_song(song, album_art_path)
|
self.update_album_art_for_song(song, album_art_path)
|
||||||
|
|
||||||
def update_album_art_for_song(
|
def update_album_art_for_song(
|
||||||
@ -401,7 +395,9 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
del audio["APIC"]
|
del audio["APIC"]
|
||||||
audio.save()
|
audio.save()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error processing {file}: {e}")
|
logging.error(
|
||||||
|
f"main.py delete_album_art_for_selected_songs() | Error processing {file}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
def update_audio_visualization(self) -> None:
|
def update_audio_visualization(self) -> None:
|
||||||
"""Handles upading points on the pyqtgraph visual"""
|
"""Handles upading points on the pyqtgraph visual"""
|
||||||
@ -445,7 +441,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
self.current_volume = self.volumeSlider.value()
|
self.current_volume = self.volumeSlider.value()
|
||||||
self.player.setVolume(self.current_volume)
|
self.player.setVolume(self.current_volume)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Changing volume error: {e}")
|
logging.error(f"main.py volume_changed() | Changing volume error: {e}")
|
||||||
|
|
||||||
def on_play_clicked(self) -> None:
|
def on_play_clicked(self) -> None:
|
||||||
"""Updates the Play & Pause buttons when clicked"""
|
"""Updates the Play & Pause buttons when clicked"""
|
||||||
@ -462,17 +458,20 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
def on_previous_clicked(self) -> None:
|
def on_previous_clicked(self) -> None:
|
||||||
""""""
|
""""""
|
||||||
print("previous")
|
logging.info("main.py on_previous_clicked()")
|
||||||
|
|
||||||
def on_next_clicked(self) -> None:
|
def on_next_clicked(self) -> None:
|
||||||
print("next")
|
logging.info("main.py on_next_clicked()")
|
||||||
|
|
||||||
def add_latest_playlist_to_tree(self) -> None:
|
def add_latest_playlist_to_tree(self) -> None:
|
||||||
"""Refreshes the playlist tree"""
|
"""Refreshes the playlist tree"""
|
||||||
self.playlistTreeView.add_latest_playlist_to_tree()
|
self.playlistTreeView.add_latest_playlist_to_tree()
|
||||||
|
|
||||||
def open_files(self) -> None:
|
def open_files(self) -> None:
|
||||||
"""Opens the open files window"""
|
"""
|
||||||
|
Opens the open files window
|
||||||
|
- File > Open > List of song(s)
|
||||||
|
"""
|
||||||
open_files_window = QFileDialog(
|
open_files_window = QFileDialog(
|
||||||
self, "Open file(s)", ".", "Audio files (*.mp3)"
|
self, "Open file(s)", ".", "Audio files (*.mp3)"
|
||||||
)
|
)
|
||||||
@ -480,7 +479,17 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
open_files_window.setFileMode(QFileDialog.ExistingFiles)
|
open_files_window.setFileMode(QFileDialog.ExistingFiles)
|
||||||
open_files_window.exec_()
|
open_files_window.exec_()
|
||||||
filenames = open_files_window.selectedFiles()
|
filenames = open_files_window.selectedFiles()
|
||||||
self.tableView.add_files(filenames)
|
# Adds files to the library in a new thread
|
||||||
|
worker = Worker(add_files_to_library, filenames)
|
||||||
|
worker.signals.signal_progress.connect(self.handle_progress)
|
||||||
|
worker.signals.signal_finished.connect(self.tableView.load_music_table)
|
||||||
|
self.threadpool.start(worker)
|
||||||
|
|
||||||
|
def handle_progress(self, data):
|
||||||
|
"""
|
||||||
|
updates the status bar when progress is emitted
|
||||||
|
"""
|
||||||
|
self.show_status_bar_message(data)
|
||||||
|
|
||||||
# File
|
# File
|
||||||
|
|
||||||
@ -584,16 +593,23 @@ if __name__ == "__main__":
|
|||||||
with open("utils/init.sql", "r") as file:
|
with open("utils/init.sql", "r") as file:
|
||||||
lines = file.read()
|
lines = file.read()
|
||||||
for statement in lines.split(";"):
|
for statement in lines.split(";"):
|
||||||
print(f"executing [{statement}]")
|
logging.info(f"executing [{statement}]")
|
||||||
db.execute(statement, ())
|
db.execute(statement, ())
|
||||||
# logging setup
|
# logging setup
|
||||||
logging.basicConfig(filename="musicpom.log", encoding="utf-8", level=logging.DEBUG)
|
file_handler = logging.FileHandler(filename="log", encoding="utf-8")
|
||||||
|
stdout_handler = logging.StreamHandler(stream=sys.stdout)
|
||||||
|
handlers = [file_handler, stdout_handler]
|
||||||
|
# logging.basicConfig(filename="log", encoding="utf-8", level=logging.DEBUG)
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.DEBUG,
|
||||||
|
format="[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s",
|
||||||
|
handlers=handlers,
|
||||||
|
)
|
||||||
# Allow for dynamic imports of my custom classes and utilities
|
# Allow for dynamic imports of my custom classes and utilities
|
||||||
project_root = os.path.abspath(os.path.dirname(__file__))
|
project_root = os.path.abspath(os.path.dirname(__file__))
|
||||||
sys.path.append(project_root)
|
sys.path.append(project_root)
|
||||||
# Start the app
|
# Start the app
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
# print(f"main.py app: {app}")
|
|
||||||
# Dark theme >:3
|
# Dark theme >:3
|
||||||
qdarktheme.setup_theme()
|
qdarktheme.setup_theme()
|
||||||
# Show the UI
|
# Show the UI
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import sys
|
import sys
|
||||||
from mutagen.id3 import ID3, APIC
|
from mutagen.id3 import ID3, APIC
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
def print_id3_tags(file_path):
|
def print_id3_tags(file_path):
|
||||||
|
|||||||
@ -8,19 +8,20 @@ config = ConfigParser()
|
|||||||
config.read("config.ini")
|
config.read("config.ini")
|
||||||
|
|
||||||
|
|
||||||
def add_files_to_library(files):
|
def add_files_to_library(files, progress_callback):
|
||||||
"""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 a list of dictionaries of metadata
|
Returns a list of dictionaries of metadata
|
||||||
"""
|
"""
|
||||||
|
logging.info("started function")
|
||||||
# print("Running add_files_to_library.py")
|
|
||||||
if not files:
|
if not files:
|
||||||
return []
|
return []
|
||||||
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
|
||||||
for filepath in files:
|
for filepath in files:
|
||||||
if any(filepath.lower().endswith(ext) for ext in extensions):
|
if any(filepath.lower().endswith(ext) for ext in extensions):
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback.emit(filepath)
|
||||||
filename = filepath.split("/")[-1]
|
filename = filepath.split("/")[-1]
|
||||||
audio = get_id3_tags(filepath)
|
audio = get_id3_tags(filepath)
|
||||||
|
|
||||||
|
|||||||
@ -1,40 +1,11 @@
|
|||||||
# from mutagen.id3 import ID3
|
|
||||||
# from mutagen.id3._frames import TIT2
|
|
||||||
# import os
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# def get_id3_tags(filename):
|
|
||||||
# """Get the ID3 tags for an audio file
|
|
||||||
#
|
|
||||||
# # Parameters
|
|
||||||
# `file` | str | Fully qualified path to file
|
|
||||||
#
|
|
||||||
# # Returns
|
|
||||||
# dict of all id3 tags
|
|
||||||
# at minimum we will get the filename as a title.[ID3:TIT2]
|
|
||||||
# """
|
|
||||||
#
|
|
||||||
# # Check if all tags are empty
|
|
||||||
# # tags_are_empty = all(not values for values in audio.values())
|
|
||||||
# audio = ID3()
|
|
||||||
# try:
|
|
||||||
# if not audio["TIT2"] or audio["TIT2"].text[0] == "":
|
|
||||||
# title = os.path.splitext(os.path.basename(filename))[0]
|
|
||||||
# frame = TIT2(encoding=3, text=[title])
|
|
||||||
# audio["TIT2"] = frame
|
|
||||||
# audio.save() # type: ignore
|
|
||||||
# except Exception as e:
|
|
||||||
# print(f"get_id3_tags.py | Could not assign file ID3 tag: {e}")
|
|
||||||
# return audio
|
|
||||||
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
from mutagen.id3 import ID3, TIT2, ID3NoHeaderError
|
from mutagen.id3 import ID3, TIT2, ID3NoHeaderError
|
||||||
|
|
||||||
|
|
||||||
def get_id3_tags(filename):
|
def get_id3_tags(filename):
|
||||||
"""Get the ID3 tags for an audio file"""
|
"""Get the ID3 tags for an audio file"""
|
||||||
print(f"get id3 tags filename: {filename}")
|
logging.info(f"filename: {filename}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Open the MP3 file and read its content
|
# Open the MP3 file and read its content
|
||||||
@ -59,6 +30,6 @@ def get_id3_tags(filename):
|
|||||||
audio.save()
|
audio.save()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"get_id3_tags.py | Could not assign file ID3 tag: {e}")
|
logging.error(f"Could not assign file ID3 tag: {e}")
|
||||||
|
|
||||||
return audio
|
return audio
|
||||||
|
|||||||
@ -92,9 +92,7 @@ def set_id3_tag(filepath: str, tag_name: str, value: str):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True / False"""
|
True / False"""
|
||||||
print(
|
logging.info(f"filepath: {filepath} | tag_name: {tag_name} | value: {value}")
|
||||||
f"set_id3_tag.py | filepath: {filepath} | tag_name: {tag_name} | value: {value}"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try: # Load existing tags
|
try: # Load existing tags
|
||||||
@ -114,9 +112,7 @@ def set_id3_tag(filepath: str, tag_name: str, value: str):
|
|||||||
try:
|
try:
|
||||||
audio = ID3(filepath)
|
audio = ID3(filepath)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.debug(
|
logging.error(f"ran into an exception: {e}")
|
||||||
f"set_id3_tag.py set_id3_tag() ran into an exception: {e}"
|
|
||||||
)
|
|
||||||
audio = ID3()
|
audio = ID3()
|
||||||
audio.delall("USLT")
|
audio.delall("USLT")
|
||||||
frame = USLT(encoding=3, text=value)
|
frame = USLT(encoding=3, text=value)
|
||||||
@ -128,7 +124,7 @@ def set_id3_tag(filepath: str, tag_name: str, value: str):
|
|||||||
tag_name = id3_tag_mapping[tag_name]
|
tag_name = id3_tag_mapping[tag_name]
|
||||||
# Other
|
# Other
|
||||||
if tag_name in mutagen_id3_tag_mapping: # Tag accounted for
|
if tag_name in mutagen_id3_tag_mapping: # Tag accounted for
|
||||||
print(f"set_id3_tag.py | tag_name = {tag_name}")
|
logging.info(f"tag_name = {tag_name}")
|
||||||
tag_class = mutagen_id3_tag_mapping[tag_name]
|
tag_class = mutagen_id3_tag_mapping[tag_name]
|
||||||
if issubclass(tag_class, Frame):
|
if issubclass(tag_class, Frame):
|
||||||
frame = tag_class(encoding=3, text=[value])
|
frame = tag_class(encoding=3, text=[value])
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user