F2 table updating ID3 tagging and reorganization function slightly functional

This commit is contained in:
billypom on debian 2024-03-27 20:40:48 -04:00
parent ef02d30d05
commit 7c4ef7e3fa
9 changed files with 179 additions and 23 deletions

17
components/ErrorDialog.py Normal file
View File

@ -0,0 +1,17 @@
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton
class ErrorDialog(QDialog):
def __init__(self, message, parent=None):
super().__init__(parent)
self.setWindowTitle("An error occurred")
self.setGeometry(100,100,400,200)
layout = QVBoxLayout()
self.label = QLabel(message)
layout.addWidget(self.label)
self.button = QPushButton("ok")
self.button.clicked.connect(self.accept) # press ok
layout.addWidget(self.button)
self.setLayout(layout)

View File

@ -1,12 +1,16 @@
from mutagen.easyid3 import EasyID3
import DBA
from PyQt5.QtGui import QStandardItem, QStandardItemModel, QKeySequence
from PyQt5.QtWidgets import QTableView, QShortcut, QMessageBox, QAbstractItemView
from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal
from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal, QTimer
from utils import add_files_to_library
from utils import get_id3_tags
from utils import get_album_art
from utils import set_id3_tag
import logging
import configparser
import os
import shutil
class MusicTable(QTableView):
@ -19,8 +23,10 @@ class MusicTable(QTableView):
self.setModel(self.model) # Same as above
self.config = configparser.ConfigParser()
self.config.read('config.ini')
self.headers = ['title', 'artist', 'album', 'genre', 'codec', 'year', 'path'] # gui names of headers
self.columns = str(self.config['table']['columns']).split(',') # db names of headers
self.table_headers = ['title', 'artist', 'album', 'genre', 'codec', 'year', 'path'] # gui names of headers
self.id3_headers = ['title', 'artist', 'album', 'content_type', ]
self.database_columns = str(self.config['table']['columns']).split(',') # db names of headers
self.vertical_scroll_position = 0
self.songChanged = None
self.selected_song_filepath = None
self.current_song_filepath = None
@ -32,6 +38,7 @@ class MusicTable(QTableView):
self.fetch_library()
self.setup_keyboard_shortcuts()
self.model.dataChanged.connect(self.on_cell_data_changed) # editing cells
self.model.layoutChanged.connect(self.restore_scroll_position)
def setup_keyboard_shortcuts(self):
@ -47,10 +54,11 @@ class MusicTable(QTableView):
filepath_index = self.model.index(topLeft.row(), filepath_column_idx) # exact index of the edited cell
filepath = self.model.data(filepath_index) # filepath
# update the ID3 information
new_data = topLeft.data()
edited_column = self.columns[topLeft.column()]
print(filepath)
print(edited_column)
user_input_data = topLeft.data()
edited_column_name = self.database_columns[topLeft.column()]
response = set_id3_tag(filepath, edited_column_name, user_input_data)
if response:
update_song_in_library(filepath, edited_column_name, user_input_data)
def reorganize_selected_files(self):
@ -60,12 +68,29 @@ class MusicTable(QTableView):
# Confirmation screen (yes, no)
reply = QMessageBox.question(self, 'Confirmation', 'Are you sure you want to reorganize these files?', QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
if reply:
target_dir = str(self.config['directories']['reorganize_destination'])
print(target_dir)
# Get target directory
# Copy files to new dir
# delete files from current dir
target_dir = str(self.config['directories']['reorganize_destination'])
for filepath in filepaths:
try:
# Read file metadata
audio = EasyID3(filepath)
artist = audio.get('artist', ['Unknown Artist'])[0]
album = audio.get('album', ['Unknown Album'])[0]
# Determine the new path that needs to be made
new_path = os.path.join(target_dir, artist, album, os.path.basename(filepath))
# Create the directories if they dont exist
os.makedirs(os.path.dirname(new_path), exist_ok=True)
# Move the file to the new directory
shutil.move(filepath, new_path)
# Update the db?
with DBA.DBAccess() as db:
db.query('UPDATE library SET filepath = ? WHERE filepath = ?', (new_path, filepath))
print(f'Moved: {filepath} -> {new_path}')
except Exception as e:
print(f'Error moving file: {filepath} | {e}')
self.fetch_library()
QMessageBox.information(self, 'Reorganization complete', 'Files successfully reorganized')
# add new files to library
@ -102,16 +127,17 @@ class MusicTable(QTableView):
def set_selected_song_filepath(self):
"""Sets the filepath of the currently selected song"""
self.selected_song_filepath = self.currentIndex().siblingAtColumn(self.headers.index('path')).data()
self.selected_song_filepath = self.currentIndex().siblingAtColumn(self.table_headers.index('path')).data()
print(f'Selected song: {self.selected_song_filepath}')
# print(get_id3_tags(self.selected_song_filepath))
def set_current_song_filepath(self):
"""Sets the filepath of the currently playing song"""
# Setting the current song filepath automatically plays that song
# self.tableView listens to this function and plays the audio file located at self.current_song_filepath
self.current_song_filepath = self.currentIndex().siblingAtColumn(self.headers.index('path')).data()
print(f'Current song: {self.current_song_filepath}')
self.current_song_filepath = self.currentIndex().siblingAtColumn(self.table_headers.index('path')).data()
# print(f'Current song: {self.current_song_filepath}')
def get_selected_rows(self):
@ -129,7 +155,7 @@ class MusicTable(QTableView):
selected_rows = self.get_selected_rows()
filepaths = []
for row in selected_rows:
idx = self.model.index(row, self.headers.index('path'))
idx = self.model.index(row, self.table_headers.index('path'))
filepaths.append(idx.data())
return filepaths
@ -161,7 +187,9 @@ class MusicTable(QTableView):
def fetch_library(self):
"""Initialize the tableview model"""
self.model.setHorizontalHeaderLabels(self.headers)
self.vertical_scroll_position = self.verticalScrollBar().value() # Get my scroll position before clearing
self.model.clear()
self.model.setHorizontalHeaderLabels(self.table_headers)
# Fetch library data
with DBA.DBAccess() as db:
data = db.query('SELECT title, artist, album, genre, codec, album_date, filepath FROM library;', ())
@ -170,7 +198,13 @@ class MusicTable(QTableView):
items = [QStandardItem(str(item)) for item in row_data]
self.model.appendRow(items)
# Update the viewport/model
self.viewport().update()
# self.viewport().update()
self.model.layoutChanged.emit()
def restore_scroll_position(self):
"""Restores the scroll position"""
print(f'Returning to {self.vertical_scroll_position}')
QTimer.singleShot(100, lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position))
def add_files(self, files):

View File

@ -1,3 +1,4 @@
from .MusicTable import MusicTable
from .AudioVisualizer import AudioVisualizer
from .PreferencesWindow import PreferencesWindow
from .ErrorDialog import ErrorDialog

View File

@ -11,6 +11,8 @@ from components import AudioVisualizer
from components import PreferencesWindow
from pyqtgraph import mkBrush
import configparser
import os
import sys
# Create ui.py file from Qt Designer
# pyuic5 ui.ui -o ui.py
@ -248,10 +250,15 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
if __name__ == "__main__":
import sys
# Allow for dynamic imports of my custom classes and utilities
project_root = os.path.abspath(os.path.dirname(__file__))
sys.path.append(project_root)
# Start the app
app = QApplication(sys.argv)
print(f'main.py app: {app}')
# Dark theme >:3
qdarktheme.setup_theme()
# Show the UI
ui = ApplicationWindow(app)
ui.show()
sys.exit(app.exec_())

View File

@ -1,6 +1,7 @@
from .safe_get import safe_get
from .get_album_art import get_album_art
from .get_id3_tags import get_id3_tags
from .set_id3_tag import set_id3_tag
from .initialize_library_database import initialize_library_database
from .scan_for_music import scan_for_music
from .fft_analyser import FFTAnalyser

View File

@ -21,7 +21,8 @@ def add_files_to_library(files):
if any(filepath.lower().endswith(ext) for ext in extensions):
filename = filepath.split("/")[-1]
audio = get_id3_tags(filepath)
if "title" not in audio:
if "title" not in audio: # This should never run
# get_id3_tags sets the title when no tags exist
return False
# Append data tuple to insert_data list
insert_data.append(

View File

@ -1,5 +1,6 @@
from mutagen.easyid3 import EasyID3
from mutagen import File
import os
def get_id3_tags(file):
@ -15,7 +16,13 @@ def get_id3_tags(file):
# Check if all tags are empty
tags_are_empty = all(not values for values in audio.values())
if tags_are_empty:
audio['title'] = [file.split('/')[-1]]
# split on / to get just the filename
# os.path.splitext to get name without extension
audio['title'] = [os.path.splitext(file.split('/')[-1])[0]]
if audio['title'] is None: # I guess a song could have other tags
# without a title, so i make sure to have title
audio['title'] = [os.path.splitext(file.split('/')[-1])[0]]
audio.save()
return audio
except Exception as e:
print(f"Error: {e}")

88
utils/set_id3_tag.py Normal file
View File

@ -0,0 +1,88 @@
import logging
from components.ErrorDialog import ErrorDialog
from mutagen.id3 import ID3, ID3NoHeaderError, Frame
from mutagen.mp3 import MP3
from mutagen.easyid3 import EasyID3
from mutagen.id3 import (
TIT2, TPE1, TALB, TRCK, 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
)
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
"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_id3_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
Args:
filepath: path to the mp3 file
tag_name: common name of the ID3 tag
value: valut to set for the tag
Returns:
True / False"""
try:
try: # Load existing tags
audio_file = MP3(filepath, ID3=ID3)
except ID3NoHeaderError: # Create new tags if none exist
audio_file = MP3(filepath)
audio_file.add_tags()
if tag_name in id3_tag_mapping: # Tag accounted for
tag_class = id3_tag_mapping[tag_name]
# if issubclass(tag_class, EasyID3) or issubclass(tag_class, ID3): # Type safety
if issubclass(tag_class, Frame):
audio_file.tags.add(tag_class(encoding=3, text=value)) # Add the tag
else:
dialog = ErrorDialog(f'ID3 tag not supported.\nTag: {tag_name}\nTag class: {tag_class}\nValue:{value}')
dialog.exec_()
return False
else:
dialog = ErrorDialog(f'Invalid ID3 tag. Tag: {tag_name}, Value:{value}')
dialog.exec_()
return False
audio_file.save()
return True
except Exception as e:
dialog = ErrorDialog(f'An unhandled exception occurred:\n{e}')
dialog.exec_()
return False