id3 better

This commit is contained in:
tsi-billypom 2025-04-18 10:50:34 -04:00
parent 8bfc269f42
commit 53fed6e2b9
9 changed files with 117 additions and 99 deletions

57
components/HeaderTags.py Normal file
View File

@ -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",
]

View File

@ -9,10 +9,8 @@ from PyQt5.QtWidgets import (
QPushButton, QPushButton,
) )
from PyQt5.QtGui import QFont from PyQt5.QtGui import QFont
from PyQt5.QtCore import pyqtSignal
from mutagen.id3 import ID3
from components.ErrorDialog import ErrorDialog 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 from logging import debug
# import re # import re
@ -30,7 +28,7 @@ class ID3LineEdit(QLineEdit):
class MetadataWindow(QDialog): 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 Window that allows batch editing of metadata for multiple files
@ -42,7 +40,7 @@ class MetadataWindow(QDialog):
super(MetadataWindow, self).__init__() super(MetadataWindow, self).__init__()
self.refreshMusicTableSignal = refreshMusicTableSignal self.refreshMusicTableSignal = refreshMusicTableSignal
self.songs = list(zip(songs, ids)) 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 # Keep a dictionary of the input fields for save function
self.input_fields = {} self.input_fields = {}
self.setWindowTitle("Edit metadata") self.setWindowTitle("Edit metadata")
@ -62,7 +60,7 @@ class MetadataWindow(QDialog):
# Get a dict of all tags for all songs # Get a dict of all tags for all songs
# e.g., { "TIT2": ["song_title1", "song_title2"], ... } # e.g., { "TIT2": ["song_title1", "song_title2"], ... }
for song in self.songs: for song in self.songs:
song_data = get_tags(song[0]) song_data = get_tags(song[0])[0]
if not song_data: if not song_data:
QMessageBox.error( QMessageBox.error(
self, self,
@ -72,25 +70,23 @@ class MetadataWindow(QDialog):
QMessageBox.Ok, QMessageBox.Ok,
) )
return return
for tag in self.id3_tag_mapping: for key, tag in self.headers.id3.items():
try: if key not in self.headers.editable_db_tags:
_ = tag_sets[tag] continue
except KeyError: if tag is not None:
# If a tag doesn't exist in our dict, create an empty list try:
tag_sets[tag] = [] _ = tag_sets[tag]
try: except KeyError:
tag_sets[tag].append(song_data[tag].text[0]) # If a tag doesn't exist in our dict, create an empty list
except KeyError: tag_sets[tag] = []
pass try:
# debug("tag sets:") tag_sets[tag].append(song_data[tag].text[0])
# debug(tag_sets) except KeyError:
pass
# UI Creation # UI Creation
current_layout = QHBoxLayout() current_layout = QHBoxLayout()
for idx, (tag, value) in enumerate(tag_sets.items()): for idx, (tag, value) in enumerate(tag_sets.items()):
# Layout creation
# if idx == 0:
# pass
if idx % 2 == 0: if idx % 2 == 0:
# Make a new horizontal layout for every 2 items # Make a new horizontal layout for every 2 items
layout.addLayout(current_layout) layout.addLayout(current_layout)
@ -101,7 +97,7 @@ class MetadataWindow(QDialog):
# If the ID3 tag is the same for every item we're editing # If the ID3 tag is the same for every item we're editing
field_text = str(value[0]) if value else "" field_text = str(value[0]) if value else ""
# Normal field # 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 = ID3LineEdit(field_text, tag)
input_field.setStyleSheet(None) input_field.setStyleSheet(None)
else: else:
@ -109,7 +105,7 @@ class MetadataWindow(QDialog):
# this means the metadata differs between the selected items for this tag # this means the metadata differs between the selected items for this tag
# so be careful...dangerous # so be careful...dangerous
field_text = "" 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 = ID3LineEdit(field_text, tag)
input_field.setStyleSheet("border: 1px solid red") input_field.setStyleSheet("border: 1px solid red")
# Save each input field to our dict for saving # Save each input field to our dict for saving
@ -138,7 +134,7 @@ class MetadataWindow(QDialog):
if success: if success:
update_song_in_database( update_song_in_database(
song[1], song[1],
edited_column_name=self.id3_tag_mapping[tag], edited_column_name=self.headers.id3_keys[tag],
user_input_data=field.text(), user_input_data=field.text(),
) )
else: else:

View File

@ -40,6 +40,7 @@ from components.LyricsWindow import LyricsWindow
from components.AddToPlaylistWindow import AddToPlaylistWindow from components.AddToPlaylistWindow import AddToPlaylistWindow
from components.MetadataWindow import MetadataWindow from components.MetadataWindow import MetadataWindow
from components.QuestionBoxDetails import QuestionBoxDetails from components.QuestionBoxDetails import QuestionBoxDetails
from components.HeaderTags import HeaderTags
from main import Worker from main import Worker
from utils import ( from utils import (
@ -51,7 +52,7 @@ from utils import (
get_album_art, get_album_art,
id3_remap, id3_remap,
get_tags, get_tags,
set_tag set_tag,
) )
from subprocess import Popen from subprocess import Popen
from logging import debug, error from logging import debug, error
@ -63,43 +64,6 @@ from appdirs import user_config_dir
from configparser import ConfigParser 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): class MusicTable(QTableView):
playlistStatsSignal = pyqtSignal(str) playlistStatsSignal = pyqtSignal(str)
playPauseSignal = pyqtSignal() playPauseSignal = pyqtSignal()
@ -142,7 +106,7 @@ class MusicTable(QTableView):
# Threads # Threads
self.threadpool = QThreadPool self.threadpool = QThreadPool
# headers class thing # headers class thing
self.headers = TableHeader() self.headers = HeaderTags()
# db names of headers # db names of headers
self.database_columns = str(self.config["table"]["columns"]).split(",") self.database_columns = str(self.config["table"]["columns"]).split(",")
@ -376,7 +340,7 @@ class MusicTable(QTableView):
def on_sort(self): def on_sort(self):
debug("on_sort") 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( selected_qmodel_index = self.find_qmodel_index_by_value(
self.model2, search_col_num, self.selected_song_filepath 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""" """Opens a form with metadata from the selected audio files"""
files = self.get_selected_songs_filepaths() files = self.get_selected_songs_filepaths()
song_ids = self.get_selected_songs_db_ids() 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.refreshMusicTableSignal.connect(self.load_music_table)
window.exec_() # Display the preferences window modally window.exec_() # Display the preferences window modally
@ -697,14 +663,15 @@ class MusicTable(QTableView):
self.disconnect_layout_changed() self.disconnect_layout_changed()
self.vertical_scroll_position = self.verticalScrollBar().value() # type: ignore self.vertical_scroll_position = self.verticalScrollBar().value() # type: ignore
self.model2.clear() 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 if playlist_id: # Load a playlist
# Fetch playlist data # Fetch playlist data
selected_playlist_id = playlist_id[0] selected_playlist_id = playlist_id[0]
try: try:
with DBA.DBAccess() as db: with DBA.DBAccess() as db:
data = db.query( 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,), (selected_playlist_id,),
) )
except Exception as e: except Exception as e:
@ -714,7 +681,7 @@ class MusicTable(QTableView):
try: try:
with DBA.DBAccess() as db: with DBA.DBAccess() as db:
data = db.query( 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: except Exception as e:
@ -835,7 +802,9 @@ class MusicTable(QTableView):
selected_rows = self.get_selected_rows() selected_rows = self.get_selected_rows()
filepaths = [] filepaths = []
for row in selected_rows: 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()) filepaths.append(idx.data())
return filepaths return filepaths
@ -874,7 +843,9 @@ class MusicTable(QTableView):
def set_selected_song_filepath(self) -> None: def set_selected_song_filepath(self) -> None:
"""Sets the filepath of the currently selected song""" """Sets the filepath of the currently selected song"""
self.selected_song_filepath = ( 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: def set_current_song_filepath(self) -> None:
@ -887,7 +858,7 @@ class MusicTable(QTableView):
# update the filepath # update the filepath
self.current_song_filepath: str = ( self.current_song_filepath: str = (
self.current_song_qmodel_index.siblingAtColumn( self.current_song_qmodel_index.siblingAtColumn(
self.table_headers.index("path") list(self.headers.gui.values()).index("path")
).data() ).data()
) )

View File

@ -10,3 +10,4 @@ from .CreatePlaylistWindow import CreatePlaylistWindow
from .PlaylistsPane import PlaylistsPane from .PlaylistsPane import PlaylistsPane
from .ExportPlaylistWindow import ExportPlaylistWindow from .ExportPlaylistWindow import ExportPlaylistWindow
from .QuestionBoxDetails import QuestionBoxDetails from .QuestionBoxDetails import QuestionBoxDetails
from .HeaderTags import HeaderTags

View File

@ -55,7 +55,7 @@ def add_files_to_database(files, progress_callback=None):
filename.split(".")[-1], filename.split(".")[-1],
audio["date"], audio["date"],
audio["bitrate"], audio["bitrate"],
audio["length"] audio["length"],
) )
) )
# Check if batch size is reached # Check if batch size is reached
@ -80,5 +80,3 @@ def add_files_to_database(files, progress_callback=None):
insert_data, insert_data,
) )
return True, failed_dict return True, failed_dict

View File

@ -56,6 +56,13 @@ def id3_remap(audio: MP3 | ID3 | FLAC) -> dict:
"lyrics": lyrics, "lyrics": lyrics,
"length": int(round(audio.info.length, 0)), "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=<Encoding.UTF8: 3>, text=['18']),
if v is None:
continue
if not isinstance(v, str) and not isinstance(v, int):
remap[k] = v.text[0]
return remap return remap

View File

@ -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",
}

View File

@ -9,6 +9,7 @@ CREATE TABLE song(
title varchar(255), title varchar(255),
album varchar(255), album varchar(255),
artist varchar(255), artist varchar(255),
album_artist varchar(255),
track_number integer, track_number integer,
length_seconds integer, length_seconds integer,
genre varchar(255), genre varchar(255),

View File

@ -1,9 +1,7 @@
from logging import debug, error from logging import debug, error
from components import ErrorDialog from components import ErrorDialog, HeaderTags
from utils.id3_tag_mapping import id3_tag_mapping
from utils.convert_date_str_to_tyer_tdat_id3_tag import ( # from utils import convert_date_str_to_tyer_tdat_id3_tag
convert_date_str_to_tyer_tdat_id3_tag,
)
from mutagen.id3 import ID3 from mutagen.id3 import ID3
from mutagen.id3._util import ID3NoHeaderError from mutagen.id3._util import ID3NoHeaderError
from mutagen.id3._frames import ( from mutagen.id3._frames import (
@ -54,7 +52,7 @@ mutagen_id3_tag_mapping = {
# "year": TYER, # Year of recording # "year": TYER, # Year of recording
# "date": TDAT, # Date # "date": TDAT, # Date
"lyrics": USLT, # Unsynchronized lyric/text transcription "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 "album_cover": APIC, # Attached picture
"composer": TCOM, # Composer "composer": TCOM, # Composer
"conductor": TPE3, # Conductor/performer refinement "conductor": TPE3, # Conductor/performer refinement
@ -92,6 +90,7 @@ def set_tag(filepath: str, tag_name: str, value: str):
Returns: Returns:
True / False""" True / False"""
headers = HeaderTags()
# debug(f"filepath: {filepath} | tag_name: {tag_name} | value: {value}") # debug(f"filepath: {filepath} | tag_name: {tag_name} | value: {value}")
try: try:
@ -119,13 +118,14 @@ def set_tag(filepath: str, tag_name: str, value: str):
audio.add(frame) audio.add(frame)
audio.save() audio.save()
return True return True
# Convert any tag (as string or name, or whatever) into the Mutagen Frame object # Convert any ID3 tag or nice name (that i chose) into into the Mutagen Frame object
if tag_name in id3_tag_mapping: if tag_name in list(headers.id3_keys.values()):
tag_name = id3_tag_mapping[tag_name] tag_nice_name = headers.id3_keys[tag_name]
else:
tag_nice_name = tag_name
# Other # Other
if tag_name in mutagen_id3_tag_mapping: # Tag accounted for if tag_nice_name in mutagen_id3_tag_mapping: # Tag accounted for
# debug(f"tag_name = {tag_name}") tag_class = mutagen_id3_tag_mapping[tag_nice_name]
tag_class = mutagen_id3_tag_mapping[tag_name]
if issubclass(tag_class, Frame): if issubclass(tag_class, Frame):
frame = tag_class(encoding=3, text=[value]) frame = tag_class(encoding=3, text=[value])
audio_file.add(frame) # Add the tag audio_file.add(frame) # Add the tag