From 554b06a667963b7e1df104664334c0a460c44e2b Mon Sep 17 00:00:00 2001 From: tsi-billypom Date: Thu, 17 Apr 2025 11:55:02 -0400 Subject: [PATCH] working on making id3 tags easier to work with --- components/MusicTable.py | 22 ++++++------ main.py | 21 +++++++----- utils/__init__.py | 2 +- utils/add_files_to_database.py | 63 ++++++++++------------------------ utils/get_id3_tags.py | 47 ++++++++++++++++--------- utils/init.sql | 1 + 6 files changed, 75 insertions(+), 81 deletions(-) diff --git a/components/MusicTable.py b/components/MusicTable.py index 11ce0bc..18574c7 100644 --- a/components/MusicTable.py +++ b/components/MusicTable.py @@ -63,6 +63,7 @@ from configparser import ConfigParser class MusicTable(QTableView): + playlistStatsSignal = pyqtSignal(str) playPauseSignal = pyqtSignal() playSignal = pyqtSignal(str) enterKey = pyqtSignal() @@ -162,7 +163,7 @@ class MusicTable(QTableView): self.model2.layoutChanged.connect(self.restore_scroll_position) self.horizontal_header.sectionResized.connect(self.on_header_resized) # Final actions - self.load_music_table() + # self.load_music_table() self.setup_keyboard_shortcuts() self.load_header_widths() @@ -455,7 +456,7 @@ class MusicTable(QTableView): except IndexError: pass except Exception as e: - debug(f'on_add_files_to_database_finished() | Something went wrong: {e}') + debug(f"on_add_files_to_database_finished() | Something went wrong: {e}") # ____________________ # | | @@ -690,9 +691,6 @@ class MusicTable(QTableView): # Fetch playlist data selected_playlist_id = playlist_id[0] try: - debug( - f"load_music_table() | selected_playlist_id: {selected_playlist_id}" - ) with DBA.DBAccess() as db: data = db.query( "SELECT s.id, s.title, s.artist, s.album, s.track_number, s.genre, s.codec, s.album_date, s.filepath FROM song s JOIN song_playlist sp ON s.id = sp.song_id WHERE sp.playlist_id = ?", @@ -712,7 +710,10 @@ class MusicTable(QTableView): error(f"load_music_table() | Unhandled exception: {e}") return # Populate the model + row_count: int = 0 + total_time: int = 0 # total time of all songs in seconds for row_data in data: + row_count += 1 id, *rest_of_data = row_data # handle different datatypes items = [] @@ -731,6 +732,7 @@ class MusicTable(QTableView): for item in items: item.setData(id, Qt.ItemDataRole.UserRole) self.model2.layoutChanged.emit() # emits a signal that the view should be updated + self.playlistStatsSignal.emit(f"Songs: {row_count} | Total time: {total_time}") self.connect_data_changed() self.connect_layout_changed() @@ -739,7 +741,7 @@ class MusicTable(QTableView): Loads the header widths from the last application close. """ table_view_column_widths = str(self.config["table"]["column_widths"]).split(",") - debug(f'loaded header widths: {table_view_column_widths}') + debug(f"loaded header widths: {table_view_column_widths}") if not isinstance(table_view_column_widths, list): for i in range(self.model2.columnCount() - 1): self.setColumnWidth(i, int(table_view_column_widths[i])) @@ -830,10 +832,6 @@ class MusicTable(QTableView): """Returns the selected songs filepath""" return self.selected_song_filepath - def get_selected_song_metadata(self) -> ID3 | dict: - """Returns the selected song's ID3 tags""" - return get_id3_tags(self.selected_song_filepath)[0] - def get_selected_songs_db_ids(self) -> list: """Returns a list of id's for the selected songs""" indexes = self.selectedIndexes() @@ -854,6 +852,10 @@ class MusicTable(QTableView): """Returns the currently playing song's ID3 tags""" return get_id3_tags(self.current_song_filepath)[0] + def get_selected_song_metadata(self) -> ID3 | dict: + """Returns the selected song's ID3 tags""" + return get_id3_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) diff --git a/main.py b/main.py index 64f2d51..257d768 100644 --- a/main.py +++ b/main.py @@ -37,7 +37,7 @@ from PyQt5.QtCore import ( QThreadPool, QRunnable, ) -from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe +from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe, QMediaPlaylist from PyQt5.QtGui import QClipboard, QCloseEvent, QFont, QPixmap, QResizeEvent from utils import ( delete_album_art, @@ -162,7 +162,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): # self.vLayoutAlbumArt.SetFixedSize() self.status_bar = QStatusBar() - self.permanent_status_label = QLabel("Status...") + self.permanent_status_label = QLabel("") self.status_bar.addPermanentWidget(self.permanent_status_label) self.setStatusBar(self.status_bar) @@ -174,6 +174,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): # widget bits self.album_art_scene: QGraphicsScene = QGraphicsScene() self.player: QMediaPlayer = QMediaPlayer() # Audio player object + self.playlist: QMediaPlaylist = QMediaPlaylist() self.probe: QAudioProbe = QAudioProbe() # Gets audio buffer data self.audio_visualizer: AudioVisualizer = AudioVisualizer( self.player, self.probe, self.PlotWidget @@ -248,13 +249,15 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): ## CONNECTIONS # tableView - # self.tableView.doubleClicked.connect(self.play_audio_file) - # self.tableView.enterKey.connect(self.play_audio_file) self.tableView.playSignal.connect(self.play_audio_file) self.tableView.playPauseSignal.connect( self.on_play_clicked ) # Spacebar toggle play/pause signal self.tableView.handleProgressSignal.connect(self.handle_progress) + self.tableView.playlistStatsSignal.connect( + self.set_permanent_status_bar_message + ) + self.tableView.load_music_table() # playlistTreeView self.playlistTreeView.playlistChoiceSignal.connect( @@ -289,7 +292,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): for i in range(self.tableView.model2.columnCount()): list_of_column_widths.append(str(self.tableView.columnWidth(i))) column_widths_as_string = ",".join(list_of_column_widths) - debug(f'saving column widths: {column_widths_as_string}') + debug(f"saving column widths: {column_widths_as_string}") self.config["table"]["column_widths"] = column_widths_as_string self.config["settings"]["volume"] = str(self.current_volume) self.config["settings"]["window_size"] = ( @@ -301,7 +304,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): with open(self.cfg_file, "w") as configfile: self.config.write(configfile) except Exception as e: - debug(f'wtf man {e}') + debug(f"wtf man {e}") if a0 is not None: super().closeEvent(a0) @@ -327,7 +330,10 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.speedLabel.setText("{:.2f}".format(rate / 50)) def on_play_clicked(self) -> None: - """Updates the Play & Pause buttons when clicked""" + """ + Plays & pauses the song + Updates the button icons + """ pixmapi = QStyle.StandardPixmap.SP_MediaPlay play_icon = self.style().standardIcon(pixmapi) # type: ignore pixmapi = QStyle.StandardPixmap.SP_MediaPause @@ -405,7 +411,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): """ Sets the permanent message label in the status bar """ - # what does this do? self.permanent_status_label.setText(message) def show_status_bar_message(self, message: str, timeout: int | None = None) -> None: diff --git a/utils/__init__.py b/utils/__init__.py index a1e1d52..4ade67c 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -3,7 +3,7 @@ from .convert_id3_timestamp_to_datetime import convert_id3_timestamp_to_datetime from .initialize_db import initialize_db from .safe_get import safe_get from .get_album_art import get_album_art -from .get_id3_tags import get_id3_tags +from .get_id3_tags import get_id3_tags, id3_remap from .get_reorganize_vars import get_reorganize_vars from .set_id3_tag import set_id3_tag from .delete_song_id_from_database import delete_song_id_from_database diff --git a/utils/add_files_to_database.py b/utils/add_files_to_database.py index 3a4df72..60f25b6 100644 --- a/utils/add_files_to_database.py +++ b/utils/add_files_to_database.py @@ -2,7 +2,7 @@ from PyQt5.QtWidgets import QMessageBox from mutagen.id3 import ID3 import DBA from logging import debug -from utils import get_id3_tags, convert_id3_timestamp_to_datetime +from utils import get_id3_tags, convert_id3_timestamp_to_datetime, id3_remap from configparser import ConfigParser from pathlib import Path from appdirs import user_config_dir @@ -14,8 +14,12 @@ def add_files_to_database(files, progress_callback=None): Args: files: list() of fully qualified paths to audio file(s) progress_callback: emit data for user feedback - Returns: - True on success, else False + + Returns a tuple where the first value is the success state + and the second value is a list of failed to add items + ``` + (True, {"filename.mp3":"failed because i said so"}) + ``` """ config = ConfigParser() cfg_file = ( @@ -23,7 +27,7 @@ def add_files_to_database(files, progress_callback=None): ) config.read(cfg_file) 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()"} extensions = config.get("settings", "extensions").split(",") failed_dict = {} insert_data = [] # To store data for batch insert @@ -33,55 +37,24 @@ def add_files_to_database(files, progress_callback=None): progress_callback.emit(filepath) filename = filepath.split("/")[-1] - audio, details = get_id3_tags(filepath) - # print('got id3 tags') - # print(type(audio)) - # print(audio) - if not isinstance(audio, ID3): + tags, details = get_id3_tags(filepath) + if details: failed_dict[filepath] = details continue - - try: - title = audio["TIT2"].text[0] - except KeyError: - title = filename - try: - artist = audio["TPE1"].text[0] - except KeyError: - artist = "" - try: - album = audio["TALB"].text[0] - except KeyError: - album = "" - try: - track_number = audio["TRCK"].text[0] - except KeyError: - track_number = None - try: - genre = audio["TCON"].text[0] - except KeyError: - genre = "" - try: - date = convert_id3_timestamp_to_datetime(audio["TDRC"].text[0]) - except KeyError: - date = "" - try: - bitrate = audio["TBIT"].text[0] - except KeyError: - bitrate = "" + audio = id3_remap(tags) # Append data tuple to insert_data list insert_data.append( ( filepath, - title, - album, - artist, - track_number, - genre, + audio["title"], + audio["album"], + audio["artist"], + audio["track_number"], + audio["genre"], filename.split(".")[-1], - date, - bitrate, + audio["date"], + audio["bitrate"], ) ) # Check if batch size is reached diff --git a/utils/get_id3_tags.py b/utils/get_id3_tags.py index 9d7e821..7c3dbe1 100644 --- a/utils/get_id3_tags.py +++ b/utils/get_id3_tags.py @@ -1,31 +1,29 @@ import os from logging import debug, error from mutagen.id3 import ID3 +from mutagen.mp3 import MP3 +from mutagen.flac import FLAC from mutagen.id3._frames import TIT2 from mutagen.id3._util import ID3NoHeaderError -def get_mp3_tags(filename: str) -> tuple[ID3 | dict, str]: +from utils import convert_id3_timestamp_to_datetime + + +def get_mp3_tags(filename: str) -> tuple[MP3 | ID3 | FLAC, str]: """Get ID3 tags for mp3 file""" try: # Open the MP3 file and read its content - audio = ID3(filename) + audio = MP3(filename) except ID3NoHeaderError: - audio = ID3() + audio = MP3() try: if os.path.exists(filename): audio.save(os.path.abspath(filename)) - # NOTE: If 'TIT2' tag is not set, we add it with a default value - # title = filename without extension - title = os.path.splitext(os.path.basename(filename))[0] - if "TIT2" in list(audio.keys()): - audio.save() - return audio, "" - else: - # if title tag doesnt exist, - # create it and return + if "TIT2" not in list(audio.keys()): + # if title tag doesnt exist, create it - filename tit2_tag = TIT2(encoding=3, text=[title]) audio["TIT2"] = tit2_tag # Save the updated tags @@ -33,9 +31,26 @@ def get_mp3_tags(filename: str) -> tuple[ID3 | dict, str]: return audio, "" except Exception as e: - return {}, f"Could not assign ID3 tag to file: {e}" + return MP3(), f"Could not assign ID3 tag to file: {e}" -def get_id3_tags(filename: str) -> tuple[ID3 | dict, str]: + +def id3_remap(audio: MP3 | ID3 | FLAC) -> dict: + """ + Turns an ID3 dict into a normal dict that I the human can use. + with words... + """ + return { + "title": audio["TIT2"].text[0], + "artist": audio["TPE1"].text[0], + "album": audio["TALB"].text[0], + "track_number": audio["TRCK"].text[0], + "genre": audio["TCON"].text[0], + "date": convert_id3_timestamp_to_datetime(audio["TDRC"].text[0]), + "bitrate": audio["TBIT"].text[0], + } + + +def get_id3_tags(filename: str) -> tuple[MP3 | ID3 | FLAC, str]: """ Get the ID3 tags for an audio file Returns a tuple of: @@ -52,7 +67,5 @@ def get_id3_tags(filename: str) -> tuple[ID3 | dict, str]: if filename.endswith(".mp3"): tags, details = get_mp3_tags(filename) else: - tags, details = {}, "non mp3 file" + tags, details = ID3(), "non mp3 file" return tags, details - - diff --git a/utils/init.sql b/utils/init.sql index 8b29b29..58cae21 100644 --- a/utils/init.sql +++ b/utils/init.sql @@ -10,6 +10,7 @@ CREATE TABLE song( album varchar(255), artist varchar(255), track_number integer, + length_seconds integer, genre varchar(255), codec varchar(15), album_date date,