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,
)
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,7 +70,10 @@ class MetadataWindow(QDialog):
QMessageBox.Ok,
)
return
for tag in self.id3_tag_mapping:
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:
@ -82,15 +83,10 @@ class MetadataWindow(QDialog):
tag_sets[tag].append(song_data[tag].text[0])
except KeyError:
pass
# debug("tag sets:")
# debug(tag_sets)
# 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:

View File

@ -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()
)

View File

@ -10,3 +10,4 @@ from .CreatePlaylistWindow import CreatePlaylistWindow
from .PlaylistsPane import PlaylistsPane
from .ExportPlaylistWindow import ExportPlaylistWindow
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],
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

View File

@ -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=<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

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),
album varchar(255),
artist varchar(255),
album_artist varchar(255),
track_number integer,
length_seconds integer,
genre varchar(255),

View File

@ -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