diff --git a/components/HeaderTags.py b/components/HeaderTags.py new file mode 100644 index 0000000..fc450b9 --- /dev/null +++ b/components/HeaderTags.py @@ -0,0 +1,57 @@ +class HeaderTags: + """ + Utility class to converting between different "standards" for tags (headers, id3, etc) + """ + + def __init__(self): + self.db: dict = { + "title": "title", + "artist": "artist", + "album": "album", + "album_artist": "album_artist", + "track_number": "track_number", + "genre": "genre", + "codec": "codec", + "length_seconds": "length_seconds", + "album_date": "album_date", + "filepath": "filepath", + } + self.gui: dict = { + "title": "title", + "artist": "artist", + "album": "album", + "album_artist": "alb artist", + "track_number": "track", + "genre": "genre", + "codec": "codec", + "length_seconds": "length", + "album_date": "year", + "filepath": "path", + } + self.id3: dict = { + "title": "TIT2", + "artist": "TPE1", + "album_artist": "TPE2", + "album": "TALB", + "track_number": "TRCK", + "genre": "TCON", + "codec": None, + "length_seconds": "TLEN", + "album_date": "TDRC", + "filepath": None, + } + # id3 is the key + self.id3_keys: dict = {} + for k, v in self.id3.items(): + if v is not None: + self.id3_keys[v] = k + + self.editable_db_tags: list = [ + "title", + "artist", + "album_artist", + "album", + "track_number", + "genre", + "album_date", + ] diff --git a/components/MetadataWindow.py b/components/MetadataWindow.py index a8d370c..e5af1a5 100644 --- a/components/MetadataWindow.py +++ b/components/MetadataWindow.py @@ -9,10 +9,8 @@ from PyQt5.QtWidgets import ( QPushButton, ) from PyQt5.QtGui import QFont -from PyQt5.QtCore import pyqtSignal -from mutagen.id3 import ID3 from components.ErrorDialog import ErrorDialog -from utils import set_tag, get_tag, update_song_in_database, id3_tag_mapping +from utils import set_tag, get_tags, update_song_in_database from logging import debug # import re @@ -30,7 +28,7 @@ class ID3LineEdit(QLineEdit): class MetadataWindow(QDialog): - def __init__(self, refreshMusicTableSignal, songs: list, ids: list): + def __init__(self, refreshMusicTableSignal, headers, songs: list, ids: list): """ Window that allows batch editing of metadata for multiple files @@ -42,7 +40,7 @@ class MetadataWindow(QDialog): super(MetadataWindow, self).__init__() self.refreshMusicTableSignal = refreshMusicTableSignal self.songs = list(zip(songs, ids)) - self.id3_tag_mapping = id3_tag_mapping + self.headers = headers # Keep a dictionary of the input fields for save function self.input_fields = {} self.setWindowTitle("Edit metadata") @@ -62,7 +60,7 @@ class MetadataWindow(QDialog): # Get a dict of all tags for all songs # e.g., { "TIT2": ["song_title1", "song_title2"], ... } for song in self.songs: - song_data = get_tags(song[0]) + song_data = get_tags(song[0])[0] if not song_data: QMessageBox.error( self, @@ -72,25 +70,23 @@ class MetadataWindow(QDialog): QMessageBox.Ok, ) return - for tag in self.id3_tag_mapping: - try: - _ = tag_sets[tag] - except KeyError: - # If a tag doesn't exist in our dict, create an empty list - tag_sets[tag] = [] - try: - tag_sets[tag].append(song_data[tag].text[0]) - except KeyError: - pass - # debug("tag sets:") - # debug(tag_sets) + for key, tag in self.headers.id3.items(): + if key not in self.headers.editable_db_tags: + continue + if tag is not None: + try: + _ = tag_sets[tag] + except KeyError: + # If a tag doesn't exist in our dict, create an empty list + tag_sets[tag] = [] + try: + tag_sets[tag].append(song_data[tag].text[0]) + except KeyError: + pass # UI Creation current_layout = QHBoxLayout() for idx, (tag, value) in enumerate(tag_sets.items()): - # Layout creation - # if idx == 0: - # pass if idx % 2 == 0: # Make a new horizontal layout for every 2 items layout.addLayout(current_layout) @@ -101,7 +97,7 @@ class MetadataWindow(QDialog): # If the ID3 tag is the same for every item we're editing field_text = str(value[0]) if value else "" # Normal field - label = QLabel(str(self.id3_tag_mapping[tag])) + label = QLabel(str(self.headers.id3_keys[tag])) input_field = ID3LineEdit(field_text, tag) input_field.setStyleSheet(None) else: @@ -109,7 +105,7 @@ class MetadataWindow(QDialog): # this means the metadata differs between the selected items for this tag # so be careful...dangerous field_text = "" - label = QLabel(str(self.id3_tag_mapping[tag])) + label = QLabel(str(self.headers.id3_keys[tag])) input_field = ID3LineEdit(field_text, tag) input_field.setStyleSheet("border: 1px solid red") # Save each input field to our dict for saving @@ -138,7 +134,7 @@ class MetadataWindow(QDialog): if success: update_song_in_database( song[1], - edited_column_name=self.id3_tag_mapping[tag], + edited_column_name=self.headers.id3_keys[tag], user_input_data=field.text(), ) else: diff --git a/components/MusicTable.py b/components/MusicTable.py index 4167e41..65dfe8b 100644 --- a/components/MusicTable.py +++ b/components/MusicTable.py @@ -40,6 +40,7 @@ from components.LyricsWindow import LyricsWindow from components.AddToPlaylistWindow import AddToPlaylistWindow from components.MetadataWindow import MetadataWindow from components.QuestionBoxDetails import QuestionBoxDetails +from components.HeaderTags import HeaderTags from main import Worker from utils import ( @@ -51,7 +52,7 @@ from utils import ( get_album_art, id3_remap, get_tags, - set_tag + set_tag, ) from subprocess import Popen from logging import debug, error @@ -63,43 +64,6 @@ from appdirs import user_config_dir from configparser import ConfigParser -class TableHeader: - def __init__(self): - self.db = { - "title": "title", - "artist": "artist", - "album": "album", - "track_number": "track_number", - "genre": "genre", - "codec": "codec", - "length_seconds": "length_seconds", - "album_date": "album_date", - "filepath": "filepath", - } - self.gui = { - "title": "Title", - "artist": "Artist", - "album": "Album", - "track_number": "Track", - "genre": "Genre", - "codec": "Codec", - "length_seconds": "Length", - "album_date": "Year", - "filepath": "Path", - } - self.id3 = { - "title": "TIT2", - "artist": "TPE1", - "album": "TALB", - "track_number": "TRCK", - "genre": "TCON", - "codec": None, - "length_seconds": "TLEN", - "album_date": "TDRC", - "filepath": None, - } - - class MusicTable(QTableView): playlistStatsSignal = pyqtSignal(str) playPauseSignal = pyqtSignal() @@ -142,7 +106,7 @@ class MusicTable(QTableView): # Threads self.threadpool = QThreadPool # headers class thing - self.headers = TableHeader() + self.headers = HeaderTags() # db names of headers self.database_columns = str(self.config["table"]["columns"]).split(",") @@ -376,7 +340,7 @@ class MusicTable(QTableView): def on_sort(self): debug("on_sort") - search_col_num = self.table_headers.index("path") + search_col_num = list(self.headers.gui.values()).index("path") selected_qmodel_index = self.find_qmodel_index_by_value( self.model2, search_col_num, self.selected_song_filepath ) @@ -554,7 +518,9 @@ class MusicTable(QTableView): """Opens a form with metadata from the selected audio files""" files = self.get_selected_songs_filepaths() song_ids = self.get_selected_songs_db_ids() - window = MetadataWindow(self.refreshMusicTable, files, song_ids) + window = MetadataWindow( + self.refreshMusicTableSignal, self.headers, files, song_ids + ) window.refreshMusicTableSignal.connect(self.load_music_table) window.exec_() # Display the preferences window modally @@ -697,14 +663,15 @@ class MusicTable(QTableView): self.disconnect_layout_changed() self.vertical_scroll_position = self.verticalScrollBar().value() # type: ignore self.model2.clear() - self.model2.setHorizontalHeaderLabels(self.headers.gui.values()) + self.model2.setHorizontalHeaderLabels(list(self.headers.gui.values())) + fields = ", ".join(list(self.headers.db.values())) if playlist_id: # Load a playlist # Fetch playlist data selected_playlist_id = playlist_id[0] try: 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 = ?", + f"SELECT id, {fields} FROM song JOIN song_playlist sp ON id = sp.song_id WHERE sp.playlist_id = ?", (selected_playlist_id,), ) except Exception as e: @@ -714,7 +681,7 @@ class MusicTable(QTableView): try: with DBA.DBAccess() as db: data = db.query( - "SELECT id, title, artist, album, track_number, genre, codec, album_date, filepath FROM song;", + f"SELECT id, {fields} FROM song;", (), ) except Exception as e: @@ -835,7 +802,9 @@ class MusicTable(QTableView): selected_rows = self.get_selected_rows() filepaths = [] for row in selected_rows: - idx = self.proxymodel.index(row, self.table_headers.index("path")) + idx = self.proxymodel.index( + row, list(self.headers.gui.values()).index("path") + ) filepaths.append(idx.data()) return filepaths @@ -874,7 +843,9 @@ class MusicTable(QTableView): def set_selected_song_filepath(self) -> None: """Sets the filepath of the currently selected song""" self.selected_song_filepath = ( - self.currentIndex().siblingAtColumn(self.table_headers.index("path")).data() + self.currentIndex() + .siblingAtColumn(list(self.headers.gui.values()).index("path")) + .data() ) def set_current_song_filepath(self) -> None: @@ -887,7 +858,7 @@ class MusicTable(QTableView): # update the filepath self.current_song_filepath: str = ( self.current_song_qmodel_index.siblingAtColumn( - self.table_headers.index("path") + list(self.headers.gui.values()).index("path") ).data() ) diff --git a/components/__init__.py b/components/__init__.py index 4c2672f..d3cd0a4 100644 --- a/components/__init__.py +++ b/components/__init__.py @@ -10,3 +10,4 @@ from .CreatePlaylistWindow import CreatePlaylistWindow from .PlaylistsPane import PlaylistsPane from .ExportPlaylistWindow import ExportPlaylistWindow from .QuestionBoxDetails import QuestionBoxDetails +from .HeaderTags import HeaderTags diff --git a/utils/add_files_to_database.py b/utils/add_files_to_database.py index 84c680c..47f659d 100644 --- a/utils/add_files_to_database.py +++ b/utils/add_files_to_database.py @@ -55,7 +55,7 @@ def add_files_to_database(files, progress_callback=None): filename.split(".")[-1], audio["date"], audio["bitrate"], - audio["length"] + audio["length"], ) ) # Check if batch size is reached @@ -80,5 +80,3 @@ def add_files_to_database(files, progress_callback=None): insert_data, ) return True, failed_dict - - diff --git a/utils/get_tags.py b/utils/get_tags.py index 11a3b4a..1f89511 100644 --- a/utils/get_tags.py +++ b/utils/get_tags.py @@ -56,6 +56,13 @@ def id3_remap(audio: MP3 | ID3 | FLAC) -> dict: "lyrics": lyrics, "length": int(round(audio.info.length, 0)), } + for k, v in remap.items(): + # we get crap like this if the tag exists + # {'title': TIT2(encoding=, text=['18']), + if v is None: + continue + if not isinstance(v, str) and not isinstance(v, int): + remap[k] = v.text[0] return remap diff --git a/utils/id3_tag_mapping.py b/utils/id3_tag_mapping.py deleted file mode 100644 index 86742de..0000000 --- a/utils/id3_tag_mapping.py +++ /dev/null @@ -1,13 +0,0 @@ -id3_tag_mapping = { - "TIT2": "title", - "TPE1": "artist", - "TALB": "album", - "TPE2": "album_artist", - "TCON": "genre", - "TRCK": "track_number", - "TDRC": "album_date", - # "TYER": "year", - # "TDAT": "date", - # "APIC": "album_cover", - "TCOP": "copyright", -} diff --git a/utils/init.sql b/utils/init.sql index 58cae21..68d6ff2 100644 --- a/utils/init.sql +++ b/utils/init.sql @@ -9,6 +9,7 @@ CREATE TABLE song( title varchar(255), album varchar(255), artist varchar(255), + album_artist varchar(255), track_number integer, length_seconds integer, genre varchar(255), diff --git a/utils/set_tag.py b/utils/set_tag.py index 3eba17c..a5da21f 100644 --- a/utils/set_tag.py +++ b/utils/set_tag.py @@ -1,9 +1,7 @@ from logging import debug, error -from components import ErrorDialog -from utils.id3_tag_mapping import id3_tag_mapping -from utils.convert_date_str_to_tyer_tdat_id3_tag import ( - convert_date_str_to_tyer_tdat_id3_tag, -) +from components import ErrorDialog, HeaderTags + +# from utils import convert_date_str_to_tyer_tdat_id3_tag from mutagen.id3 import ID3 from mutagen.id3._util import ID3NoHeaderError from mutagen.id3._frames import ( @@ -54,7 +52,7 @@ mutagen_id3_tag_mapping = { # "year": TYER, # Year of recording # "date": TDAT, # Date "lyrics": USLT, # Unsynchronized lyric/text transcription - "track": TRCK, # Track number/Position in set + "track_number": TRCK, # Track number/Position in set "album_cover": APIC, # Attached picture "composer": TCOM, # Composer "conductor": TPE3, # Conductor/performer refinement @@ -92,6 +90,7 @@ def set_tag(filepath: str, tag_name: str, value: str): Returns: True / False""" + headers = HeaderTags() # debug(f"filepath: {filepath} | tag_name: {tag_name} | value: {value}") try: @@ -119,13 +118,14 @@ def set_tag(filepath: str, tag_name: str, value: str): audio.add(frame) audio.save() return True - # Convert any tag (as string or name, or whatever) into the Mutagen Frame object - if tag_name in id3_tag_mapping: - tag_name = id3_tag_mapping[tag_name] + # Convert any ID3 tag or nice name (that i chose) into into the Mutagen Frame object + if tag_name in list(headers.id3_keys.values()): + tag_nice_name = headers.id3_keys[tag_name] + else: + tag_nice_name = tag_name # Other - if tag_name in mutagen_id3_tag_mapping: # Tag accounted for - # debug(f"tag_name = {tag_name}") - tag_class = mutagen_id3_tag_mapping[tag_name] + if tag_nice_name in mutagen_id3_tag_mapping: # Tag accounted for + tag_class = mutagen_id3_tag_mapping[tag_nice_name] if issubclass(tag_class, Frame): frame = tag_class(encoding=3, text=[value]) audio_file.add(frame) # Add the tag