moving id3 tags and headers to dataclass implementation

This commit is contained in:
tsi-billypom 2025-10-03 16:50:15 -04:00
parent bc24d6378a
commit 84f8563671
4 changed files with 133 additions and 124 deletions

View File

@ -1,31 +1,129 @@
from configparser import ConfigParser from configparser import ConfigParser
from pathlib import Path from pathlib import Path
from typing import Optional
from appdirs import user_config_dir from appdirs import user_config_dir
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict
from mutagen.id3._frames import (
TIT2,
TPE1,
TALB,
TRCK,
TDRC,
# TYER,
TLEN,
TCON,
TPOS,
COMM,
TPE2,
TCOM,
TPE3,
TPE4,
TCOP,
WOAR,
USLT,
APIC,
TENC,
TBPM,
TKEY,
# TDAT,
TIME,
TSSE,
TOPE,
TEXT,
TOLY,
TORY,
TPUB,
WCOM,
WCOP,
WOAS,
WORS,
WPAY,
WPUB,
)
mutagen_id3_tag_mapping = {
"title": TIT2, # Title/song name/content description
"artist": TPE1, # Lead performer(s)/Soloist(s)
"album": TALB, # Album/Movie/Show title
"album_artist": TPE2, # Band/orchestra/accompaniment
"genre": TCON, # Content type
"album_date": TDRC,
# "year": TYER, # Year of recording
# "date": TDAT, # Date
"lyrics": USLT, # Unsynchronized lyric/text transcription
"track_number": TRCK, # Track number/Position in set
"album_cover": APIC, # Attached picture
"composer": TCOM, # Composer
"conductor": TPE3, # Conductor/performer refinement
"remixed_by": TPE4, # Interpreted, remixed, or otherwise modified by
"part_of_a_set": TPOS, # Part of a set
"comments": COMM, # Comments
"copyright": TCOP, # Copyright message
"url_artist": WOAR, # Official artist/performer webpage
"encoded_by": TENC, # Encoded by
"bpm": TBPM, # BPM (beats per minute)
"initial_key": TKEY, # Initial key
"time": TIME, # Time
"encoding_settings": TSSE, # Software/Hardware and settings used for encoding
"original_artist": TOPE, # Original artist(s)/performer(s)
"lyricist": TEXT, # Lyricist/Text writer
"original_lyricist": TOLY, # Original lyricist(s)/text writer(s)
"original_release_year": TORY, # Original release year
"publisher": TPUB, # Publisher
"commercial_info": WCOM, # Commercial information
"copyright_info": WCOP, # Copyright information
"official_audio_source_url": WOAS, # Official audio source webpage
"official_internet_radio_station_homepage": WORS, # Official Internet radio station homepage
"payment_url": WPAY, # Payment (URL)
"publishers_official_webpage": WPUB, # Publishers official webpage
}
@dataclass @dataclass
class SQLiteMap: class SQLiteMap:
title: str | None = None title: str | TIT2 | None = None
artist: str | None = None artist: str | TPE1 | None = None
album: str | None = None album: str | TALB | None = None
album_artist: str | None = None album_artist: str | TPE2 | None = None
track_number: str | None = None track_number: str | TRCK | None = None
genre: str | None = None genre: str | TCON | None = None
length_seconds: str | None = None length_ms: str | TLEN | None = None
album_date: str | None = None album_date: str | TDRC | None = None
codec: str | None = None codec: str | None = None
filepath: str | None = None filepath: str | None = None
bitrate: str | None = None bitrate: str | None = None
# lyrics: str | USLT | None = None
# album_art: str | APIC | None = None
@dataclass
class ID3Field:
db: str # e.g., "title"
gui: str # e.g., "Title"
frame_class: Optional[type] = None # e.g., TPE1
frame_id: Optional[str] = None # e.g., "TPE1"
""" class HeaderTags2:
db names are called FIELDS (e.g., title, track_number, length_seconds) def __init__(self):
gui names are called HEADERS (e.g., title, track, length, year) self.headers = [
id3 names are called TAGS (e.g., TIT2, TPE1, TALB) ID3Field(frame_class=TIT2, frame_id="TIT2", db="title", gui="Title"),
ID3Field(frame_class=TPE1, frame_id="TPE1", db="artist", gui="Artist"),
is dataclasses rly worth it? ID3Field(frame_class=TALB, frame_id="TALB", db="album", gui="Album"),
""" ID3Field(frame_class=TPE2, frame_id="TPE2", db="album_artist", gui="Album Artist"),
ID3Field(frame_class=TRCK, frame_id="TRCK", db="track_number", gui="Track"),
ID3Field(frame_class=TCON, frame_id="TCON", db="genre", gui="Genre"),
ID3Field(frame_class=TLEN, frame_id="TLEN", db="length_ms", gui="Time"),
ID3Field(frame_class=TDRC, frame_id="TDRC", db="album_date", gui="Year"),
ID3Field(db="codec", gui="Codec"),
ID3Field(db="filepath", gui="Filepath"),
ID3Field(db="bitrate", gui="Bitrate"),
]
# Lookup dicts
# - Usage example: frame_id['TPE1'].db # => "artist"
self.frame_id = {f.frame_id: f for f in self.headers}
self.db = {f.db: f for f in self.headers}
self.gui = {f.gui: f for f in self.headers}
class HeaderTags: class HeaderTags:
@ -69,7 +167,7 @@ class HeaderTags:
genre="genre", genre="genre",
album_date="year", album_date="year",
codec="codec", codec="codec",
length_seconds="length", length_ms="length",
filepath="path", filepath="path",
bitrate="bitrate", bitrate="bitrate",
) )

