diff --git a/components/HeaderTags.py b/components/HeaderTags.py index ff71cfa..37c7531 100644 --- a/components/HeaderTags.py +++ b/components/HeaderTags.py @@ -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", ) diff --git a/components/__init__.py b/components/__init__.py index e048de5..2b3c518 100644 --- a/components/__init__.py +++ b/components/__init__.py @@ -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 diff --git a/main.py b/main.py index 636fa15..967f6a2 100644 --- a/main.py +++ b/main.py @@ -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 diff --git a/utils/set_tag.py b/utils/set_tag.py index 10248f7..ac92658 100644 --- a/utils/set_tag.py +++ b/utils/set_tag.py @@ -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 - else: - pass + # 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: - 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: