trying to multithread

This commit is contained in:
tsi-billypom 2024-09-05 14:37:05 -04:00
parent cc21dd6403
commit 14b3014518
5 changed files with 143 additions and 23 deletions

View File

@ -103,7 +103,7 @@ class ExportPlaylistWindow(QDialog):
self.chosen_list_widget_item: QListWidgetItem | None = ( self.chosen_list_widget_item: QListWidgetItem | None = (
self.playlist_listWidget.currentItem() self.playlist_listWidget.currentItem()
) )
# We don't care if its None # We don't care if nothing is chosen
if self.chosen_list_widget_item is None: if self.chosen_list_widget_item is None:
return return
@ -146,6 +146,7 @@ class ExportPlaylistWindow(QDialog):
) )
error_dialog.exec() error_dialog.exec()
self.close() self.close()
return
# Gather playlist song paths # Gather playlist song paths
write_paths = [] write_paths = []
@ -167,7 +168,9 @@ class ExportPlaylistWindow(QDialog):
os.makedirs(os.path.dirname(filename), exist_ok=True) os.makedirs(os.path.dirname(filename), exist_ok=True)
with open(filename, "w") as f: with open(filename, "w") as f:
f.writelines(write_paths) f.writelines(write_paths)
self.close() self.close()
return
def cancel(self) -> None: def cancel(self) -> None:
self.close() self.close()

View File

@ -24,6 +24,7 @@ from components.AddToPlaylistWindow import AddToPlaylistWindow
from components.MetadataWindow import MetadataWindow from components.MetadataWindow import MetadataWindow
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.update_song_in_database import update_song_in_database from utils.update_song_in_database import update_song_in_database
from utils.get_id3_tags import get_id3_tags from utils.get_id3_tags import get_id3_tags
from utils.get_album_art import get_album_art from utils.get_album_art import get_album_art
@ -311,21 +312,7 @@ class MusicTable(QTableView):
for filepath in filepaths: for filepath in filepaths:
try: try:
# Read file metadata # Read file metadata
audio = ID3(filepath) artist, album = get_reorganize_vars(filepath)
try:
artist = audio["TPE1"].text[0]
if artist == "":
artist = "Unknown Artist"
except KeyError:
artist = "Unknown Artist"
try:
album = audio["TALB"].text[0]
if album == "":
album = "Unknown Album"
except KeyError:
album = "Unknown Album"
# Determine the new path that needs to be made # Determine the new path that needs to be made
new_path = os.path.join( new_path = os.path.join(
target_dir, artist, album, os.path.basename(filepath) target_dir, artist, album, os.path.basename(filepath)

141
main.py
View File

@ -13,16 +13,28 @@ import DBA
from ui import Ui_MainWindow from ui import Ui_MainWindow
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QFileDialog, QFileDialog,
QLabel,
QMainWindow, QMainWindow,
QApplication, QApplication,
QGraphicsScene, QGraphicsScene,
QHeaderView, QHeaderView,
QGraphicsPixmapItem, QGraphicsPixmapItem,
QMessageBox, QMessageBox,
QStatusBar,
)
from PyQt5.QtCore import (
QUrl,
QTimer,
Qt,
pyqtSignal,
QObject,
pyqtSlot,
QThreadPool,
QRunnable,
) )
from PyQt5.QtCore import QUrl, QTimer, Qt, pyqtSignal
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 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
from components import ( from components import (
PreferencesWindow, PreferencesWindow,
@ -35,6 +47,51 @@ from components import (
# pyuic5 ui.ui -o ui.py # pyuic5 ui.ui -o ui.py
class WorkerSignals(QObject):
"""
How to use signals for a QRunnable class; unlike most cases where signals
are defined as class attributes directly in the class, here we define a
class that inherits from QObject and define the signals as class
attributes in that class. Then we can instantiate that class and use it
as a signal object.
"""
# 1)
# Use a naming convention for signals that makes it clear that they are signals
# and a corresponding naming convention for the slots that handle them.
# For example signal_* and handle_*.
# 2)
# And try to make the signal content as small as possible. DO NOT pass large objects through signals, like
# pandas DataFrames or numpy arrays. Instead, pass the minimum amount of information needed
# (i.e. lists of filepaths)
signal_started = pyqtSignal(list)
signal_finished = pyqtSignal(UUID)
signal_progress = pyqtSignal(str)
class WorkerThread(QRunnable):
"""
This is the thread that is going to do the work so that the
application doesn't freeze
"""
def __init__(self, worker_id: UUID) -> None:
super().__init__()
# allow for signals to be used
self.worker_id = worker_id
self.signals: WorkerSignals = WorkerSignals()
def run(self):
"""
This is where the work is done.
MUST be called run() in order for QRunnable to work
"""
self.signals.signal_started.emit("start worker")
self.signals.signal_progress.emit("worker progress")
self.signals.signal_finished.emit(self.worker_id)
class ApplicationWindow(QMainWindow, Ui_MainWindow): class ApplicationWindow(QMainWindow, Ui_MainWindow):
playlistCreatedSignal = pyqtSignal() playlistCreatedSignal = pyqtSignal()
@ -42,8 +99,16 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
super(ApplicationWindow, self).__init__() super(ApplicationWindow, self).__init__()
global stopped global stopped
stopped = False stopped = False
# Multithreading stuff...
self.workers = dict[UUID, WorkerThread] = {}
self.threadpool = QThreadPool()
# UI
self.setupUi(self) self.setupUi(self)
self.setWindowTitle("MusicPom") self.setWindowTitle("MusicPom")
self.status_bar = QStatusBar()
self.permanent_status_label = QLabel("Status...")
self.status_bar.addPermanentWidget(self.permanent_status_label)
self.setStatusBar(self.status_bar)
self.selected_song_filepath: str | None = None self.selected_song_filepath: str | None = None
self.current_song_filepath: str | None = None self.current_song_filepath: str | None = None
self.current_song_metadata: ID3 | dict | None = None self.current_song_metadata: ID3 | dict | None = None
@ -146,6 +211,45 @@ 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
@ -160,6 +264,21 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.config.write(configfile) self.config.write(configfile)
super().closeEvent(a0) super().closeEvent(a0)
def show_status_bar_message(self, message: str, timeout: int | None = None) -> None:
"""
Show a `message` in the status bar for a length of time - `timeout` in ms
"""
if timeout:
self.status_bar.showMessage(message, timeout)
else:
self.status_bar.showMessage(message)
def set_permanent_status_bar_message(self, message: str) -> None:
"""
Sets the permanent message label in the status bar
"""
self.permanent_status_label.setText(message)
def play_audio_file(self) -> None: def play_audio_file(self) -> None:
"""Start playback of tableView.current_song_filepath track & moves playback slider""" """Start playback of tableView.current_song_filepath track & moves playback slider"""
self.current_song_metadata = ( self.current_song_metadata = (
@ -346,6 +465,10 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
def on_next_clicked(self) -> None: def on_next_clicked(self) -> None:
print("next") print("next")
def add_latest_playlist_to_tree(self) -> None:
"""Refreshes the playlist 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"""
open_files_window = QFileDialog( open_files_window = QFileDialog(
@ -357,16 +480,14 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
filenames = open_files_window.selectedFiles() filenames = open_files_window.selectedFiles()
self.tableView.add_files(filenames) self.tableView.add_files(filenames)
# File
def create_playlist(self) -> None: def create_playlist(self) -> None:
"""Creates a database record for a playlist, given a name""" """Creates a database record for a playlist, given a name"""
window = CreatePlaylistWindow(self.playlistCreatedSignal) window = CreatePlaylistWindow(self.playlistCreatedSignal)
window.playlistCreatedSignal.connect(self.add_latest_playlist_to_tree) window.playlistCreatedSignal.connect(self.add_latest_playlist_to_tree)
window.exec_() window.exec_()
def add_latest_playlist_to_tree(self) -> None:
"""Refreshes the playlist tree"""
self.playlistTreeView.add_latest_playlist_to_tree()
def import_playlist(self) -> None: def import_playlist(self) -> None:
""" """
Imports a .m3u file, given a base path attempts to match playlist files to Imports a .m3u file, given a base path attempts to match playlist files to
@ -382,14 +503,20 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
export_playlist_window = ExportPlaylistWindow() export_playlist_window = ExportPlaylistWindow()
export_playlist_window.exec_() export_playlist_window.exec_()
# Edit
def open_preferences(self) -> None: def open_preferences(self) -> None:
"""Opens the preferences window""" """Opens the preferences window"""
preferences_window = PreferencesWindow(self.config) preferences_window = PreferencesWindow(self.config)
preferences_window.exec_() # Display the preferences window modally preferences_window.exec_() # Display the preferences window modally
# Quick Actions
def scan_libraries(self) -> None: def scan_libraries(self) -> None:
"""Scans for new files in the configured library folder """
Refreshes the datagridview""" Scans for new files in the configured library folder
Refreshes the datagridview
"""
scan_for_music() scan_for_music()
self.tableView.load_music_table() self.tableView.load_music_table()

View File

@ -2,6 +2,7 @@ import DBA
from configparser import ConfigParser from configparser import ConfigParser
from utils import get_id3_tags, id3_timestamp_to_datetime from utils import get_id3_tags, id3_timestamp_to_datetime
import logging import logging
from PyQt5.QtCore import pyqtSignal
config = ConfigParser() config = ConfigParser()
config.read("config.ini") config.read("config.ini")
@ -12,6 +13,7 @@ def add_files_to_library(files):
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
""" """
# print("Running add_files_to_library.py") # print("Running add_files_to_library.py")
if not files: if not files:
return [] return []

View File

@ -1,6 +1,7 @@
import os import os
from configparser import ConfigParser from configparser import ConfigParser
from PyQt5.QtCore import pyqtSignal
from utils.add_files_to_library import add_files_to_library from utils.add_files_to_library import add_files_to_library
config = ConfigParser() config = ConfigParser()