View File

@ -11,6 +11,6 @@ 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 from .HeaderTags import HeaderTags, HeaderTags2
from .MediaPlayer import MediaPlayer from .MediaPlayer import MediaPlayer
from .SearchLineEdit import SearchLineEdit from .SearchLineEdit import SearchLineEdit

View File

@ -4,7 +4,6 @@ import logging
from PyQt5 import QtCore from PyQt5 import QtCore
import typing import typing
import DBA import DBA
import qdarktheme
from subprocess import run from subprocess import run
# from pyqtgraph import mkBrush # from pyqtgraph import mkBrush
from mutagen.id3 import ID3 from mutagen.id3 import ID3
@ -737,7 +736,7 @@ if __name__ == "__main__":
clipboard = app.clipboard() clipboard = app.clipboard()
# Dark theme >:3 # Dark theme >:3
# qdarktheme.setup_theme() # qdarktheme.setup_theme()
qdarktheme.setup_theme("auto") # this is supposed to work but doesnt # qdarktheme.setup_theme("auto") # this is supposed to work but doesnt
# Show the UI # Show the UI
ui = ApplicationWindow(clipboard) ui = ApplicationWindow(clipboard)
# window size # window size

View File

@ -1,118 +1,32 @@
from logging import debug, error, warning from logging import debug, error, warning
from components import ErrorDialog from components import ErrorDialog
from components.HeaderTags import HeaderTags from components.HeaderTags import HeaderTags2
# from utils import 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 USLT, Frame
Frame,
TIT2,
TPE1,
TALB,
TRCK,
TDRC,
# TYER,
TCON,
TPOS,
COMM,
TPE2,
TCOM,
TPE3,
TPE4,
TCOP,
WOAR,
USLT,
APIC,
TENC,
TBPM,
TKEY,
# TDAT,
TIME,
TSSE,
TOPE,
TEXT,
TOLY,
TORY,
TPUB,
WCOM,
WCOP,
WOAS,
WORS,
WPAY,
WPUB,
)
mutagen_id3_tag_mapping = { def set_tag(filepath: str, db_column: str, value: str):
"title": TIT2, # Title/song name/content description
"artist": TPE1, # Lead performer(s)/Soloist(s)
"album": TALB, # Album/Movie/Show title
"album_artist": TPE2, # Band/orchestra/accompaniment
"genre": TCON, # Content type
"album_date": TDRC,
# "year": TYER, # Year of recording
# "date": TDAT, # Date
"lyrics": USLT, # Unsynchronized lyric/text transcription
"track_number": TRCK, # Track number/Position in set
"album_cover": APIC, # Attached picture
"composer": TCOM, # Composer
"conductor": TPE3, # Conductor/performer refinement
"remixed_by": TPE4, # Interpreted, remixed, or otherwise modified by
"part_of_a_set": TPOS, # Part of a set
"comments": COMM, # Comments
"copyright": TCOP, # Copyright message
"url_artist": WOAR, # Official artist/performer webpage
"encoded_by": TENC, # Encoded by
"bpm": TBPM, # BPM (beats per minute)
"initial_key": TKEY, # Initial key
"time": TIME, # Time
"encoding_settings": TSSE, # Software/Hardware and settings used for encoding
"original_artist": TOPE, # Original artist(s)/performer(s)
"lyricist": TEXT, # Lyricist/Text writer
"original_lyricist": TOLY, # Original lyricist(s)/text writer(s)
"original_release_year": TORY, # Original release year
"publisher": TPUB, # Publisher
"commercial_info": WCOM, # Commercial information
"copyright_info": WCOP, # Copyright information
"official_audio_source_url": WOAS, # Official audio source webpage
"official_internet_radio_station_homepage": WORS, # Official Internet radio station homepage
"payment_url": WPAY, # Payment (URL)
"publishers_official_webpage": WPUB, # Publishers official webpage
}
def set_tag(filepath: str, tag_name: str, value: str):
""" """
Sets the ID3 tag for a file given a filepath, tag_name, and a value for the tag Sets the ID3 tag for a file given a filepath, db_column, and a value for the tag
Args: Args:
filepath: path to the mp3 file filepath: path to the mp3 file
tag_name: common name of the ID3 tag db_column: db column name of the ID3 tag
value: value to set for the tag value: value to set for the tag
Returns: Returns:
True / False True / False
""" """
headers = HeaderTags() headers = HeaderTags2()
debug(f"filepath: {filepath} | tag_name: {tag_name} | value: {value}") debug(f"filepath: {filepath} | db_column: {db_column} | value: {value}")
try: try:
try: # Load existing tags try: # Load existing tags
audio_file = ID3(filepath) audio_file = ID3(filepath)
except ID3NoHeaderError: # Create new tags if none exist except ID3NoHeaderError: # Create new tags if none exist
audio_file = ID3() audio_file = ID3()
# Date handling - TDRC vs TYER+TDAT # Lyrics get handled differently
# if tag_name == "album_date": if db_column == "lyrics":
# tyer_tag, tdat_tag = convert_date_str_to_tyer_tdat_id3_tag(value)
# # always update TYER
# audio_file.add(tyer_tag)
# if tdat_tag:
# # update TDAT if we have it
# audio_file.add(tdat_tag)
# Lyrics
# if tag_name == "lyrics" or tag_name == "USLT":
if tag_name == "lyrics":
try: try:
audio = ID3(filepath) audio = ID3(filepath)
except Exception as e: except Exception as e:
@ -123,17 +37,15 @@ 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 ID3 tag nice name (that i chose) into into the Mutagen Frame object # DB Tag into Mutagen Frame Class
if tag_name in mutagen_id3_tag_mapping: # Tag accounted for if db_column in headers.db:
tag_class = mutagen_id3_tag_mapping[tag_name] frame_class = headers.db[db_column].frame_class
if issubclass(tag_class, Frame): assert frame_class is not None # ooo scary
frame = tag_class(encoding=3, text=[value]) if issubclass(frame_class, Frame):
audio_file.add(frame) # Add the tag frame = frame_class(encoding=3, text=[value])
else: audio_file.add(frame)
pass
else: else:
warning('nice name tag not found - skipping updating id3 tag') warning(f'Tag "{db_column}" not found - ID3 tag update skipped')
pass
audio_file.save(filepath) audio_file.save(filepath)
return True return True
except Exception as e: except Exception as e: