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):
playlistStatsSignal = pyqtSignal(str)
playPauseSignal = pyqtSignal()
playSignal = pyqtSignal(str)
enterKey = pyqtSignal()
@ -162,7 +163,7 @@ class MusicTable(QTableView):
self.model2.layoutChanged.connect(self.restore_scroll_position)
self.horizontal_header.sectionResized.connect(self.on_header_resized)
# Final actions
self.load_music_table()
# self.load_music_table()
self.setup_keyboard_shortcuts()
self.load_header_widths()
@ -455,7 +456,7 @@ class MusicTable(QTableView):
except IndexError:
pass
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
selected_playlist_id = playlist_id[0]
try:
debug(
f"load_music_table() | selected_playlist_id: {selected_playlist_id}"
)
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 = ?",
@ -712,7 +710,10 @@ class MusicTable(QTableView):
error(f"load_music_table() | Unhandled exception: {e}")
return
# Populate the model
row_count: int = 0
total_time: int = 0 # total time of all songs in seconds
for row_data in data:
row_count += 1
id, *rest_of_data = row_data
# handle different datatypes
items = []
@ -731,6 +732,7 @@ class MusicTable(QTableView):
for item in items:
item.setData(id, Qt.ItemDataRole.UserRole)
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_layout_changed()
@ -739,7 +741,7 @@ class MusicTable(QTableView):
Loads the header widths from the last application close.
"""
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):
for i in range(self.model2.columnCount() - 1):
self.setColumnWidth(i, int(table_view_column_widths[i]))
@ -830,10 +832,6 @@ class MusicTable(QTableView):
"""Returns the selected songs 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:
"""Returns a list of id's for the selected songs"""
indexes = self.selectedIndexes()
@ -854,6 +852,10 @@ class MusicTable(QTableView):
"""Returns the currently playing song's ID3 tags"""
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:
"""Returns the APIC data (album art lol) for the currently playing song"""
return get_album_art(self.current_song_filepath)

21
main.py
View File

@ -37,7 +37,7 @@ from PyQt5.QtCore import (
QThreadPool,
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 utils import (
delete_album_art,
@ -162,7 +162,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
# self.vLayoutAlbumArt.SetFixedSize()
self.status_bar = QStatusBar()
self.permanent_status_label = QLabel("Status...")
self.permanent_status_label = QLabel("")
self.status_bar.addPermanentWidget(self.permanent_status_label)
self.setStatusBar(self.status_bar)
@ -174,6 +174,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
# widget bits
self.album_art_scene: QGraphicsScene = QGraphicsScene()
self.player: QMediaPlayer = QMediaPlayer() # Audio player object
self.playlist: QMediaPlaylist = QMediaPlaylist()
self.probe: QAudioProbe = QAudioProbe() # Gets audio buffer data
self.audio_visualizer: AudioVisualizer = AudioVisualizer(
self.player, self.probe, self.PlotWidget
@ -248,13 +249,15 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
## CONNECTIONS
# 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.playPauseSignal.connect(
self.on_play_clicked
) # Spacebar toggle play/pause signal
self.tableView.handleProgressSignal.connect(self.handle_progress)
self.tableView.playlistStatsSignal.connect(
self.set_permanent_status_bar_message
)
self.tableView.load_music_table()
# playlistTreeView
self.playlistTreeView.playlistChoiceSignal.connect(
@ -289,7 +292,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
for i in range(self.tableView.model2.columnCount()):
list_of_column_widths.append(str(self.tableView.columnWidth(i)))
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["settings"]["volume"] = str(self.current_volume)
self.config["settings"]["window_size"] = (
@ -301,7 +304,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
with open(self.cfg_file, "w") as configfile:
self.config.write(configfile)
except Exception as e:
debug(f'wtf man {e}')
debug(f"wtf man {e}")
if a0 is not None:
super().closeEvent(a0)
@ -327,7 +330,10 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.speedLabel.setText("{:.2f}".format(rate / 50))
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
play_icon = self.style().standardIcon(pixmapi) # type: ignore
pixmapi = QStyle.StandardPixmap.SP_MediaPause
@ -405,7 +411,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
"""
Sets the permanent message label in the status bar
"""
# what does this do?
self.permanent_status_label.setText(message)
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 .safe_get import safe_get
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 .set_id3_tag import set_id3_tag
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
import DBA
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 pathlib import Path
from appdirs import user_config_dir
@ -14,8 +14,12 @@ def add_files_to_database(files, progress_callback=None):
Args:
files: list() of fully qualified paths to audio file(s)
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()
cfg_file = (
@ -23,7 +27,7 @@ def add_files_to_database(files, progress_callback=None):
)
config.read(cfg_file)
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(",")
failed_dict = {}
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)
filename = filepath.split("/")[-1]
audio, details = get_id3_tags(filepath)
# print('got id3 tags')
# print(type(audio))
# print(audio)
if not isinstance(audio, ID3):
tags, details = get_id3_tags(filepath)
if details:
failed_dict[filepath] = details
continue
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 = ""
audio = id3_remap(tags)
# Append data tuple to insert_data list
insert_data.append(
(
filepath,
title,
album,
artist,
track_number,
genre,
audio["title"],
audio["album"],
audio["artist"],
audio["track_number"],
audio["genre"],
filename.split(".")[-1],
date,
bitrate,
audio["date"],
audio["bitrate"],
)
)
# Check if batch size is reached

View File

@ -1,31 +1,29 @@
import os
from logging import debug, error
from mutagen.id3 import ID3
from mutagen.mp3 import MP3
from mutagen.flac import FLAC
from mutagen.id3._frames import TIT2
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"""
try:
# Open the MP3 file and read its content
audio = ID3(filename)
audio = MP3(filename)
except ID3NoHeaderError:
audio = ID3()
audio = MP3()
try:
if os.path.exists(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]
if "TIT2" in list(audio.keys()):
audio.save()
return audio, ""
else:
# if title tag doesnt exist,
# create it and return
if "TIT2" not in list(audio.keys()):
# if title tag doesnt exist, create it - filename
tit2_tag = TIT2(encoding=3, text=[title])
audio["TIT2"] = tit2_tag
# Save the updated tags
@ -33,9 +31,26 @@ def get_mp3_tags(filename: str) -> tuple[ID3 | dict, str]:
return audio, ""
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
Returns a tuple of:
@ -52,7 +67,5 @@ def get_id3_tags(filename: str) -> tuple[ID3 | dict, str]:
if filename.endswith(".mp3"):
tags, details = get_mp3_tags(filename)
else:
tags, details = {}, "non mp3 file"
tags, details = ID3(), "non mp3 file"
return tags, details

View File

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