diff --git a/components/MusicTable.py b/components/MusicTable.py index 08c4c89..40a9b03 100644 --- a/components/MusicTable.py +++ b/components/MusicTable.py @@ -1,7 +1,7 @@ import DBA -from PyQt5.QtGui import QStandardItem, QStandardItemModel, QKeySequence -from PyQt5.QtWidgets import QTableView, QShortcut -from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtGui import QStandardItem, QStandardItemModel, QKeySequence +from PyQt5.QtWidgets import QTableView, QShortcut, QMessageBox, QAbstractItemView +from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal from utils import add_files_to_library from utils import get_id3_tags from utils import get_album_art @@ -12,15 +12,18 @@ import configparser class MusicTable(QTableView): playPauseSignal = pyqtSignal() enterKey = pyqtSignal() - def __init__(self, parent=None, qapp=None): - QTableView.__init__(self, parent) - self.headers = ['title', 'artist', 'album', 'genre', 'codec', 'year', 'path'] + def __init__(self, parent=None): + # QTableView.__init__(self, parent) + super().__init__(parent) + self.model = QStandardItemModel(self) # Necessary for actions related to cell values + self.setModel(self.model) # Same as above + self.config = configparser.ConfigParser() + self.config.read('config.ini') + self.headers = ['title', 'artist', 'album', 'genre', 'codec', 'year', 'path'] # gui names of headers + self.columns = str(self.config['table']['columns']).split(',') # db names of headers self.songChanged = None self.selected_song_filepath = None self.current_song_filepath = None - self.qapp = None - self.config = configparser.ConfigParser() - self.config.read('config.ini') # self.tableView.resizeColumnsToContents() self.clicked.connect(self.set_selected_song_filepath) # doubleClicked is a built in event for QTableView - we listen for this event and run set_current_song_filepath @@ -28,23 +31,43 @@ class MusicTable(QTableView): self.enterKey.connect(self.set_current_song_filepath) self.fetch_library() self.setup_keyboard_shortcuts() + self.model.dataChanged.connect(self.on_cell_data_changed) # editing cells + def setup_keyboard_shortcuts(self): """Setup shortcuts here""" shortcut = QShortcut(QKeySequence("Ctrl+Shift+R"), self) shortcut.activated.connect(self.reorganize_selected_files) + + def on_cell_data_changed(self, topLeft: QModelIndex, bottomRight: QModelIndex): + """Handles updating ID3 tags when data changes in a cell""" + print('on_cell_data_changed') + filepath_column_idx = self.model.columnCount() - 1 # always the last column + filepath_index = self.model.index(topLeft.row(), filepath_column_idx) # exact index of the edited cell + filepath = self.model.data(filepath_index) # filepath + # update the ID3 information + new_data = topLeft.data() + edited_column = self.columns[topLeft.column()] + print(filepath) + print(edited_column) + + def reorganize_selected_files(self): """Ctrl+Shift+R = Reorganize""" filepaths = self.get_selected_songs_filepaths() - # right now this just prints the filepaths to the songs obv. print(f'yay: {filepaths}') - # TODO - # Get user confirmation screen (default yes, so i can press enter and go) - # Get target directory - # Copy files to new dir - # delete files from current dir - # add new files to library + # Confirmation screen (yes, no) + reply = QMessageBox.question(self, 'Confirmation', 'Are you sure you want to reorganize these files?', QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) + if reply: + target_dir = str(self.config['directories']['reorganize_destination']) + print(target_dir) + + # Get target directory + # Copy files to new dir + # delete files from current dir + # add new files to library + def keyPressEvent(self, event): """Press a key. Do a thing""" @@ -62,21 +85,27 @@ class MusicTable(QTableView): if new_index.isValid(): self.setCurrentIndex(new_index) elif key in (Qt.Key_Return, Qt.Key_Enter): - self.enterKey.emit() # Enter key detected + if self.state() != QAbstractItemView.EditingState: + self.enterKey.emit() # Enter key detected + else: + super().keyPressEvent(event) else: # Default behavior super().keyPressEvent(event) + def toggle_play_pause(self): """Toggles the currently playing song by emitting a signal""" if not self.current_song_filepath: self.set_current_song_filepath() self.playPauseSignal.emit() + def set_selected_song_filepath(self): """Sets the filepath of the currently selected song""" self.selected_song_filepath = self.currentIndex().siblingAtColumn(self.headers.index('path')).data() print(f'Selected song: {self.selected_song_filepath}') + def set_current_song_filepath(self): """Sets the filepath of the currently playing song""" # Setting the current song filepath automatically plays that song @@ -84,6 +113,7 @@ class MusicTable(QTableView): self.current_song_filepath = self.currentIndex().siblingAtColumn(self.headers.index('path')).data() print(f'Current song: {self.current_song_filepath}') + def get_selected_rows(self): """Returns a list of indexes for every selected row""" selection_model = self.selectionModel() @@ -93,6 +123,7 @@ class MusicTable(QTableView): # rows.append(idx.row()) # return rows + def get_selected_songs_filepaths(self): """Returns a list of the filepaths for the currently selected songs""" selected_rows = self.get_selected_rows() @@ -102,29 +133,34 @@ class MusicTable(QTableView): filepaths.append(idx.data()) return filepaths + def get_selected_song_filepath(self): """Returns the selected songs filepath""" return self.selected_song_filepath + def get_selected_song_metadata(self): """Returns the selected song's ID3 tags""" return get_id3_tags(self.selected_song_filepath) + def get_current_song_filepath(self): """Returns the currently playing song filepath""" return self.current_song_filepath + def get_current_song_metadata(self): """Returns the currently playing song's ID3 tags""" return get_id3_tags(self.current_song_filepath) + def get_current_song_album_art(self): """Returns the APIC data (album art lol) for the currently playing song""" return get_album_art(self.current_song_filepath) + def fetch_library(self): """Initialize the tableview model""" - self.model = QStandardItemModel() self.model.setHorizontalHeaderLabels(self.headers) # Fetch library data with DBA.DBAccess() as db: @@ -133,11 +169,10 @@ class MusicTable(QTableView): for row_data in data: items = [QStandardItem(str(item)) for item in row_data] self.model.appendRow(items) - # Set the model to the tableView (we are the tableview) - self.setModel(self.model) - # self.update() + # Update the viewport/model self.viewport().update() + def add_files(self, files): """When song(s) added to the library, update the tableview model - Drag & Drop song(s) on tableView @@ -148,6 +183,7 @@ class MusicTable(QTableView): if number_of_files_added: self.fetch_library() + def load_qapp(self, qapp): # why was this necessary again? :thinking: self.qapp = qapp diff --git a/main.py b/main.py index 45078c7..a086aae 100644 --- a/main.py +++ b/main.py @@ -2,9 +2,9 @@ from ui import Ui_MainWindow from PyQt5.QtWidgets import QMainWindow, QApplication, QGraphicsScene, \ QHeaderView, QGraphicsPixmapItem import qdarktheme -from PyQt5.QtCore import QUrl, QTimer, QEvent, Qt +from PyQt5.QtCore import QUrl, QTimer, QEvent, Qt, QModelIndex from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe -from PyQt5.QtGui import QPixmap +from PyQt5.QtGui import QPixmap, QStandardItemModel from utils import scan_for_music from utils import initialize_library_database from components import AudioVisualizer @@ -27,17 +27,18 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.current_song_album_art = None self.album_art_scene = QGraphicsScene() self.qapp = qapp - print(f'ApplicationWindow self.qapp: {self.qapp}') + # print(f'ApplicationWindow self.qapp: {self.qapp}') self.tableView.load_qapp(self.qapp) + # print(f'tableView type: {type(self.tableView)}') self.config = configparser.ConfigParser() self.config.read('config.ini') global stopped stopped = False # Initialization - self.player = QMediaPlayer() # Audio player - self.probe = QAudioProbe() # Get audio data + self.player = QMediaPlayer() # Audio player object + self.probe = QAudioProbe() # Gets audio data self.timer = QTimer(self) # Audio timing things - # self.model = QStandardItemModel() # Table library listing + # self.music_table_model = QStandardItemModel(self) # Table library listing self.audio_visualizer = AudioVisualizer(self.player) self.current_volume = 50 self.player.setVolume(self.current_volume) @@ -49,7 +50,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): # Slider Timer (realtime playback feedback horizontal bar) self.timer.start(150) # 150ms update interval solved problem with drag seeking halting playback self.timer.timeout.connect(self.move_slider) - + # Graphics plot self.PlotWidget.setXRange(0,100,padding=0) # x axis range self.PlotWidget.setYRange(0,0.3,padding=0) # y axis range @@ -59,26 +60,22 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): # Remove y-axis labels and decorations self.PlotWidget.getAxis('left').setTicks([]) # Remove y-axis ticks self.PlotWidget.getAxis('left').setLabel('') # Remove y-axis label - - + # Connections # ! FIXME moving the slider while playing is happening frequently causes playback to halt - the pyqtgraph is also affected by this self.playbackSlider.sliderMoved[int].connect(lambda: self.player.setPosition(self.playbackSlider.value())) # Move slidet to adjust playback time self.volumeSlider.sliderMoved[int].connect(lambda: self.volume_changed()) # Move slider to adjust volume self.playButton.clicked.connect(self.on_play_clicked) # Click to play/pause - # self.pauseButton.clicked.connect(self.on_pause_clicked) self.previousButton.clicked.connect(self.on_previous_clicked) # Click to previous song self.nextButton.clicked.connect(self.on_next_clicked) # Click to next song self.actionPreferences.triggered.connect(self.actionPreferencesClicked) # Open preferences menu self.actionScanLibraries.triggered.connect(self.scan_libraries) # Scan library self.actionClearDatabase.triggered.connect(self.clear_database) # Clear database ## tableView - # self.tableView.clicked.connect(self.set_clicked_cell_filepath) self.tableView.doubleClicked.connect(self.play_audio_file) # Listens for the double click event, then plays the song self.tableView.enterKey.connect(self.play_audio_file) # Listens for the enter key event, then plays the song self.tableView.playPauseSignal.connect(self.on_play_clicked) # Spacebar toggle play/pause signal self.tableView.viewport().installEventFilter(self) # for drag & drop functionality - # self.tableView.model.layoutChanged() # set column widths table_view_column_widths = str(self.config['table']['column_widths']).split(',') for i in range(self.tableView.model.columnCount()): @@ -104,7 +101,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): event.accept() return True return super().eventFilter(source, event) - + def closeEvent(self, event): """Save settings when closing the application""" # MusicTable/tableView column widths @@ -113,12 +110,13 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): list_of_column_widths.append(str(self.tableView.columnWidth(i))) column_widths_as_string = ','.join(list_of_column_widths) self.config['table']['column_widths'] = column_widths_as_string - + # Save the config with open('config.ini', 'w') as configfile: self.config.write(configfile) super().closeEvent(event) - + + def play_audio_file(self): """Start playback of tableView.current_song_filepath track & moves playback slider""" self.current_song_metadata = self.tableView.get_current_song_metadata() # get metadata @@ -128,7 +126,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.player.setMedia(content) # what content to play self.player.play() # play self.move_slider() # mover - + # assign metadata # FIXME when i change tinytag to something else artist = self.current_song_metadata["artist"][0] if "artist" in self.current_song_metadata else None @@ -140,7 +138,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.titleLabel.setText(title) # set album artwork self.load_album_art(self.current_song_album_art) - def load_album_art(self, album_art_data): """Sets the album art for the currently playing track""" @@ -161,21 +158,21 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.albumGraphicsView.setScene(self.album_art_scene) # Adjust the album art scaling self.adjustPixmapScaling(pixmapItem) - + def adjustPixmapScaling(self, pixmapItem): """Adjust the scaling of the pixmap item to fit the QGraphicsView, maintaining aspect ratio""" viewWidth = self.albumGraphicsView.width() viewHeight = self.albumGraphicsView.height() pixmapSize = pixmapItem.pixmap().size() - + # Calculate scaling factor while maintaining aspect ratio scaleX = viewWidth / pixmapSize.width() scaleY = viewHeight / pixmapSize.height() scaleFactor = min(scaleX, scaleY) - + # Apply scaling to the pixmap item pixmapItem.setScale(scaleFactor) - + def update_audio_visualization(self): """Handles upading points on the pyqtgraph visual""" self.clear_audio_visualization() @@ -183,10 +180,10 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.x = [i for i in range(len(self.y))] self.PlotWidget.plot(self.x, self.y, fillLevel=0, fillBrush=mkBrush('b')) self.PlotWidget.show() - + def clear_audio_visualization(self): self.PlotWidget.clear() - + def move_slider(self): """Handles moving the playback slider""" if stopped: diff --git a/sample_config.ini b/sample_config.ini index 74fa845..5df090a 100644 --- a/sample_config.ini +++ b/sample_config.ini @@ -12,5 +12,6 @@ reorganize_destination = /where/to/reorganize/to extensions = mp3,wav,ogg,flac [table] -# Music table column widths -column_widths = 181,116,222,76,74,72,287 \ No newline at end of file +# Music table options +columns = title,artist,album,genre,codec,album_date,filepath +column_widths = 181,116,222,76,74,72,287 diff --git a/utils/add_files_to_library.py b/utils/add_files_to_library.py index a82d09d..0f1b8b8 100644 --- a/utils/add_files_to_library.py +++ b/utils/add_files_to_library.py @@ -9,14 +9,12 @@ config.read("config.ini") def add_files_to_library(files): """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 true if any files were added """ if not files: return False - print(f"utils | adding files to library: {files}") + print(f"utils/add_files_to_library: {files}") extensions = config.get("settings", "extensions").split(",") insert_data = [] # To store data for batch insert for filepath in files: diff --git a/utils/get_id3_tags.py b/utils/get_id3_tags.py index 359dbbd..31d15e8 100644 --- a/utils/get_id3_tags.py +++ b/utils/get_id3_tags.py @@ -1,18 +1,27 @@ from mutagen.easyid3 import EasyID3 from mutagen import File + def get_id3_tags(file): """Get the ID3 tags for an audio file # Parameters `file` | str | Fully qualified path to file - # Returns dict of all id3 tags + if all tags are empty, at minimum fill in the 'title' """ try: audio = EasyID3(file) - print(f'ID3 Tags: {audio}') + # Check if all tags are empty + tags_are_empty = all(not values for values in audio.values()) + if tags_are_empty: + audio['title'] = [file.split('/')[-1]] return audio except Exception as e: print(f"Error: {e}") - return {} \ No newline at end of file + return {} + +# import sys +# my_file = sys.argv[1] +# data = get_id3_tags(my_file) +# print(data)