id3 better
This commit is contained in:
parent
8bfc269f42
commit
53fed6e2b9
57
components/HeaderTags.py
Normal file
57
components/HeaderTags.py
Normal 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",
|
||||
]
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
)
|
||||
|
||||
|
||||
@ -10,3 +10,4 @@ from .CreatePlaylistWindow import CreatePlaylistWindow
|
||||
from .PlaylistsPane import PlaylistsPane
|
||||
from .ExportPlaylistWindow import ExportPlaylistWindow
|
||||
from .QuestionBoxDetails import QuestionBoxDetails
|
||||
from .HeaderTags import HeaderTags
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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",
|
||||
}
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user