diff --git a/README.md b/README.md index be847e2..f804e06 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ player = new QMediaPlayer; player->setMedia(QUrl("gst-pipeline: videotestsrc ! autovideosink")); player->play(); ``` +QMultimedia.EncodingMode / Encoding quality... ##### misc - database playlist autoexporting - .wav, .ogg, .flac convertor diff --git a/components/MediaPlayer.py b/components/MediaPlayer.py index d158e76..8f81183 100644 --- a/components/MediaPlayer.py +++ b/components/MediaPlayer.py @@ -1,19 +1,18 @@ -from PyQt5.QtCore import QObject, QUrl +from PyQt5.QtCore import QObject, QUrl, pyqtSignal from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QMediaPlaylist from PyQt5.QtWidgets import QApplication import sys class MediaPlayer(QMediaPlayer): + playlistNextSignal = pyqtSignal() def __init__(self): super().__init__() # Connect mediaStatusChanged signal to our custom function self.mediaStatusChanged.connect(self.on_media_status_changed) - # def play(self, file_path): - # media_content = QMediaContent(QUrl.fromLocalFile(file_path)) - # self.player.setMedia(media_content) - # self.player.play() + def play(self): + super().play() def on_media_status_changed(self, status): if status == QMediaPlayer.MediaStatus.EndOfMedia: @@ -21,6 +20,6 @@ class MediaPlayer(QMediaPlayer): self.on_song_ended() def on_song_ended(self): - # Your custom logic when the song ends + self.playlistNextSignal.emit() print("Custom function executed after song ended!") diff --git a/components/MusicTable.py b/components/MusicTable.py index cc98d31..ddddd56 100644 --- a/components/MusicTable.py +++ b/components/MusicTable.py @@ -67,6 +67,7 @@ from configparser import ConfigParser class MusicTable(QTableView): playlistStatsSignal = pyqtSignal(str) loadMusicTableSignal = pyqtSignal() + sortSignal = pyqtSignal() playPauseSignal = pyqtSignal() playSignal = pyqtSignal(str) enterKey = pyqtSignal() @@ -352,12 +353,14 @@ class MusicTable(QTableView): self.set_selected_song_qmodel_index(selected_qmodel_index) self.set_current_song_qmodel_index(current_qmodel_index) self.jump_to_selected_song() + self.sortSignal.emit() def on_cell_clicked(self, index): """ When a cell is clicked, do some stuff :) - this func also runs when double click happens, fyi """ + print(index.row(), index.column()) self.set_selected_song_filepath() self.set_selected_song_qmodel_index() self.viewport().update() # type: ignore @@ -457,6 +460,7 @@ class MusicTable(QTableView): Sets the current song filepath Emits a signal that the current song should start playback """ + self.set_current_song_qmodel_index() self.set_current_song_filepath() self.playSignal.emit(self.current_song_filepath) @@ -653,6 +657,7 @@ class MusicTable(QTableView): def toggle_play_pause(self): """Toggles the currently playing song by emitting a Signal""" if not self.current_song_filepath: + self.set_current_song_qmodel_index() self.set_current_song_filepath() self.playPauseSignal.emit() @@ -847,10 +852,6 @@ class MusicTable(QTableView): """Returns the selected song's ID3 tags""" return id3_remap(get_tags(self.selected_song_filepath)[0]) - def get_current_song_album_art(self) -> bytes: - """Returns the APIC data (album art lol) for the currently playing song""" - return get_album_art(self.current_song_filepath) - def set_selected_song_filepath(self) -> None: """Sets the filepath of the currently selected song""" try: @@ -864,13 +865,11 @@ class MusicTable(QTableView): filepath = db.query('SELECT filepath FROM song WHERE id = ?', (id,))[0][0] self.selected_song_filepath = filepath - def set_current_song_filepath(self) -> None: + def set_current_song_filepath(self, filepath=None) -> None: """ - Sets the current song filepath to the value in column 'path' from the current selected row index - - Store the QModelIndex for navigation """ - self.set_current_song_qmodel_index() # update the filepath self.current_song_filepath: str = ( self.current_song_qmodel_index.siblingAtColumn( @@ -879,18 +878,28 @@ class MusicTable(QTableView): ) def set_current_song_qmodel_index(self, index=None): + """ + Takes in the proxy model index for current song - QModelIndex + converts to model2 index + stores it + """ if index is None: - # map proxy (sortable) model to the original model (used for interactions) - index = self.proxymodel.mapToSource(self.currentIndex()) - # set the proxy model index - self.current_song_qmodel_index: QModelIndex = index + index = self.currentIndex() + # map proxy (sortable) model to the original model (used for interactions) + model_index: QModelIndex = self.proxymodel.mapToSource(index) + self.current_song_qmodel_index: QModelIndex = model_index def set_selected_song_qmodel_index(self, index=None): + """ + Takes in the proxy model index for current song - QModelIndex + converts to model2 index + stores it + """ if index is None: - # map proxy (sortable) model to the original model (used for interactions) - index = self.proxymodel.mapToSource(self.currentIndex()) - # set the proxy model index - self.selected_song_qmodel_index: QModelIndex = index + index = self.currentIndex() + # map proxy (sortable) model to the original model (used for interactions) + model_index: QModelIndex = self.proxymodel.mapToSource(index) + self.selected_song_qmodel_index: QModelIndex = model_index def load_qapp(self, qapp) -> None: """Necessary for using members and methods of main application window""" diff --git a/main.py b/main.py index 178fb6f..a6e4de8 100644 --- a/main.py +++ b/main.py @@ -26,8 +26,10 @@ from PyQt5.QtWidgets import ( QPushButton, QStatusBar, QStyle, + QTableView, ) from PyQt5.QtCore import ( + QModelIndex, QSize, QThread, QUrl, @@ -39,7 +41,7 @@ from PyQt5.QtCore import ( QThreadPool, QRunnable, ) -from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe, QMediaPlaylist +from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe, QMediaPlaylist, QMultimedia from PyQt5.QtGui import QClipboard, QCloseEvent, QFont, QPixmap, QResizeEvent from utils import ( delete_album_art, @@ -48,9 +50,12 @@ from utils import ( initialize_db, add_files_to_database, set_album_art, + id3_remap ) from components import ( + HeaderTags, MediaPlayer, + MusicTable, PreferencesWindow, AudioVisualizer, CreatePlaylistWindow, @@ -168,10 +173,10 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.current_song_album_art: bytes | None = None # widget bits + self.tableView: MusicTable self.album_art_scene: QGraphicsScene = QGraphicsScene() # self.player: QMediaPlayer = QMediaPlayer() # Audio player object self.player: QMediaPlayer = MediaPlayer() - self.playlist: QMediaPlaylist = QMediaPlaylist() # set index on choose song # index is the model2's row number? i guess? self.probe: QAudioProbe = QAudioProbe() # Gets audio buffer data @@ -200,6 +205,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): # sharing functions with other classes and that self.tableView.load_qapp(self) self.albumGraphicsView.load_qapp(self) + self.headers = HeaderTags() # Settings init self.current_volume: int = int(self.config["settings"]["volume"]) @@ -224,7 +230,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): ) # self.speedSlider.doubleClicked.connect(lambda: self.on_speed_changed(1)) self.playButton.clicked.connect(self.on_play_clicked) # Click to play/pause - self.previousButton.clicked.connect(self.on_previous_clicked) + self.previousButton.clicked.connect(self.on_prev_clicked) self.nextButton.clicked.connect(self.on_next_clicked) # Click to next song # FILE MENU @@ -257,8 +263,8 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.tableView.playlistStatsSignal.connect( self.set_permanent_status_bar_message ) - self.tableView.loadMusicTableSignal.connect(self.load_media_playlist) self.tableView.load_music_table() + self.player.playlistNextSignal.connect(self.on_next_clicked) # playlistTreeView self.playlistTreeView.playlistChoiceSignal.connect( @@ -316,6 +322,14 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): # | | # |____________________| + def on_playlist_media_changed(self, media: QMediaContent): + """Update stuff when the song changes""" + if not media.isNull(): + file_url = media.canonicalUrl().toLocalFile() + metadata = id3_remap(get_tags(file_url)[0]) + if metadata is not None: + self.set_ui_metadata(metadata["title"], metadata["artist"], metadata["album"]) + def on_volume_changed(self) -> None: """Handles volume changes""" try: @@ -351,15 +365,29 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): else: self.playButton.setText("👽") - def on_previous_clicked(self) -> None: - """""" - # TODO: implement this - debug("main.py on_previous_clicked()") + def on_prev_clicked(self) -> None: + """click previous - go to previous song""" + current_real_index = self.tableView.current_song_qmodel_index + index = self.tableView.proxymodel.mapFromSource(current_real_index) + row: int = index.row() + prev_row: int = row - 1 + prev_index: QModelIndex = self.tableView.proxymodel.index(prev_row, index.column()) + prev_filepath = prev_index.siblingAtColumn(self.headers.user_headers.index("filepath")).data() + + self.play_audio_file(prev_filepath) + self.tableView.set_current_song_qmodel_index(prev_index) def on_next_clicked(self) -> None: - """""" - # TODO: implement this - debug("main.py on_next_clicked()") + """click next (or song ended) - go to next song""" + current_real_index = self.tableView.current_song_qmodel_index + index = self.tableView.proxymodel.mapFromSource(current_real_index) + row: int = index.row() + next_row: int = row + 1 + next_index: QModelIndex = self.tableView.proxymodel.index(next_row, index.column()) + next_filepath = next_index.siblingAtColumn(self.headers.user_headers.index("filepath")).data() + + self.play_audio_file(next_filepath) + self.tableView.set_current_song_qmodel_index(next_index) # ____________________ # | | @@ -368,11 +396,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): # | | # |____________________| - def load_media_playlist(self): - self.proxymodel.row - pass - # self.playlist. - def setup_fonts(self): """Initializes font sizes and behaviors for various UI components""" font: QFont = QFont() @@ -437,8 +460,8 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): """ if not filepath: filepath = self.tableView.get_selected_song_filepath() - # get metadata - metadata = get_tags(filepath)[0] + file_url = QUrl.fromLocalFile(filepath) + metadata = id3_remap(get_tags(filepath)[0]) # read the file url = QUrl.fromLocalFile(filepath) # load the audio content @@ -451,15 +474,20 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): # assign "now playing" labels & album artwork if metadata is not None: - artist = metadata["TPE1"][0] if "TPE1" in metadata else None - album = metadata["TALB"][0] if "TALB" in metadata else None - title = metadata["TIT2"][0] if "TIT2" in metadata else None - self.artistLabel.setText(artist) - self.albumLabel.setText(album) - self.titleLabel.setText(title) - # set album artwork - album_art_data = self.tableView.get_current_song_album_art() - self.albumGraphicsView.load_album_art(album_art_data) + self.set_ui_metadata(metadata["title"], metadata["artist"], metadata["album"], filepath) + + def set_ui_metadata(self, title, artist, album, filepath): + """ + Loads metadata into UI, presumably for current song + But you could pass any text here i guess + album art will always try to be current song + """ + self.artistLabel.setText(artist) + self.albumLabel.setText(album) + self.titleLabel.setText(title) + # set album artwork + album_art_data = get_album_art(filepath) + self.albumGraphicsView.load_album_art(album_art_data) def set_album_art_for_selected_songs(self, album_art_path: str) -> None: """Sets the ID3 tag APIC (album art) for all selected song filepaths""" diff --git a/utils/get_album_art.py b/utils/get_album_art.py index d599376..567ab64 100644 --- a/utils/get_album_art.py +++ b/utils/get_album_art.py @@ -9,6 +9,7 @@ def get_album_art(file: str | None) -> bytes: # Returns bytes for album art or placeholder artwork """ + debug(f'try album art: {file}') default_image_path = "./assets/default_album_art.jpg" if file: try: @@ -20,6 +21,7 @@ def get_album_art(file: str | None) -> bytes: return audio.getall("APIC")[0].data except Exception as e: error(f"Error retrieving album art: {e}") + return bytes() with open(default_image_path, "rb") as f: debug("loading placeholder album art") return f.read()