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 pathlib import Path
from typing import Optional
from appdirs import user_config_dir
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
class SQLiteMap:
title: str | None = None
artist: str | None = None
album: str | None = None
album_artist: str | None = None
track_number: str | None = None
genre: str | None = None
length_seconds: str | None = None
album_date: str | None = None
title: str | TIT2 | None = None
artist: str | TPE1 | None = None
album: str | TALB | None = None
album_artist: str | TPE2 | None = None
track_number: str | TRCK | None = None
genre: str | TCON | None = None
length_ms: str | TLEN | None = None
album_date: str | TDRC | None = None
codec: str | None = None
filepath: 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"
"""
db names are called FIELDS (e.g., title, track_number, length_seconds)
gui names are called HEADERS (e.g., title, track, length, year)
id3 names are called TAGS (e.g., TIT2, TPE1, TALB)
is dataclasses rly worth it?
"""
class HeaderTags2:
def __init__(self):
self.headers = [
ID3Field(frame_class=TIT2, frame_id="TIT2", db="title", gui="Title"),
ID3Field(frame_class=TPE1, frame_id="TPE1", db="artist", gui="Artist"),
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:
@ -69,7 +167,7 @@ class HeaderTags:
genre="genre",
album_date="year",
codec="codec",
length_seconds="length",
length_ms="length",
filepath="path",
bitrate="bitrate",
)

View File

@ -11,6 +11,6 @@ from .CreatePlaylistWindow import CreatePlaylistWindow
from .PlaylistsPane import PlaylistsPane
from .ExportPlaylistWindow import ExportPlaylistWindow
from .QuestionBoxDetails import QuestionBoxDetails
from .HeaderTags import HeaderTags
from .HeaderTags import HeaderTags, HeaderTags2
from .MediaPlayer import MediaPlayer
from .SearchLineEdit import SearchLineEdit

View File

@ -4,7 +4,6 @@ import logging
from PyQt5 import QtCore
import typing
import DBA
import qdarktheme
from subprocess import run
# from pyqtgraph import mkBrush
from mutagen.id3 import ID3
@ -737,7 +736,7 @@ if __name__ == "__main__":
clipboard = app.clipboard()
# Dark theme >:3
# 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
ui = ApplicationWindow(clipboard)
# window size

View File

@ -1,118 +1,32 @@
from logging import debug, error, warning
from components import ErrorDialog
from components.HeaderTags import HeaderTags
# from utils import convert_date_str_to_tyer_tdat_id3_tag
from components.HeaderTags import HeaderTags2
from mutagen.id3 import ID3
from mutagen.id3._util import ID3NoHeaderError
from mutagen.id3._frames import (
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,
)
from mutagen.id3._frames import USLT, Frame
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
}
def set_tag(filepath: str, tag_name: str, value: str):
def set_tag(filepath: str, db_column: 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:
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
Returns:
True / False
"""
headers = HeaderTags()
debug(f"filepath: {filepath} | tag_name: {tag_name} | value: {value}")
headers = HeaderTags2()
debug(f"filepath: {filepath} | db_column: {db_column} | value: {value}")
try:
try: # Load existing tags
audio_file = ID3(filepath)
except ID3NoHeaderError: # Create new tags if none exist
audio_file = ID3()
# Date handling - TDRC vs TYER+TDAT
# if tag_name == "album_date":
# 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":
# Lyrics get handled differently
if db_column == "lyrics":
try:
audio = ID3(filepath)
except Exception as e:
@ -123,17 +37,15 @@ def set_tag(filepath: str, tag_name: str, value: str):
audio.add(frame)
audio.save()
return True
# Convert ID3 tag nice name (that i chose) into into the Mutagen Frame object
if tag_name in mutagen_id3_tag_mapping: # Tag accounted for
tag_class = mutagen_id3_tag_mapping[tag_name]
if issubclass(tag_class, Frame):
frame = tag_class(encoding=3, text=[value])
audio_file.add(frame) # Add the tag
# DB Tag into Mutagen Frame Class
if db_column in headers.db:
frame_class = headers.db[db_column].frame_class
assert frame_class is not None # ooo scary
if issubclass(frame_class, Frame):
frame = frame_class(encoding=3, text=[value])
audio_file.add(frame)
else:
pass
else:
warning('nice name tag not found - skipping updating id3 tag')
pass
warning(f'Tag "{db_column}" not found - ID3 tag update skipped')
audio_file.save(filepath)
return True
except Exception as e: