working on making id3 tags easier to work with

This commit is contained in:
tsi-billypom 2025-04-17 11:55:02 -04:00
parent d3313db6ab
commit 554b06a667
6 changed files with 75 additions and 81 deletions

View File

@ -63,6 +63,7 @@ from configparser import ConfigParser
class MusicTable(QTableView): class MusicTable(QTableView):
playlistStatsSignal = pyqtSignal(str)
playPauseSignal = pyqtSignal() playPauseSignal = pyqtSignal()
playSignal = pyqtSignal(str) playSignal = pyqtSignal(str)
enterKey = pyqtSignal() enterKey = pyqtSignal()
@ -162,7 +163,7 @@ class MusicTable(QTableView):
self.model2.layoutChanged.connect(self.restore_scroll_position) self.model2.layoutChanged.connect(self.restore_scroll_position)
self.horizontal_header.sectionResized.connect(self.on_header_resized) self.horizontal_header.sectionResized.connect(self.on_header_resized)
# Final actions # Final actions
self.load_music_table() # self.load_music_table()
self.setup_keyboard_shortcuts() self.setup_keyboard_shortcuts()
self.load_header_widths() self.load_header_widths()
@ -455,7 +456,7 @@ class MusicTable(QTableView):
except IndexError: except IndexError:
pass pass
except Exception as e: except Exception as e:
debug(f'on_add_files_to_database_finished() | Something went wrong: {e}') debug(f"on_add_files_to_database_finished() | Something went wrong: {e}")
# ____________________ # ____________________
# | | # | |
@ -690,9 +691,6 @@ class MusicTable(QTableView):
# Fetch playlist data # Fetch playlist data
selected_playlist_id = playlist_id[0] selected_playlist_id = playlist_id[0]
try: try:
debug(
f"load_music_table() | selected_playlist_id: {selected_playlist_id}"
)
with DBA.DBAccess() as db: with DBA.DBAccess() as db:
data = db.query( 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 = ?", "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 = ?",
@ -712,7 +710,10 @@ class MusicTable(QTableView):
error(f"load_music_table() | Unhandled exception: {e}") error(f"load_music_table() | Unhandled exception: {e}")
return return
# Populate the model # Populate the model
row_count: int = 0
total_time: int = 0 # total time of all songs in seconds
for row_data in data: for row_data in data:
row_count += 1
id, *rest_of_data = row_data id, *rest_of_data = row_data
# handle different datatypes # handle different datatypes
items = [] items = []
@ -731,6 +732,7 @@ class MusicTable(QTableView):
for item in items: for item in items:
item.setData(id, Qt.ItemDataRole.UserRole) item.setData(id, Qt.ItemDataRole.UserRole)
self.model2.layoutChanged.emit() # emits a signal that the view should be updated self.model2.layoutChanged.emit() # emits a signal that the view should be updated
self.playlistStatsSignal.emit(f"Songs: {row_count} | Total time: {total_time}")
self.connect_data_changed() self.connect_data_changed()
self.connect_layout_changed() self.connect_layout_changed()
@ -739,7 +741,7 @@ class MusicTable(QTableView):
Loads the header widths from the last application close. Loads the header widths from the last application close.
""" """
table_view_column_widths = str(self.config["table"]["column_widths"]).split(",") table_view_column_widths = str(self.config["table"]["column_widths"]).split(",")
debug(f'loaded header widths: {table_view_column_widths}') debug(f"loaded header widths: {table_view_column_widths}")
if not isinstance(table_view_column_widths, list): if not isinstance(table_view_column_widths, list):
for i in range(self.model2.columnCount() - 1): for i in range(self.model2.columnCount() - 1):
self.setColumnWidth(i, int(table_view_column_widths[i])) self.setColumnWidth(i, int(table_view_column_widths[i]))
@ -830,10 +832,6 @@ class MusicTable(QTableView):
"""Returns the selected songs filepath""" """Returns the selected songs filepath"""
return self.selected_song_filepath return self.selected_song_filepath
def get_selected_song_metadata(self) -> ID3 | dict:
"""Returns the selected song's ID3 tags"""
return get_id3_tags(self.selected_song_filepath)[0]
def get_selected_songs_db_ids(self) -> list: def get_selected_songs_db_ids(self) -> list:
"""Returns a list of id's for the selected songs""" """Returns a list of id's for the selected songs"""
indexes = self.selectedIndexes() indexes = self.selectedIndexes()
@ -854,6 +852,10 @@ class MusicTable(QTableView):
"""Returns the currently playing song's ID3 tags""" """Returns the currently playing song's ID3 tags"""
return get_id3_tags(self.current_song_filepath)[0] return get_id3_tags(self.current_song_filepath)[0]
def get_selected_song_metadata(self) -> ID3 | dict:
"""Returns the selected song's ID3 tags"""
return get_id3_tags(self.selected_song_filepath)[0]
def get_current_song_album_art(self) -> bytes: def get_current_song_album_art(self) -> bytes:
"""Returns the APIC data (album art lol) for the currently playing song""" """Returns the APIC data (album art lol) for the currently playing song"""
return get_album_art(self.current_song_filepath) return get_album_art(self.current_song_filepath)

21
main.py
View File

@ -37,7 +37,7 @@ from PyQt5.QtCore import (
QThreadPool, QThreadPool,
QRunnable, QRunnable,
) )
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe, QMediaPlaylist
from PyQt5.QtGui import QClipboard, QCloseEvent, QFont, QPixmap, QResizeEvent from PyQt5.QtGui import QClipboard, QCloseEvent, QFont, QPixmap, QResizeEvent
from utils import ( from utils import (
delete_album_art, delete_album_art,
@ -162,7 +162,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
# self.vLayoutAlbumArt.SetFixedSize() # self.vLayoutAlbumArt.SetFixedSize()
self.status_bar = QStatusBar() self.status_bar = QStatusBar()
self.permanent_status_label = QLabel("Status...") self.permanent_status_label = QLabel("")
self.status_bar.addPermanentWidget(self.permanent_status_label) self.status_bar.addPermanentWidget(self.permanent_status_label)
self.setStatusBar(self.status_bar) self.setStatusBar(self.status_bar)
@ -174,6 +174,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
# widget bits # widget bits
self.album_art_scene: QGraphicsScene = QGraphicsScene() self.album_art_scene: QGraphicsScene = QGraphicsScene()
self.player: QMediaPlayer = QMediaPlayer() # Audio player object self.player: QMediaPlayer = QMediaPlayer() # Audio player object
self.playlist: QMediaPlaylist = QMediaPlaylist()
self.probe: QAudioProbe = QAudioProbe() # Gets audio buffer data self.probe: QAudioProbe = QAudioProbe() # Gets audio buffer data
self.audio_visualizer: AudioVisualizer = AudioVisualizer( self.audio_visualizer: AudioVisualizer = AudioVisualizer(
self.player, self.probe, self.PlotWidget self.player, self.probe, self.PlotWidget
@ -248,13 +249,15 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
## CONNECTIONS ## CONNECTIONS
# tableView # tableView
# self.tableView.doubleClicked.connect(self.play_audio_file)
# self.tableView.enterKey.connect(self.play_audio_file)
self.tableView.playSignal.connect(self.play_audio_file) self.tableView.playSignal.connect(self.play_audio_file)
self.tableView.playPauseSignal.connect( self.tableView.playPauseSignal.connect(
self.on_play_clicked self.on_play_clicked
) # Spacebar toggle play/pause signal ) # Spacebar toggle play/pause signal
self.tableView.handleProgressSignal.connect(self.handle_progress) self.tableView.handleProgressSignal.connect(self.handle_progress)
self.tableView.playlistStatsSignal.connect(
self.set_permanent_status_bar_message
)
self.tableView.load_music_table()
# playlistTreeView # playlistTreeView
self.playlistTreeView.playlistChoiceSignal.connect( self.playlistTreeView.playlistChoiceSignal.connect(
@ -289,7 +292,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
for i in range(self.tableView.model2.columnCount()): for i in range(self.tableView.model2.columnCount()):
list_of_column_widths.append(str(self.tableView.columnWidth(i))) list_of_column_widths.append(str(self.tableView.columnWidth(i)))
column_widths_as_string = ",".join(list_of_column_widths) column_widths_as_string = ",".join(list_of_column_widths)
debug(f'saving column widths: {column_widths_as_string}') debug(f"saving column widths: {column_widths_as_string}")
self.config["table"]["column_widths"] = column_widths_as_string self.config["table"]["column_widths"] = column_widths_as_string
self.config["settings"]["volume"] = str(self.current_volume) self.config["settings"]["volume"] = str(self.current_volume)
self.config["settings"]["window_size"] = ( self.config["settings"]["window_size"] = (
@ -301,7 +304,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
with open(self.cfg_file, "w") as configfile: with open(self.cfg_file, "w") as configfile:
self.config.write(configfile) self.config.write(configfile)
except Exception as e: except Exception as e:
debug(f'wtf man {e}') debug(f"wtf man {e}")
if a0 is not None: if a0 is not None:
super().closeEvent(a0) super().closeEvent(a0)
@ -327,7 +330,10 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.speedLabel.setText("{:.2f}".format(rate / 50)) self.speedLabel.setText("{:.2f}".format(rate / 50))
def on_play_clicked(self) -> None: def on_play_clicked(self) -> None:
"""Updates the Play & Pause buttons when clicked""" """
Plays & pauses the song
Updates the button icons
"""
pixmapi = QStyle.StandardPixmap.SP_MediaPlay pixmapi = QStyle.StandardPixmap.SP_MediaPlay
play_icon = self.style().standardIcon(pixmapi) # type: ignore play_icon = self.style().standardIcon(pixmapi) # type: ignore
pixmapi = QStyle.StandardPixmap.SP_MediaPause pixmapi = QStyle.StandardPixmap.SP_MediaPause
@ -405,7 +411,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
""" """
Sets the permanent message label in the status bar Sets the permanent message label in the status bar
""" """
# what does this do?
self.permanent_status_label.setText(message) self.permanent_status_label.setText(message)
def show_status_bar_message(self, message: str, timeout: int | None = None) -> None: def show_status_bar_message(self, message: str, timeout: int | None = None) -> None:

View File

@ -3,7 +3,7 @@ from .convert_id3_timestamp_to_datetime import convert_id3_timestamp_to_datetime
from .initialize_db import initialize_db from .initialize_db import initialize_db
from .safe_get import safe_get from .safe_get import safe_get
from .get_album_art import get_album_art from .get_album_art import get_album_art
from .get_id3_tags import get_id3_tags from .get_id3_tags import get_id3_tags, id3_remap
from .get_reorganize_vars import get_reorganize_vars from .get_reorganize_vars import get_reorganize_vars
from .set_id3_tag import set_id3_tag from .set_id3_tag import set_id3_tag
from .delete_song_id_from_database import delete_song_id_from_database from .delete_song_id_from_database import delete_song_id_from_database

View File

@ -2,7 +2,7 @@ from PyQt5.QtWidgets import QMessageBox
from mutagen.id3 import ID3 from mutagen.id3 import ID3
import DBA import DBA
from logging import debug from logging import debug
from utils import get_id3_tags, convert_id3_timestamp_to_datetime from utils import get_id3_tags, convert_id3_timestamp_to_datetime, id3_remap
from configparser import ConfigParser from configparser import ConfigParser
from pathlib import Path from pathlib import Path
from appdirs import user_config_dir from appdirs import user_config_dir
@ -14,8 +14,12 @@ def add_files_to_database(files, progress_callback=None):
Args: Args:
files: list() of fully qualified paths to audio file(s) files: list() of fully qualified paths to audio file(s)
progress_callback: emit data for user feedback progress_callback: emit data for user feedback
Returns:
True on success, else False Returns a tuple where the first value is the success state
and the second value is a list of failed to add items
```
(True, {"filename.mp3":"failed because i said so"})
```
""" """
config = ConfigParser() config = ConfigParser()
cfg_file = ( cfg_file = (
@ -23,7 +27,7 @@ def add_files_to_database(files, progress_callback=None):
) )
config.read(cfg_file) config.read(cfg_file)
if not files: if not files:
return False, {'Failure': 'All operations failed in add_files_to_database()'} return False, {"Failure": "All operations failed in add_files_to_database()"}
extensions = config.get("settings", "extensions").split(",") extensions = config.get("settings", "extensions").split(",")
failed_dict = {} failed_dict = {}
insert_data = [] # To store data for batch insert insert_data = [] # To store data for batch insert
@ -33,55 +37,24 @@ def add_files_to_database(files, progress_callback=None):
progress_callback.emit(filepath) progress_callback.emit(filepath)
filename = filepath.split("/")[-1] filename = filepath.split("/")[-1]
audio, details = get_id3_tags(filepath) tags, details = get_id3_tags(filepath)
# print('got id3 tags') if details:
# print(type(audio))
# print(audio)
if not isinstance(audio, ID3):
failed_dict[filepath] = details failed_dict[filepath] = details
continue continue
audio = id3_remap(tags)
try:
title = audio["TIT2"].text[0]
except KeyError:
title = filename
try:
artist = audio["TPE1"].text[0]
except KeyError:
artist = ""
try:
album = audio["TALB"].text[0]
except KeyError:
album = ""
try:
track_number = audio["TRCK"].text[0]
except KeyError:
track_number = None
try:
genre = audio["TCON"].text[0]
except KeyError:
genre = ""
try:
date = convert_id3_timestamp_to_datetime(audio["TDRC"].text[0])
except KeyError:
date = ""
try:
bitrate = audio["TBIT"].text[0]
except KeyError:
bitrate = ""
# Append data tuple to insert_data list # Append data tuple to insert_data list
insert_data.append( insert_data.append(
( (
filepath, filepath,
title, audio["title"],
album, audio["album"],
artist, audio["artist"],
track_number, audio["track_number"],
genre, audio["genre"],
filename.split(".")[-1], filename.split(".")[-1],
date, audio["date"],
bitrate, audio["bitrate"],
) )
) )
# Check if batch size is reached # Check if batch size is reached

View File

@ -1,31 +1,29 @@
import os import os
from logging import debug, error from logging import debug, error
from mutagen.id3 import ID3 from mutagen.id3 import ID3
from mutagen.mp3 import MP3
from mutagen.flac import FLAC
from mutagen.id3._frames import TIT2 from mutagen.id3._frames import TIT2
from mutagen.id3._util import ID3NoHeaderError from mutagen.id3._util import ID3NoHeaderError
def get_mp3_tags(filename: str) -> tuple[ID3 | dict, str]: from utils import convert_id3_timestamp_to_datetime
def get_mp3_tags(filename: str) -> tuple[MP3 | ID3 | FLAC, str]:
"""Get ID3 tags for mp3 file""" """Get ID3 tags for mp3 file"""
try: try:
# Open the MP3 file and read its content # Open the MP3 file and read its content
audio = ID3(filename) audio = MP3(filename)
except ID3NoHeaderError: except ID3NoHeaderError:
audio = ID3() audio = MP3()
try: try:
if os.path.exists(filename): if os.path.exists(filename):
audio.save(os.path.abspath(filename)) audio.save(os.path.abspath(filename))
# NOTE: If 'TIT2' tag is not set, we add it with a default value
# title = filename without extension
title = os.path.splitext(os.path.basename(filename))[0] title = os.path.splitext(os.path.basename(filename))[0]
if "TIT2" in list(audio.keys()): if "TIT2" not in list(audio.keys()):
audio.save() # if title tag doesnt exist, create it - filename
return audio, ""
else:
# if title tag doesnt exist,
# create it and return
tit2_tag = TIT2(encoding=3, text=[title]) tit2_tag = TIT2(encoding=3, text=[title])
audio["TIT2"] = tit2_tag audio["TIT2"] = tit2_tag
# Save the updated tags # Save the updated tags
@ -33,9 +31,26 @@ def get_mp3_tags(filename: str) -> tuple[ID3 | dict, str]:
return audio, "" return audio, ""
except Exception as e: except Exception as e:
return {}, f"Could not assign ID3 tag to file: {e}" return MP3(), f"Could not assign ID3 tag to file: {e}"
def get_id3_tags(filename: str) -> tuple[ID3 | dict, str]:
def id3_remap(audio: MP3 | ID3 | FLAC) -> dict:
"""
Turns an ID3 dict into a normal dict that I the human can use.
with words...
"""
return {
"title": audio["TIT2"].text[0],
"artist": audio["TPE1"].text[0],
"album": audio["TALB"].text[0],
"track_number": audio["TRCK"].text[0],
"genre": audio["TCON"].text[0],
"date": convert_id3_timestamp_to_datetime(audio["TDRC"].text[0]),
"bitrate": audio["TBIT"].text[0],
}
def get_id3_tags(filename: str) -> tuple[MP3 | ID3 | FLAC, str]:
""" """
Get the ID3 tags for an audio file Get the ID3 tags for an audio file
Returns a tuple of: Returns a tuple of:
@ -52,7 +67,5 @@ def get_id3_tags(filename: str) -> tuple[ID3 | dict, str]:
if filename.endswith(".mp3"): if filename.endswith(".mp3"):
tags, details = get_mp3_tags(filename) tags, details = get_mp3_tags(filename)
else: else:
tags, details = {}, "non mp3 file" tags, details = ID3(), "non mp3 file"
return tags, details return tags, details

View File

@ -10,6 +10,7 @@ CREATE TABLE song(
album varchar(255), album varchar(255),
artist varchar(255), artist varchar(255),
track_number integer, track_number integer,
length_seconds integer,
genre varchar(255), genre varchar(255),
codec varchar(15), codec varchar(15),
album_date date, album_date date,