id3 tag date fix
This commit is contained in:
parent
b9c045574c
commit
b962ffd583
BIN
assets/default_album_art.jpg
Normal file
BIN
assets/default_album_art.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 300 KiB |
107
components/AlbumArtGraphicsView.py
Normal file
107
components/AlbumArtGraphicsView.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from PyQt5.QtWidgets import QGraphicsView, QMenu, QAction
|
||||||
|
from PyQt5.QtCore import QEvent, Qt, pyqtSignal, QUrl, QPoint
|
||||||
|
from PyQt5.QtGui import QDragEnterEvent, QDropEvent, QClipboard, QPixmap
|
||||||
|
|
||||||
|
|
||||||
|
class AlbumArtGraphicsView(QGraphicsView):
|
||||||
|
albumArtDropped = pyqtSignal(str)
|
||||||
|
albumArtDeleted = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
|
self.customContextMenuRequested.connect(self.showContextMenu)
|
||||||
|
self.qapp = None
|
||||||
|
|
||||||
|
def dragEnterEvent(self, event):
|
||||||
|
if event.mimeData().hasUrls():
|
||||||
|
event.accept()
|
||||||
|
else:
|
||||||
|
event.ignore()
|
||||||
|
|
||||||
|
def dragMoveEvent(self, event):
|
||||||
|
if event.mimeData().hasUrls():
|
||||||
|
event.accept()
|
||||||
|
else:
|
||||||
|
event.ignore()
|
||||||
|
|
||||||
|
def dropEvent(self, event):
|
||||||
|
"""Handles drag and drop pic onto view to add album art to songs"""
|
||||||
|
urls = event.mimeData().urls()
|
||||||
|
if urls:
|
||||||
|
first_url = urls[0].toLocalFile()
|
||||||
|
if first_url.lower().endswith((".png", ".jpg", ".jpeg")):
|
||||||
|
print(f"dropped {first_url}")
|
||||||
|
self.albumArtDropped.emit(
|
||||||
|
first_url
|
||||||
|
) # emit signal that album art was dropped
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
event.ignore()
|
||||||
|
|
||||||
|
def showContextMenu(self, position: QPoint):
|
||||||
|
"""Handles showing a context menu when right clicking the album art"""
|
||||||
|
contextMenu = QMenu(self) # Create the menu
|
||||||
|
|
||||||
|
copyAction = QAction("Copy", self) # Add the actions
|
||||||
|
pasteAction = QAction("Paste", self)
|
||||||
|
deleteAction = QAction("Delete", self)
|
||||||
|
|
||||||
|
copyAction.triggered.connect(
|
||||||
|
self.copy_album_art_to_clipboard
|
||||||
|
) # Add the signal triggers
|
||||||
|
pasteAction.triggered.connect(self.paste_album_art_from_clipboard)
|
||||||
|
deleteAction.triggered.connect(self.delete_album_art)
|
||||||
|
|
||||||
|
contextMenu.addAction(copyAction) # Add actions to the menu
|
||||||
|
contextMenu.addAction(pasteAction)
|
||||||
|
contextMenu.addAction(deleteAction)
|
||||||
|
# DO
|
||||||
|
contextMenu.exec_(self.mapToGlobal(position)) # Show the menu
|
||||||
|
|
||||||
|
def copy_album_art_to_clipboard(self):
|
||||||
|
"""Copies album art to the clipboard"""
|
||||||
|
if not self.scene().items():
|
||||||
|
return # dont care if no pic
|
||||||
|
clipboard = self.qapp.clipboard()
|
||||||
|
pixmap_item = self.scene().items()[0]
|
||||||
|
clipboard.setPixmap(pixmap_item.pixmap())
|
||||||
|
|
||||||
|
def paste_album_art_from_clipboard(self):
|
||||||
|
"""Handles pasting album art into a song via system clipboard"""
|
||||||
|
clipboard = self.qapp.clipboard()
|
||||||
|
mime_data = clipboard.mimeData()
|
||||||
|
# Check if clipboard data is raw data or filepath
|
||||||
|
if mime_data.hasUrls():
|
||||||
|
for url in mime_data.urls():
|
||||||
|
file_path = url.toLocalFile()
|
||||||
|
if os.path.isfile(file_path):
|
||||||
|
pixmap = QPixmap(file_path)
|
||||||
|
else:
|
||||||
|
pixmap = clipboard.pixmap()
|
||||||
|
# Put image on screen and emit signal for ID3 tags to be updated
|
||||||
|
if not pixmap.isNull(): # Add pixmap raw data image
|
||||||
|
try:
|
||||||
|
self.scene().clear() # clear the scene
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.scene().addPixmap(pixmap)
|
||||||
|
# Create temp file for pic
|
||||||
|
temp_file, file_path = tempfile.mkstemp(suffix=".jpg")
|
||||||
|
os.close(temp_file) # close the file
|
||||||
|
pixmap.save(file_path, "JPEG")
|
||||||
|
self.albumArtDropped.emit(
|
||||||
|
file_path
|
||||||
|
) # emit signal that album art was pasted
|
||||||
|
|
||||||
|
def delete_album_art(self):
|
||||||
|
"""Emits a signal for the album art to be deleted"""
|
||||||
|
# Signal emits and ID3 tag is updated
|
||||||
|
self.albumArtDeleted.emit()
|
||||||
|
|
||||||
|
def load_qapp(self, qapp):
|
||||||
|
"""Necessary for talking between components..."""
|
||||||
|
self.qapp = qapp
|
||||||
@ -1,6 +1,12 @@
|
|||||||
from mutagen.easyid3 import EasyID3
|
from mutagen.easyid3 import EasyID3
|
||||||
import DBA
|
import DBA
|
||||||
from PyQt5.QtGui import QStandardItem, QStandardItemModel, QKeySequence, QDragEnterEvent, QDropEvent
|
from PyQt5.QtGui import (
|
||||||
|
QStandardItem,
|
||||||
|
QStandardItemModel,
|
||||||
|
QKeySequence,
|
||||||
|
QDragEnterEvent,
|
||||||
|
QDropEvent,
|
||||||
|
)
|
||||||
from PyQt5.QtWidgets import QTableView, QShortcut, QMessageBox, QAbstractItemView
|
from PyQt5.QtWidgets import QTableView, QShortcut, QMessageBox, QAbstractItemView
|
||||||
from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal, QTimer
|
from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal, QTimer
|
||||||
from utils import add_files_to_library
|
from utils import add_files_to_library
|
||||||
@ -17,16 +23,37 @@ import shutil
|
|||||||
class MusicTable(QTableView):
|
class MusicTable(QTableView):
|
||||||
playPauseSignal = pyqtSignal()
|
playPauseSignal = pyqtSignal()
|
||||||
enterKey = pyqtSignal()
|
enterKey = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
# QTableView.__init__(self, parent)
|
# QTableView.__init__(self, parent)
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.model = QStandardItemModel(self) # Necessary for actions related to cell values
|
self.model = QStandardItemModel(self)
|
||||||
self.setModel(self.model) # Same as above
|
# Necessary for actions related to cell values
|
||||||
|
self.setModel(self.model) # Same as above
|
||||||
self.config = configparser.ConfigParser()
|
self.config = configparser.ConfigParser()
|
||||||
self.config.read('config.ini')
|
self.config.read("config.ini")
|
||||||
self.table_headers = ['title', 'artist', 'album', 'genre', 'codec', 'year', 'path'] # gui names of headers
|
# gui names of headers
|
||||||
self.id3_headers = ['title', 'artist', 'album', 'content_type', ]
|
self.table_headers = [
|
||||||
self.database_columns = str(self.config['table']['columns']).split(',') # db names of headers
|
"title",
|
||||||
|
"artist",
|
||||||
|
"album",
|
||||||
|
"genre",
|
||||||
|
"codec",
|
||||||
|
"year",
|
||||||
|
"path",
|
||||||
|
]
|
||||||
|
# id3 names of headers
|
||||||
|
self.id3_headers = [
|
||||||
|
"title",
|
||||||
|
"artist",
|
||||||
|
"album",
|
||||||
|
"content_type",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
]
|
||||||
|
# db names of headers
|
||||||
|
self.database_columns = str(self.config["table"]["columns"]).split(",")
|
||||||
self.vertical_scroll_position = 0
|
self.vertical_scroll_position = 0
|
||||||
self.songChanged = None
|
self.songChanged = None
|
||||||
self.selected_song_filepath = None
|
self.selected_song_filepath = None
|
||||||
@ -38,24 +65,21 @@ class MusicTable(QTableView):
|
|||||||
self.enterKey.connect(self.set_current_song_filepath)
|
self.enterKey.connect(self.set_current_song_filepath)
|
||||||
self.fetch_library()
|
self.fetch_library()
|
||||||
self.setup_keyboard_shortcuts()
|
self.setup_keyboard_shortcuts()
|
||||||
self.model.dataChanged.connect(self.on_cell_data_changed) # editing cells
|
self.model.dataChanged.connect(self.on_cell_data_changed) # editing cells
|
||||||
self.model.layoutChanged.connect(self.restore_scroll_position)
|
self.model.layoutChanged.connect(self.restore_scroll_position)
|
||||||
|
|
||||||
|
|
||||||
def dragEnterEvent(self, event: QDragEnterEvent):
|
def dragEnterEvent(self, event: QDragEnterEvent):
|
||||||
if event.mimeData().hasUrls():
|
if event.mimeData().hasUrls():
|
||||||
event.accept()
|
event.accept()
|
||||||
else:
|
else:
|
||||||
event.ignore()
|
event.ignore()
|
||||||
|
|
||||||
|
|
||||||
def dragMoveEvent(self, event: QDragEnterEvent):
|
def dragMoveEvent(self, event: QDragEnterEvent):
|
||||||
if event.mimeData().hasUrls():
|
if event.mimeData().hasUrls():
|
||||||
event.accept()
|
event.accept()
|
||||||
else:
|
else:
|
||||||
event.ignore()
|
event.ignore()
|
||||||
|
|
||||||
|
|
||||||
def dropEvent(self, event: QDropEvent):
|
def dropEvent(self, event: QDropEvent):
|
||||||
if event.mimeData().hasUrls():
|
if event.mimeData().hasUrls():
|
||||||
files = []
|
files = []
|
||||||
@ -67,103 +91,122 @@ class MusicTable(QTableView):
|
|||||||
else:
|
else:
|
||||||
event.ignore()
|
event.ignore()
|
||||||
|
|
||||||
|
|
||||||
def setup_keyboard_shortcuts(self):
|
def setup_keyboard_shortcuts(self):
|
||||||
"""Setup shortcuts here"""
|
"""Setup shortcuts here"""
|
||||||
shortcut = QShortcut(QKeySequence("Ctrl+Shift+R"), self)
|
shortcut = QShortcut(QKeySequence("Ctrl+Shift+R"), self)
|
||||||
shortcut.activated.connect(self.reorganize_selected_files)
|
shortcut.activated.connect(self.reorganize_selected_files)
|
||||||
|
|
||||||
|
|
||||||
def on_cell_data_changed(self, topLeft: QModelIndex, bottomRight: QModelIndex):
|
def on_cell_data_changed(self, topLeft: QModelIndex, bottomRight: QModelIndex):
|
||||||
"""Handles updating ID3 tags when data changes in a cell"""
|
"""Handles updating ID3 tags when data changes in a cell"""
|
||||||
print('on_cell_data_changed')
|
print("on_cell_data_changed")
|
||||||
id_index = self.model.index(topLeft.row(), 0) # ID is column 0, always
|
id_index = self.model.index(topLeft.row(), 0) # ID is column 0, always
|
||||||
library_id = self.model.data(id_index, Qt.UserRole)
|
library_id = self.model.data(id_index, Qt.UserRole)
|
||||||
filepath_column_idx = self.model.columnCount() - 1 # filepath is always the last column
|
# filepath is always the last column
|
||||||
filepath_index = self.model.index(topLeft.row(), filepath_column_idx) # exact index of the edited cell in 2d space
|
filepath_column_idx = self.model.columnCount() - 1
|
||||||
filepath = self.model.data(filepath_index) # filepath
|
filepath_index = self.model.index(topLeft.row(), filepath_column_idx)
|
||||||
|
# exact index of the edited cell in 2d space
|
||||||
|
filepath = self.model.data(filepath_index) # filepath
|
||||||
# update the ID3 information
|
# update the ID3 information
|
||||||
user_input_data = topLeft.data()
|
user_input_data = topLeft.data()
|
||||||
edited_column_name = self.database_columns[topLeft.column()]
|
edited_column_name = self.database_columns[topLeft.column()]
|
||||||
|
print(f"edited column name: {edited_column_name}")
|
||||||
response = set_id3_tag(filepath, edited_column_name, user_input_data)
|
response = set_id3_tag(filepath, edited_column_name, user_input_data)
|
||||||
if response:
|
if response:
|
||||||
# Update the library with new metadata
|
# Update the library with new metadata
|
||||||
update_song_in_library(library_id, edited_column_name, user_input_data)
|
update_song_in_library(library_id, edited_column_name, user_input_data)
|
||||||
|
|
||||||
|
|
||||||
def reorganize_selected_files(self):
|
def reorganize_selected_files(self):
|
||||||
"""Ctrl+Shift+R = Reorganize"""
|
"""Ctrl+Shift+R = Reorganize"""
|
||||||
filepaths = self.get_selected_songs_filepaths()
|
filepaths = self.get_selected_songs_filepaths()
|
||||||
# Confirmation screen (yes, no)
|
# Confirmation screen (yes, no)
|
||||||
reply = QMessageBox.question(self, 'Confirmation', 'Are you sure you want to reorganize these files?', QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
|
reply = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"Confirmation",
|
||||||
|
"Are you sure you want to reorganize these files?",
|
||||||
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
|
QMessageBox.Yes,
|
||||||
|
)
|
||||||
if reply:
|
if reply:
|
||||||
# Get target directory
|
# Get target directory
|
||||||
target_dir = str(self.config['directories']['reorganize_destination'])
|
target_dir = str(self.config["directories"]["reorganize_destination"])
|
||||||
for filepath in filepaths:
|
for filepath in filepaths:
|
||||||
try:
|
try:
|
||||||
# Read file metadata
|
# Read file metadata
|
||||||
audio = EasyID3(filepath)
|
audio = EasyID3(filepath)
|
||||||
artist = audio.get('artist', ['Unknown Artist'])[0]
|
artist = audio.get("artist", ["Unknown Artist"])[0]
|
||||||
album = audio.get('album', ['Unknown Album'])[0]
|
album = audio.get("album", ["Unknown Album"])[0]
|
||||||
# Determine the new path that needs to be made
|
# Determine the new path that needs to be made
|
||||||
new_path = os.path.join(target_dir, artist, album, os.path.basename(filepath))
|
new_path = os.path.join(
|
||||||
|
target_dir, artist, album, os.path.basename(filepath)
|
||||||
|
)
|
||||||
# Create the directories if they dont exist
|
# Create the directories if they dont exist
|
||||||
os.makedirs(os.path.dirname(new_path), exist_ok=True)
|
os.makedirs(os.path.dirname(new_path), exist_ok=True)
|
||||||
# Move the file to the new directory
|
# Move the file to the new directory
|
||||||
shutil.move(filepath, new_path)
|
shutil.move(filepath, new_path)
|
||||||
# Update the db
|
# Update the db
|
||||||
with DBA.DBAccess() as db:
|
with DBA.DBAccess() as db:
|
||||||
db.query('UPDATE library SET filepath = ? WHERE filepath = ?', (new_path, filepath))
|
db.query(
|
||||||
print(f'Moved: {filepath} -> {new_path}')
|
"UPDATE library SET filepath = ? WHERE filepath = ?",
|
||||||
|
(new_path, filepath),
|
||||||
|
)
|
||||||
|
print(f"Moved: {filepath} -> {new_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'Error moving file: {filepath} | {e}')
|
print(f"Error moving file: {filepath} | {e}")
|
||||||
# Draw the rest of the owl
|
# Draw the rest of the owl
|
||||||
self.model.dataChanged.disconnect(self.on_cell_data_changed)
|
self.model.dataChanged.disconnect(self.on_cell_data_changed)
|
||||||
self.fetch_library()
|
self.fetch_library()
|
||||||
self.model.dataChanged.connect(self.on_cell_data_changed)
|
self.model.dataChanged.connect(self.on_cell_data_changed)
|
||||||
QMessageBox.information(self, 'Reorganization complete', 'Files successfully reorganized')
|
QMessageBox.information(
|
||||||
|
self, "Reorganization complete", "Files successfully reorganized"
|
||||||
|
)
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
def keyPressEvent(self, event):
|
||||||
"""Press a key. Do a thing"""
|
"""Press a key. Do a thing"""
|
||||||
key = event.key()
|
key = event.key()
|
||||||
if key == Qt.Key_Space: # Spacebar to play/pause
|
if key == Qt.Key_Space: # Spacebar to play/pause
|
||||||
self.toggle_play_pause()
|
self.toggle_play_pause()
|
||||||
elif key == Qt.Key_Up: # Arrow key navigation
|
elif key == Qt.Key_Up: # Arrow key navigation
|
||||||
current_index = self.currentIndex()
|
current_index = self.currentIndex()
|
||||||
new_index = self.model.index(current_index.row() - 1, current_index.column())
|
new_index = self.model.index(
|
||||||
|
current_index.row() - 1, current_index.column()
|
||||||
|
)
|
||||||
if new_index.isValid():
|
if new_index.isValid():
|
||||||
self.setCurrentIndex(new_index)
|
self.setCurrentIndex(new_index)
|
||||||
elif key == Qt.Key_Down: # Arrow key navigation
|
elif key == Qt.Key_Down: # Arrow key navigation
|
||||||
current_index = self.currentIndex()
|
current_index = self.currentIndex()
|
||||||
new_index = self.model.index(current_index.row() + 1, current_index.column())
|
new_index = self.model.index(
|
||||||
|
current_index.row() + 1, current_index.column()
|
||||||
|
)
|
||||||
if new_index.isValid():
|
if new_index.isValid():
|
||||||
self.setCurrentIndex(new_index)
|
self.setCurrentIndex(new_index)
|
||||||
elif key in (Qt.Key_Return, Qt.Key_Enter):
|
elif key in (Qt.Key_Return, Qt.Key_Enter):
|
||||||
if self.state() != QAbstractItemView.EditingState:
|
if self.state() != QAbstractItemView.EditingState:
|
||||||
self.enterKey.emit() # Enter key detected
|
self.enterKey.emit() # Enter key detected
|
||||||
else:
|
else:
|
||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
else: # Default behavior
|
else: # Default behavior
|
||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
|
|
||||||
|
|
||||||
def toggle_play_pause(self):
|
def toggle_play_pause(self):
|
||||||
"""Toggles the currently playing song by emitting a signal"""
|
"""Toggles the currently playing song by emitting a signal"""
|
||||||
if not self.current_song_filepath:
|
if not self.current_song_filepath:
|
||||||
self.set_current_song_filepath()
|
self.set_current_song_filepath()
|
||||||
self.playPauseSignal.emit()
|
self.playPauseSignal.emit()
|
||||||
|
|
||||||
|
|
||||||
def fetch_library(self):
|
def fetch_library(self):
|
||||||
"""Initialize the tableview model"""
|
"""Initialize the tableview model"""
|
||||||
self.vertical_scroll_position = self.verticalScrollBar().value() # Get my scroll position before clearing
|
self.vertical_scroll_position = (
|
||||||
|
self.verticalScrollBar().value()
|
||||||
|
) # Get my scroll position before clearing
|
||||||
# temporarily disconnect the datachanged signal to avoid EVERY SONG getting triggered
|
# temporarily disconnect the datachanged signal to avoid EVERY SONG getting triggered
|
||||||
self.model.clear()
|
self.model.clear()
|
||||||
self.model.setHorizontalHeaderLabels(self.table_headers)
|
self.model.setHorizontalHeaderLabels(self.table_headers)
|
||||||
# Fetch library data
|
# Fetch library data
|
||||||
with DBA.DBAccess() as db:
|
with DBA.DBAccess() as db:
|
||||||
data = db.query('SELECT id, title, artist, album, genre, codec, album_date, filepath FROM library;', ())
|
data = db.query(
|
||||||
|
"SELECT id, title, artist, album, genre, codec, album_date, filepath FROM library;",
|
||||||
|
(),
|
||||||
|
)
|
||||||
# Populate the model
|
# Populate the model
|
||||||
for row_data in data:
|
for row_data in data:
|
||||||
id, *rest_of_data = row_data
|
id, *rest_of_data = row_data
|
||||||
@ -174,16 +217,16 @@ class MusicTable(QTableView):
|
|||||||
for item in items:
|
for item in items:
|
||||||
item.setData(id, Qt.UserRole)
|
item.setData(id, Qt.UserRole)
|
||||||
# Update the viewport/model
|
# Update the viewport/model
|
||||||
self.model.layoutChanged.emit() # emits a signal that the view should be updated
|
self.model.layoutChanged.emit() # emits a signal that the view should be updated
|
||||||
|
|
||||||
|
def restore_scroll_position(self) -> None:
|
||||||
def restore_scroll_position(self):
|
|
||||||
"""Restores the scroll position"""
|
"""Restores the scroll position"""
|
||||||
print(f'Returning to {self.vertical_scroll_position}')
|
QTimer.singleShot(
|
||||||
QTimer.singleShot(100, lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position))
|
100,
|
||||||
|
lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position),
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_files(self, files) -> None:
|
||||||
def add_files(self, files):
|
|
||||||
"""When song(s) added to the library, update the tableview model
|
"""When song(s) added to the library, update the tableview model
|
||||||
- Drag & Drop song(s) on tableView
|
- Drag & Drop song(s) on tableView
|
||||||
- File > Open > List of song(s)
|
- File > Open > List of song(s)
|
||||||
@ -194,62 +237,54 @@ class MusicTable(QTableView):
|
|||||||
self.fetch_library()
|
self.fetch_library()
|
||||||
self.model.dataChanged.connect(self.on_cell_data_changed)
|
self.model.dataChanged.connect(self.on_cell_data_changed)
|
||||||
|
|
||||||
|
def get_selected_rows(self) -> list[int]:
|
||||||
def set_selected_song_filepath(self):
|
|
||||||
"""Sets the filepath of the currently selected song"""
|
|
||||||
self.selected_song_filepath = self.currentIndex().siblingAtColumn(self.table_headers.index('path')).data()
|
|
||||||
|
|
||||||
|
|
||||||
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.table_headers.index('path')).data()
|
|
||||||
|
|
||||||
|
|
||||||
def get_selected_rows(self):
|
|
||||||
"""Returns a list of indexes for every selected row"""
|
"""Returns a list of indexes for every selected row"""
|
||||||
selection_model = self.selectionModel()
|
selection_model = self.selectionModel()
|
||||||
return [index.row() for index in selection_model.selectedRows()]
|
return [index.row() for index in selection_model.selectedRows()]
|
||||||
|
|
||||||
|
def get_selected_songs_filepaths(self) -> list[str]:
|
||||||
def get_selected_songs_filepaths(self):
|
|
||||||
"""Returns a list of the filepaths for the currently selected songs"""
|
"""Returns a list of the filepaths for the currently selected songs"""
|
||||||
selected_rows = self.get_selected_rows()
|
selected_rows = self.get_selected_rows()
|
||||||
filepaths = []
|
filepaths = []
|
||||||
for row in selected_rows:
|
for row in selected_rows:
|
||||||
idx = self.model.index(row, self.table_headers.index('path'))
|
idx = self.model.index(row, self.table_headers.index("path"))
|
||||||
filepaths.append(idx.data())
|
filepaths.append(idx.data())
|
||||||
return filepaths
|
return filepaths
|
||||||
|
|
||||||
|
def get_selected_song_filepath(self) -> str:
|
||||||
def get_selected_song_filepath(self):
|
|
||||||
"""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) -> dict:
|
||||||
def get_selected_song_metadata(self):
|
|
||||||
"""Returns the selected song's ID3 tags"""
|
"""Returns the selected song's ID3 tags"""
|
||||||
return get_id3_tags(self.selected_song_filepath)
|
return get_id3_tags(self.selected_song_filepath)
|
||||||
|
|
||||||
|
def get_current_song_filepath(self) -> str:
|
||||||
def get_current_song_filepath(self):
|
|
||||||
"""Returns the currently playing song filepath"""
|
"""Returns the currently playing song filepath"""
|
||||||
return self.current_song_filepath
|
return self.current_song_filepath
|
||||||
|
|
||||||
|
def get_current_song_metadata(self) -> dict:
|
||||||
def get_current_song_metadata(self):
|
|
||||||
"""Returns the currently playing song's ID3 tags"""
|
"""Returns the currently playing song's ID3 tags"""
|
||||||
return get_id3_tags(self.current_song_filepath)
|
return get_id3_tags(self.current_song_filepath)
|
||||||
|
|
||||||
|
def get_current_song_album_art(self) -> None:
|
||||||
def get_current_song_album_art(self):
|
|
||||||
"""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)
|
||||||
|
|
||||||
|
def set_selected_song_filepath(self) -> None:
|
||||||
|
"""Sets the filepath of the currently selected song"""
|
||||||
|
self.selected_song_filepath = (
|
||||||
|
self.currentIndex().siblingAtColumn(self.table_headers.index("path")).data()
|
||||||
|
)
|
||||||
|
|
||||||
def load_qapp(self, qapp):
|
def set_current_song_filepath(self) -> None:
|
||||||
# why was this necessary again? :thinking:
|
"""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.table_headers.index("path")).data()
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_qapp(self, qapp) -> None:
|
||||||
|
"""Necessary for talking between components..."""
|
||||||
self.qapp = qapp
|
self.qapp = qapp
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
from .MusicTable import MusicTable
|
from .MusicTable import MusicTable
|
||||||
|
from .AlbumArtGraphicsView import AlbumArtGraphicsView
|
||||||
from .AudioVisualizer import AudioVisualizer
|
from .AudioVisualizer import AudioVisualizer
|
||||||
from .PreferencesWindow import PreferencesWindow
|
from .PreferencesWindow import PreferencesWindow
|
||||||
from .ErrorDialog import ErrorDialog
|
from .ErrorDialog import ErrorDialog
|
||||||
|
|||||||
248
main.py
248
main.py
@ -1,7 +1,18 @@
|
|||||||
from ui import Ui_MainWindow
|
import os
|
||||||
from PyQt5.QtWidgets import QMainWindow, QApplication, QGraphicsScene, \
|
import configparser
|
||||||
QHeaderView, QGraphicsPixmapItem
|
import sys
|
||||||
import qdarktheme
|
import qdarktheme
|
||||||
|
from pyqtgraph import mkBrush
|
||||||
|
from mutagen.id3 import ID3, APIC, error
|
||||||
|
from mutagen.mp3 import MP3
|
||||||
|
from ui import Ui_MainWindow
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QMainWindow,
|
||||||
|
QApplication,
|
||||||
|
QGraphicsScene,
|
||||||
|
QHeaderView,
|
||||||
|
QGraphicsPixmapItem,
|
||||||
|
)
|
||||||
from PyQt5.QtCore import QUrl, QTimer, QEvent, Qt, QModelIndex
|
from PyQt5.QtCore import QUrl, QTimer, QEvent, Qt, QModelIndex
|
||||||
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe
|
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe
|
||||||
from PyQt5.QtGui import QPixmap, QStandardItemModel
|
from PyQt5.QtGui import QPixmap, QStandardItemModel
|
||||||
@ -9,10 +20,6 @@ from utils import scan_for_music
|
|||||||
from utils import delete_and_create_library_database
|
from utils import delete_and_create_library_database
|
||||||
from components import AudioVisualizer
|
from components import AudioVisualizer
|
||||||
from components import PreferencesWindow
|
from components import PreferencesWindow
|
||||||
from pyqtgraph import mkBrush
|
|
||||||
import configparser
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Create ui.py file from Qt Designer
|
# Create ui.py file from Qt Designer
|
||||||
# pyuic5 ui.ui -o ui.py
|
# pyuic5 ui.ui -o ui.py
|
||||||
@ -22,7 +29,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
def __init__(self, qapp):
|
def __init__(self, qapp):
|
||||||
super(ApplicationWindow, self).__init__()
|
super(ApplicationWindow, self).__init__()
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
self.setWindowTitle('MusicPom')
|
self.setWindowTitle("MusicPom")
|
||||||
self.selected_song_filepath = None
|
self.selected_song_filepath = None
|
||||||
self.current_song_filepath = None
|
self.current_song_filepath = None
|
||||||
self.current_song_metadata = None
|
self.current_song_metadata = None
|
||||||
@ -31,9 +38,10 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
self.qapp = qapp
|
self.qapp = qapp
|
||||||
# print(f'ApplicationWindow self.qapp: {self.qapp}')
|
# print(f'ApplicationWindow self.qapp: {self.qapp}')
|
||||||
self.tableView.load_qapp(self.qapp)
|
self.tableView.load_qapp(self.qapp)
|
||||||
|
self.albumGraphicsView.load_qapp(self.qapp)
|
||||||
# print(f'tableView type: {type(self.tableView)}')
|
# print(f'tableView type: {type(self.tableView)}')
|
||||||
self.config = configparser.ConfigParser()
|
self.config = configparser.ConfigParser()
|
||||||
self.config.read('config.ini')
|
self.config.read("config.ini")
|
||||||
global stopped
|
global stopped
|
||||||
stopped = False
|
stopped = False
|
||||||
# Initialization
|
# Initialization
|
||||||
@ -48,74 +56,111 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
# Provides faster updates than move_slider
|
# Provides faster updates than move_slider
|
||||||
self.probe.setSource(self.player)
|
self.probe.setSource(self.player)
|
||||||
self.probe.audioBufferProbed.connect(self.process_probe)
|
self.probe.audioBufferProbed.connect(self.process_probe)
|
||||||
|
|
||||||
# Slider Timer (realtime playback feedback horizontal bar)
|
# Slider Timer (realtime playback feedback horizontal bar)
|
||||||
self.timer.start(150) # 150ms update interval solved problem with drag seeking halting playback
|
self.timer.start(
|
||||||
|
150
|
||||||
|
) # 150ms update interval solved problem with drag seeking halting playback
|
||||||
self.timer.timeout.connect(self.move_slider)
|
self.timer.timeout.connect(self.move_slider)
|
||||||
|
|
||||||
# Graphics plot
|
# Graphics plot
|
||||||
self.PlotWidget.setXRange(0,100,padding=0) # x axis range
|
self.PlotWidget.setXRange(0, 100, padding=0) # x axis range
|
||||||
self.PlotWidget.setYRange(0,0.3,padding=0) # y axis range
|
self.PlotWidget.setYRange(0, 0.3, padding=0) # y axis range
|
||||||
self.PlotWidget.getAxis('bottom').setTicks([]) # Remove x-axis ticks
|
self.PlotWidget.getAxis("bottom").setTicks([]) # Remove x-axis ticks
|
||||||
self.PlotWidget.getAxis('bottom').setLabel('') # Remove x-axis label
|
self.PlotWidget.getAxis("bottom").setLabel("") # Remove x-axis label
|
||||||
self.PlotWidget.setLogMode(False, False)
|
self.PlotWidget.setLogMode(False, False)
|
||||||
# Remove y-axis labels and decorations
|
# Remove y-axis labels and decorations
|
||||||
self.PlotWidget.getAxis('left').setTicks([]) # Remove y-axis ticks
|
self.PlotWidget.getAxis("left").setTicks([]) # Remove y-axis ticks
|
||||||
self.PlotWidget.getAxis('left').setLabel('') # Remove y-axis label
|
self.PlotWidget.getAxis("left").setLabel("") # Remove y-axis label
|
||||||
|
|
||||||
# Connections
|
# Connections
|
||||||
# ! FIXME moving the slider while playing is happening frequently causes playback to halt - the pyqtgraph is also affected by this
|
# ! FIXME moving the slider while playing is happening frequently causes playback to halt - the pyqtgraph is also affected by this
|
||||||
self.playbackSlider.sliderMoved[int].connect(lambda: self.player.setPosition(self.playbackSlider.value())) # Move slidet to adjust playback time
|
self.playbackSlider.sliderMoved[int].connect(
|
||||||
self.volumeSlider.sliderMoved[int].connect(lambda: self.volume_changed()) # Move slider to adjust volume
|
lambda: self.player.setPosition(self.playbackSlider.value())
|
||||||
self.playButton.clicked.connect(self.on_play_clicked) # Click to play/pause
|
) # Move slidet to adjust playback time
|
||||||
self.previousButton.clicked.connect(self.on_previous_clicked) # Click to previous song
|
self.volumeSlider.sliderMoved[int].connect(
|
||||||
self.nextButton.clicked.connect(self.on_next_clicked) # Click to next song
|
lambda: self.volume_changed()
|
||||||
self.actionPreferences.triggered.connect(self.actionPreferencesClicked) # Open preferences menu
|
) # Move slider to adjust volume
|
||||||
self.actionScanLibraries.triggered.connect(self.scan_libraries) # Scan library
|
self.playButton.clicked.connect(self.on_play_clicked) # Click to play/pause
|
||||||
self.actionClearDatabase.triggered.connect(self.clear_database) # Clear database
|
self.previousButton.clicked.connect(
|
||||||
|
self.on_previous_clicked
|
||||||
|
) # Click to previous song
|
||||||
|
self.nextButton.clicked.connect(self.on_next_clicked) # Click to next song
|
||||||
|
self.actionPreferences.triggered.connect(
|
||||||
|
self.actionPreferencesClicked
|
||||||
|
) # Open preferences menu
|
||||||
|
self.actionScanLibraries.triggered.connect(self.scan_libraries) # Scan library
|
||||||
|
self.actionClearDatabase.triggered.connect(
|
||||||
|
self.clear_database
|
||||||
|
) # Clear database
|
||||||
## tableView
|
## tableView
|
||||||
self.tableView.doubleClicked.connect(self.play_audio_file) # Listens for the double click event, then plays the song
|
self.tableView.doubleClicked.connect(
|
||||||
self.tableView.enterKey.connect(self.play_audio_file) # Listens for the enter key event, then plays the song
|
self.play_audio_file
|
||||||
self.tableView.playPauseSignal.connect(self.on_play_clicked) # Spacebar toggle play/pause signal
|
) # Listens for the double click event, then plays the song
|
||||||
self.tableView.viewport().installEventFilter(self) # for drag & drop functionality
|
self.tableView.enterKey.connect(
|
||||||
|
self.play_audio_file
|
||||||
|
) # Listens for the enter key event, then plays the song
|
||||||
|
self.tableView.playPauseSignal.connect(
|
||||||
|
self.on_play_clicked
|
||||||
|
) # Spacebar toggle play/pause signal
|
||||||
|
# albumGraphicsView
|
||||||
|
self.albumGraphicsView.albumArtDropped.connect(
|
||||||
|
self.set_album_art_for_selected_songs
|
||||||
|
)
|
||||||
|
self.albumGraphicsView.albumArtDeleted.connect(
|
||||||
|
self.delete_album_art_for_selected_songs
|
||||||
|
)
|
||||||
|
self.tableView.viewport().installEventFilter(
|
||||||
|
self
|
||||||
|
) # for drag & drop functionality
|
||||||
# set column widths
|
# set column widths
|
||||||
table_view_column_widths = str(self.config['table']['column_widths']).split(',')
|
table_view_column_widths = str(self.config["table"]["column_widths"]).split(",")
|
||||||
for i in range(self.tableView.model.columnCount()):
|
for i in range(self.tableView.model.columnCount()):
|
||||||
self.tableView.setColumnWidth(i, int(table_view_column_widths[i]))
|
self.tableView.setColumnWidth(i, int(table_view_column_widths[i]))
|
||||||
# dont extend last column past table view border
|
# dont extend last column past table view border
|
||||||
self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||||
self.tableView.horizontalHeader().setStretchLastSection(False)
|
self.tableView.horizontalHeader().setStretchLastSection(False)
|
||||||
|
|
||||||
|
def closeEvent(self, event) -> None:
|
||||||
def closeEvent(self, event):
|
|
||||||
"""Save settings when closing the application"""
|
"""Save settings when closing the application"""
|
||||||
# MusicTable/tableView column widths
|
# MusicTable/tableView column widths
|
||||||
list_of_column_widths = []
|
list_of_column_widths = []
|
||||||
for i in range(self.tableView.model.columnCount()):
|
for i in range(self.tableView.model.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)
|
||||||
self.config['table']['column_widths'] = column_widths_as_string
|
self.config["table"]["column_widths"] = column_widths_as_string
|
||||||
|
|
||||||
# Save the config
|
# Save the config
|
||||||
with open('config.ini', 'w') as configfile:
|
with open("config.ini", "w") as configfile:
|
||||||
self.config.write(configfile)
|
self.config.write(configfile)
|
||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
def play_audio_file(self) -> None:
|
||||||
def play_audio_file(self):
|
|
||||||
"""Start playback of tableView.current_song_filepath track & moves playback slider"""
|
"""Start playback of tableView.current_song_filepath track & moves playback slider"""
|
||||||
self.current_song_metadata = self.tableView.get_current_song_metadata() # get metadata
|
self.current_song_metadata = (
|
||||||
|
self.tableView.get_current_song_metadata()
|
||||||
|
) # get metadata
|
||||||
self.current_song_album_art = self.tableView.get_current_song_album_art()
|
self.current_song_album_art = self.tableView.get_current_song_album_art()
|
||||||
url = QUrl.fromLocalFile(self.tableView.get_current_song_filepath()) # read the file
|
url = QUrl.fromLocalFile(
|
||||||
content = QMediaContent(url) # load the audio content
|
self.tableView.get_current_song_filepath()
|
||||||
self.player.setMedia(content) # what content to play
|
) # read the file
|
||||||
self.player.play() # play
|
content = QMediaContent(url) # load the audio content
|
||||||
self.move_slider() # mover
|
self.player.setMedia(content) # what content to play
|
||||||
|
self.player.play() # play
|
||||||
|
self.move_slider() # mover
|
||||||
|
|
||||||
# assign metadata
|
# assign metadata
|
||||||
# FIXME when i change tinytag to something else
|
# FIXME when i change tinytag to something else
|
||||||
artist = self.current_song_metadata["artist"][0] if "artist" in self.current_song_metadata else None
|
artist = (
|
||||||
album = self.current_song_metadata["album"][0] if "album" in self.current_song_metadata else None
|
self.current_song_metadata["artist"][0]
|
||||||
|
if "artist" in self.current_song_metadata
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
album = (
|
||||||
|
self.current_song_metadata["album"][0]
|
||||||
|
if "album" in self.current_song_metadata
|
||||||
|
else None
|
||||||
|
)
|
||||||
title = self.current_song_metadata["title"][0]
|
title = self.current_song_metadata["title"][0]
|
||||||
# edit labels
|
# edit labels
|
||||||
self.artistLabel.setText(artist)
|
self.artistLabel.setText(artist)
|
||||||
@ -124,19 +169,24 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
# set album artwork
|
# set album artwork
|
||||||
self.load_album_art(self.current_song_album_art)
|
self.load_album_art(self.current_song_album_art)
|
||||||
|
|
||||||
def load_album_art(self, album_art_data):
|
def load_album_art(self, album_art_data) -> None:
|
||||||
"""Displays the album art for the currently playing track in the GraphicsView"""
|
"""Displays the album art for the currently playing track in the GraphicsView"""
|
||||||
if self.current_song_album_art:
|
if self.current_song_album_art:
|
||||||
# Clear the scene
|
# Clear the scene
|
||||||
self.album_art_scene.clear()
|
try:
|
||||||
|
self.album_art_scene.clear()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
# Reset the scene
|
# Reset the scene
|
||||||
self.albumGraphicsView.setScene(None)
|
self.albumGraphicsView.setScene(None)
|
||||||
# Create pixmap for album art
|
# Create pixmap for album art
|
||||||
pixmap = QPixmap()
|
pixmap = QPixmap()
|
||||||
pixmap.loadFromData(self.current_song_album_art)
|
pixmap.loadFromData(album_art_data)
|
||||||
# Create a QGraphicsPixmapItem for more control over pic
|
# Create a QGraphicsPixmapItem for more control over pic
|
||||||
pixmapItem = QGraphicsPixmapItem(pixmap)
|
pixmapItem = QGraphicsPixmapItem(pixmap)
|
||||||
pixmapItem.setTransformationMode(Qt.SmoothTransformation) # For better quality scaling
|
pixmapItem.setTransformationMode(
|
||||||
|
Qt.SmoothTransformation
|
||||||
|
) # For better quality scaling
|
||||||
# Add pixmap item to the scene
|
# Add pixmap item to the scene
|
||||||
self.album_art_scene.addItem(pixmapItem)
|
self.album_art_scene.addItem(pixmapItem)
|
||||||
# Set the scene
|
# Set the scene
|
||||||
@ -144,32 +194,82 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
# Adjust the album art scaling
|
# Adjust the album art scaling
|
||||||
self.adjustPixmapScaling(pixmapItem)
|
self.adjustPixmapScaling(pixmapItem)
|
||||||
|
|
||||||
def adjustPixmapScaling(self, pixmapItem):
|
def adjustPixmapScaling(self, pixmapItem) -> None:
|
||||||
"""Adjust the scaling of the pixmap item to fit the QGraphicsView, maintaining aspect ratio"""
|
"""Adjust the scaling of the pixmap item to fit the QGraphicsView, maintaining aspect ratio"""
|
||||||
viewWidth = self.albumGraphicsView.width()
|
viewWidth = self.albumGraphicsView.width()
|
||||||
viewHeight = self.albumGraphicsView.height()
|
viewHeight = self.albumGraphicsView.height()
|
||||||
pixmapSize = pixmapItem.pixmap().size()
|
pixmapSize = pixmapItem.pixmap().size()
|
||||||
|
|
||||||
# Calculate scaling factor while maintaining aspect ratio
|
# Calculate scaling factor while maintaining aspect ratio
|
||||||
scaleX = viewWidth / pixmapSize.width()
|
scaleX = viewWidth / pixmapSize.width()
|
||||||
scaleY = viewHeight / pixmapSize.height()
|
scaleY = viewHeight / pixmapSize.height()
|
||||||
scaleFactor = min(scaleX, scaleY)
|
scaleFactor = min(scaleX, scaleY)
|
||||||
|
|
||||||
# Apply scaling to the pixmap item
|
# Apply scaling to the pixmap item
|
||||||
pixmapItem.setScale(scaleFactor)
|
pixmapItem.setScale(scaleFactor)
|
||||||
|
|
||||||
def update_audio_visualization(self):
|
def set_album_art_for_selected_songs(self, album_art_path: str) -> None:
|
||||||
|
"""Sets the ID3 tag APIC (album art) for all selected song filepaths"""
|
||||||
|
selected_songs = self.tableView.get_selected_songs_filepaths()
|
||||||
|
for song in selected_songs:
|
||||||
|
print(f"updating album art for {song}")
|
||||||
|
self.update_album_art_for_song(song, album_art_path)
|
||||||
|
|
||||||
|
def update_album_art_for_song(
|
||||||
|
self, song_file_path: str, album_art_path: str
|
||||||
|
) -> None:
|
||||||
|
"""Updates the ID3 tag APIC (album art) for 1 song"""
|
||||||
|
audio = MP3(song_file_path, ID3=ID3)
|
||||||
|
# Remove existing APIC Frames (album art)
|
||||||
|
if audio.tags is not None:
|
||||||
|
audio.tags.delall("APIC")
|
||||||
|
# Add the album art
|
||||||
|
with open(album_art_path, "rb") as album_art_file:
|
||||||
|
if album_art_path.endswith(".jpg") or album_art_path.endswith(".jpeg"):
|
||||||
|
audio.tags.add(
|
||||||
|
APIC(
|
||||||
|
encoding=3, # 3 = utf-8
|
||||||
|
mime="image/jpeg",
|
||||||
|
type=3, # 3 = cover image
|
||||||
|
desc="Cover",
|
||||||
|
data=album_art_file.read(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif album_art_path.endswith(".png"):
|
||||||
|
audio.tags.add(
|
||||||
|
APIC(
|
||||||
|
encoding=3, # 3 = utf-8
|
||||||
|
mime="image/png",
|
||||||
|
type=3, # 3 = cover image
|
||||||
|
desc="Cover",
|
||||||
|
data=album_art_file.read(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
audio.save()
|
||||||
|
|
||||||
|
def delete_album_art_for_selected_songs(self) -> None:
|
||||||
|
"""Handles deleting the ID3 tag APIC (album art) for all selected songs"""
|
||||||
|
filepaths = self.tableView.get_selected_songs_filepaths()
|
||||||
|
for file in filepaths:
|
||||||
|
# delete APIC data
|
||||||
|
try:
|
||||||
|
audio = ID3(file)
|
||||||
|
if "APIC:" in audio:
|
||||||
|
del audio["APIC"]
|
||||||
|
audio.save()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing {file}: {e}")
|
||||||
|
|
||||||
|
def update_audio_visualization(self) -> None:
|
||||||
"""Handles upading points on the pyqtgraph visual"""
|
"""Handles upading points on the pyqtgraph visual"""
|
||||||
self.clear_audio_visualization()
|
self.clear_audio_visualization()
|
||||||
self.y = self.audio_visualizer.get_amplitudes()
|
self.y = self.audio_visualizer.get_amplitudes()
|
||||||
self.x = [i for i in range(len(self.y))]
|
self.x = [i for i in range(len(self.y))]
|
||||||
self.PlotWidget.plot(self.x, self.y, fillLevel=0, fillBrush=mkBrush('b'))
|
self.PlotWidget.plot(self.x, self.y, fillLevel=0, fillBrush=mkBrush("b"))
|
||||||
self.PlotWidget.show()
|
self.PlotWidget.show()
|
||||||
|
|
||||||
def clear_audio_visualization(self):
|
def clear_audio_visualization(self) -> None:
|
||||||
self.PlotWidget.clear()
|
self.PlotWidget.clear()
|
||||||
|
|
||||||
def move_slider(self):
|
def move_slider(self) -> None:
|
||||||
"""Handles moving the playback slider"""
|
"""Handles moving the playback slider"""
|
||||||
if stopped:
|
if stopped:
|
||||||
return
|
return
|
||||||
@ -180,14 +280,18 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
self.playbackSlider.setMaximum(self.player.duration())
|
self.playbackSlider.setMaximum(self.player.duration())
|
||||||
slider_position = self.player.position()
|
slider_position = self.player.position()
|
||||||
self.playbackSlider.setValue(slider_position)
|
self.playbackSlider.setValue(slider_position)
|
||||||
|
|
||||||
current_minutes, current_seconds = divmod(slider_position / 1000, 60)
|
current_minutes, current_seconds = divmod(slider_position / 1000, 60)
|
||||||
duration_minutes, duration_seconds = divmod(self.player.duration() / 1000, 60)
|
duration_minutes, duration_seconds = divmod(
|
||||||
|
self.player.duration() / 1000, 60
|
||||||
|
)
|
||||||
|
self.startTimeLabel.setText(
|
||||||
|
f"{int(current_minutes):02d}:{int(current_seconds):02d}"
|
||||||
|
)
|
||||||
|
self.endTimeLabel.setText(
|
||||||
|
f"{int(duration_minutes):02d}:{int(duration_seconds):02d}"
|
||||||
|
)
|
||||||
|
|
||||||
self.startTimeLabel.setText(f"{int(current_minutes):02d}:{int(current_seconds):02d}")
|
def volume_changed(self) -> None:
|
||||||
self.endTimeLabel.setText(f"{int(duration_minutes):02d}:{int(duration_seconds):02d}")
|
|
||||||
|
|
||||||
def volume_changed(self):
|
|
||||||
"""Handles volume changes"""
|
"""Handles volume changes"""
|
||||||
try:
|
try:
|
||||||
self.current_volume = self.volumeSlider.value()
|
self.current_volume = self.volumeSlider.value()
|
||||||
@ -195,7 +299,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Changing volume error: {e}")
|
print(f"Changing volume error: {e}")
|
||||||
|
|
||||||
def on_play_clicked(self):
|
def on_play_clicked(self) -> None:
|
||||||
"""Updates the Play & Pause buttons when clicked"""
|
"""Updates the Play & Pause buttons when clicked"""
|
||||||
if self.player.state() == QMediaPlayer.PlayingState:
|
if self.player.state() == QMediaPlayer.PlayingState:
|
||||||
self.player.pause()
|
self.player.pause()
|
||||||
@ -208,26 +312,26 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
self.play_audio_file()
|
self.play_audio_file()
|
||||||
self.playButton.setText("⏸️")
|
self.playButton.setText("⏸️")
|
||||||
|
|
||||||
def on_previous_clicked(self):
|
def on_previous_clicked(self) -> None:
|
||||||
""""""
|
""""""
|
||||||
print('previous')
|
print("previous")
|
||||||
|
|
||||||
def on_next_clicked(self):
|
def on_next_clicked(self) -> None:
|
||||||
print('next')
|
print("next")
|
||||||
|
|
||||||
def actionPreferencesClicked(self):
|
def actionPreferencesClicked(self) -> None:
|
||||||
preferences_window = PreferencesWindow(self.config)
|
preferences_window = PreferencesWindow(self.config)
|
||||||
preferences_window.exec_() # Display the preferences window modally
|
preferences_window.exec_() # Display the preferences window modally
|
||||||
|
|
||||||
def scan_libraries(self):
|
def scan_libraries(self) -> None:
|
||||||
scan_for_music()
|
scan_for_music()
|
||||||
self.tableView.fetch_library()
|
self.tableView.fetch_library()
|
||||||
|
|
||||||
def clear_database(self):
|
def clear_database(self) -> None:
|
||||||
delete_and_create_library_database()
|
delete_and_create_library_database()
|
||||||
self.tableView.fetch_library()
|
self.tableView.fetch_library()
|
||||||
|
|
||||||
def process_probe(self, buff):
|
def process_probe(self, buff) -> None:
|
||||||
buff.startTime()
|
buff.startTime()
|
||||||
self.update_audio_visualization()
|
self.update_audio_visualization()
|
||||||
|
|
||||||
@ -238,7 +342,7 @@ if __name__ == "__main__":
|
|||||||
sys.path.append(project_root)
|
sys.path.append(project_root)
|
||||||
# Start the app
|
# Start the app
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
print(f'main.py app: {app}')
|
print(f"main.py app: {app}")
|
||||||
# Dark theme >:3
|
# Dark theme >:3
|
||||||
qdarktheme.setup_theme()
|
qdarktheme.setup_theme()
|
||||||
# Show the UI
|
# Show the UI
|
||||||
|
|||||||
36
show_id3_tags.py
Normal file
36
show_id3_tags.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import sys
|
||||||
|
from mutagen.id3 import ID3, APIC
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def print_id3_tags(file_path):
|
||||||
|
"""Prints all ID3 tags for a given audio file."""
|
||||||
|
try:
|
||||||
|
audio = ID3(file_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading ID3 tags: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
for tag in audio.keys():
|
||||||
|
# Special handling for APIC frames (attached pictures)
|
||||||
|
if isinstance(audio[tag], APIC):
|
||||||
|
print(
|
||||||
|
f"{tag}: Picture, MIME type: {audio[tag].mime}, Description: {audio[tag].desc}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"{tag}: {audio[tag].pprint()}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print("Usage: python show_id3_tags.py /path/to/audio/file")
|
||||||
|
sys.exit(1)
|
||||||
|
file_path = sys.argv[1]
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
print("File does not exist.")
|
||||||
|
sys.exit(1)
|
||||||
|
print_id3_tags(file_path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
7
ui.py
7
ui.py
@ -25,7 +25,7 @@ class Ui_MainWindow(object):
|
|||||||
self.vlayoutAlbumArt = QtWidgets.QVBoxLayout()
|
self.vlayoutAlbumArt = QtWidgets.QVBoxLayout()
|
||||||
self.vlayoutAlbumArt.setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
|
self.vlayoutAlbumArt.setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
|
||||||
self.vlayoutAlbumArt.setObjectName("vlayoutAlbumArt")
|
self.vlayoutAlbumArt.setObjectName("vlayoutAlbumArt")
|
||||||
self.albumGraphicsView = QtWidgets.QGraphicsView(self.centralwidget)
|
self.albumGraphicsView = AlbumArtGraphicsView(self.centralwidget)
|
||||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
|
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
|
||||||
sizePolicy.setHorizontalStretch(0)
|
sizePolicy.setHorizontalStretch(0)
|
||||||
sizePolicy.setVerticalStretch(0)
|
sizePolicy.setVerticalStretch(0)
|
||||||
@ -33,6 +33,7 @@ class Ui_MainWindow(object):
|
|||||||
self.albumGraphicsView.setSizePolicy(sizePolicy)
|
self.albumGraphicsView.setSizePolicy(sizePolicy)
|
||||||
self.albumGraphicsView.setMinimumSize(QtCore.QSize(200, 200))
|
self.albumGraphicsView.setMinimumSize(QtCore.QSize(200, 200))
|
||||||
self.albumGraphicsView.setMaximumSize(QtCore.QSize(16777215, 16777215))
|
self.albumGraphicsView.setMaximumSize(QtCore.QSize(16777215, 16777215))
|
||||||
|
self.albumGraphicsView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||||
self.albumGraphicsView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
self.albumGraphicsView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||||
self.albumGraphicsView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
self.albumGraphicsView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||||
self.albumGraphicsView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustIgnored)
|
self.albumGraphicsView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustIgnored)
|
||||||
@ -153,7 +154,7 @@ class Ui_MainWindow(object):
|
|||||||
self.verticalLayout_3.setStretch(3, 1)
|
self.verticalLayout_3.setStretch(3, 1)
|
||||||
MainWindow.setCentralWidget(self.centralwidget)
|
MainWindow.setCentralWidget(self.centralwidget)
|
||||||
self.menubar = QtWidgets.QMenuBar(MainWindow)
|
self.menubar = QtWidgets.QMenuBar(MainWindow)
|
||||||
self.menubar.setGeometry(QtCore.QRect(0, 0, 1152, 24))
|
self.menubar.setGeometry(QtCore.QRect(0, 0, 1152, 41))
|
||||||
self.menubar.setObjectName("menubar")
|
self.menubar.setObjectName("menubar")
|
||||||
self.menuFile = QtWidgets.QMenu(self.menubar)
|
self.menuFile = QtWidgets.QMenu(self.menubar)
|
||||||
self.menuFile.setObjectName("menuFile")
|
self.menuFile.setObjectName("menuFile")
|
||||||
@ -204,5 +205,5 @@ class Ui_MainWindow(object):
|
|||||||
self.actionPreferences.setStatusTip(_translate("MainWindow", "Open preferences"))
|
self.actionPreferences.setStatusTip(_translate("MainWindow", "Open preferences"))
|
||||||
self.actionScanLibraries.setText(_translate("MainWindow", "Scan libraries"))
|
self.actionScanLibraries.setText(_translate("MainWindow", "Scan libraries"))
|
||||||
self.actionClearDatabase.setText(_translate("MainWindow", "Clear Database"))
|
self.actionClearDatabase.setText(_translate("MainWindow", "Clear Database"))
|
||||||
from components import MusicTable
|
from components import AlbumArtGraphicsView, MusicTable
|
||||||
from pyqtgraph import PlotWidget
|
from pyqtgraph import PlotWidget
|
||||||
|
|||||||
10
ui.ui
10
ui.ui
@ -26,7 +26,7 @@
|
|||||||
<enum>QLayout::SetFixedSize</enum>
|
<enum>QLayout::SetFixedSize</enum>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGraphicsView" name="albumGraphicsView">
|
<widget class="AlbumArtGraphicsView" name="albumGraphicsView">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
@ -45,6 +45,9 @@
|
|||||||
<height>16777215</height>
|
<height>16777215</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="contextMenuPolicy">
|
||||||
|
<enum>Qt::CustomContextMenu</enum>
|
||||||
|
</property>
|
||||||
<property name="verticalScrollBarPolicy">
|
<property name="verticalScrollBarPolicy">
|
||||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||||
</property>
|
</property>
|
||||||
@ -334,6 +337,11 @@
|
|||||||
<extends>QTableView</extends>
|
<extends>QTableView</extends>
|
||||||
<header>components</header>
|
<header>components</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>AlbumArtGraphicsView</class>
|
||||||
|
<extends>QGraphicsView</extends>
|
||||||
|
<header>components</header>
|
||||||
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections/>
|
<connections/>
|
||||||
|
|||||||
@ -7,3 +7,4 @@ from .update_song_in_library import update_song_in_library
|
|||||||
from .scan_for_music import scan_for_music
|
from .scan_for_music import scan_for_music
|
||||||
from .fft_analyser import FFTAnalyser
|
from .fft_analyser import FFTAnalyser
|
||||||
from .add_files_to_library import add_files_to_library
|
from .add_files_to_library import add_files_to_library
|
||||||
|
from .handle_year_and_date_id3_tag import handle_year_and_date_id3_tag
|
||||||
|
|||||||
@ -4,14 +4,19 @@ def get_album_art(file):
|
|||||||
"""Get the album art for an audio file
|
"""Get the album art for an audio file
|
||||||
# Parameters
|
# Parameters
|
||||||
`file` | str | Fully qualified path to file
|
`file` | str | Fully qualified path to file
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
dict of all id3 tags
|
Data for album art or default file
|
||||||
"""
|
"""
|
||||||
|
default_image_path = './assets/default_album_art.jpg'
|
||||||
try:
|
try:
|
||||||
audio = ID3(file)
|
audio = ID3(file)
|
||||||
album_art = audio.get('APIC:').data if 'APIC:' in audio else None
|
for tag in audio.getall('APIC'):
|
||||||
return album_art
|
if tag.type == 3: # 3 is the type for front cover
|
||||||
|
return tag.data
|
||||||
|
if audio.getall('APIC'):
|
||||||
|
return audio.getall('APIC')[0].data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error retrieving album art: {e}")
|
||||||
return {}
|
with open(default_image_path, 'rb') as file:
|
||||||
|
print(f'album art type: {type(file.read())}')
|
||||||
|
return file.read()
|
||||||
|
|||||||
21
utils/handle_year_and_date_id3_tag.py
Normal file
21
utils/handle_year_and_date_id3_tag.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import re
|
||||||
|
from mutagen.id3 import TYER, TDAT
|
||||||
|
|
||||||
|
|
||||||
|
def handle_year_and_date_id3_tag(date_str):
|
||||||
|
"""Handles date formatting on update in music table
|
||||||
|
Date format = YYYY-MM-DD"""
|
||||||
|
match = re.match(r"(\d{4})[-/](\d{2})[-/](\d{2})", date_str)
|
||||||
|
if not match:
|
||||||
|
raise ValueError("Invalid date format")
|
||||||
|
print(f"handle_year_and_date_id3_tag(): match: {match}")
|
||||||
|
year, month, day = match.groups()
|
||||||
|
print(f"handle_year_and_date_id3_tag(): month: {month} | day: {day} | year: {year}")
|
||||||
|
# only year
|
||||||
|
if not month or not day:
|
||||||
|
print(TYER(encoding=3, text=year))
|
||||||
|
return TYER(encoding=3, text=year), None
|
||||||
|
else:
|
||||||
|
date_value = f"{day}{month}"
|
||||||
|
print(TYER(encoding=3, text=year), TDAT(encoding=3, text=date_value))
|
||||||
|
return TYER(encoding=3, text=year), TDAT(encoding=3, text=date_value)
|
||||||
@ -1,12 +1,44 @@
|
|||||||
import logging
|
import logging
|
||||||
from components.ErrorDialog import ErrorDialog
|
from components.ErrorDialog import ErrorDialog
|
||||||
|
from utils.get_id3_tags import get_id3_tags
|
||||||
|
from utils.handle_year_and_date_id3_tag import handle_year_and_date_id3_tag
|
||||||
from mutagen.id3 import ID3, ID3NoHeaderError, Frame
|
from mutagen.id3 import ID3, ID3NoHeaderError, Frame
|
||||||
from mutagen.mp3 import MP3
|
from mutagen.mp3 import MP3
|
||||||
from mutagen.easyid3 import EasyID3
|
from mutagen.easyid3 import EasyID3
|
||||||
from mutagen.id3 import (
|
from mutagen.id3 import (
|
||||||
TIT2, TPE1, TALB, TRCK, TYER, TCON, TPOS, COMM, TPE2, TCOM, TPE3, TPE4,
|
TIT2,
|
||||||
TCOP, WOAR, USLT, APIC, TENC, TBPM, TKEY, TDAT, TIME, TSSE, TOPE, TEXT,
|
TPE1,
|
||||||
TOLY, TORY, TPUB, WCOM, WCOP, WOAS, WORS, WPAY, WPUB
|
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 = {
|
id3_tag_mapping = {
|
||||||
@ -15,12 +47,11 @@ id3_tag_mapping = {
|
|||||||
"album": TALB, # Album/Movie/Show title
|
"album": TALB, # Album/Movie/Show title
|
||||||
"album_artist": TPE2, # Band/orchestra/accompaniment
|
"album_artist": TPE2, # Band/orchestra/accompaniment
|
||||||
"genre": TCON, # Content type
|
"genre": TCON, # Content type
|
||||||
"year": TYER, # Year of recording
|
# "year": TYER, # Year of recording
|
||||||
"date": TDAT, # Date
|
# "date": TDAT, # Date
|
||||||
"lyrics": USLT, # Unsynchronized lyric/text transcription
|
"lyrics": USLT, # Unsynchronized lyric/text transcription
|
||||||
"track_number": TRCK, # Track number/Position in set
|
"track_number": TRCK, # Track number/Position in set
|
||||||
"album_cover": APIC, # Attached picture
|
"album_cover": APIC, # Attached picture
|
||||||
|
|
||||||
"composer": TCOM, # Composer
|
"composer": TCOM, # Composer
|
||||||
"conductor": TPE3, # Conductor/performer refinement
|
"conductor": TPE3, # Conductor/performer refinement
|
||||||
"remixed_by": TPE4, # Interpreted, remixed, or otherwise modified by
|
"remixed_by": TPE4, # Interpreted, remixed, or otherwise modified by
|
||||||
@ -46,6 +77,7 @@ id3_tag_mapping = {
|
|||||||
"publishers_official_webpage": WPUB, # Publishers official webpage
|
"publishers_official_webpage": WPUB, # Publishers official webpage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def set_id3_tag(filepath: str, tag_name: str, value: str):
|
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
|
"""Sets the ID3 tag for a file given a filepath, tag_name, and a value for the tag
|
||||||
|
|
||||||
@ -56,33 +88,46 @@ def set_id3_tag(filepath: str, tag_name: str, value: str):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True / False"""
|
True / False"""
|
||||||
|
print(
|
||||||
|
f"set_id3_tag(): filepath: {filepath} | tag_name: {tag_name} | value: {value}"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try: # Load existing tags
|
try: # Load existing tags
|
||||||
audio_file = MP3(filepath, ID3=ID3)
|
audio_file = MP3(filepath, ID3=ID3)
|
||||||
except ID3NoHeaderError: # Create new tags if none exist
|
except ID3NoHeaderError: # Create new tags if none exist
|
||||||
audio_file = MP3(filepath)
|
audio_file = MP3(filepath)
|
||||||
audio_file.add_tags()
|
audio_file.add_tags()
|
||||||
|
if tag_name == "album_date":
|
||||||
if tag_name in id3_tag_mapping: # Tag accounted for
|
tyer_tag, tdat_tag = handle_year_and_date_id3_tag(value)
|
||||||
|
# always update TYER
|
||||||
|
audio_file.tags.add(tyer_tag)
|
||||||
|
if tdat_tag:
|
||||||
|
# update TDAT if we have it
|
||||||
|
audio_file.tags.add(tdat_tag)
|
||||||
|
elif tag_name in id3_tag_mapping: # Tag accounted for
|
||||||
tag_class = id3_tag_mapping[tag_name]
|
tag_class = id3_tag_mapping[tag_name]
|
||||||
# if issubclass(tag_class, EasyID3) or issubclass(tag_class, ID3): # Type safety
|
# if issubclass(tag_class, EasyID3) or issubclass(tag_class, ID3): # Type safety
|
||||||
if issubclass(tag_class, Frame):
|
if issubclass(tag_class, Frame):
|
||||||
audio_file.tags.add(tag_class(encoding=3, text=value)) # Add the tag
|
audio_file.tags.add(tag_class(encoding=3, text=value)) # Add the tag
|
||||||
else:
|
else:
|
||||||
dialog = ErrorDialog(f'ID3 tag not supported.\nTag: {tag_name}\nTag class: {tag_class}\nValue:{value}')
|
# dialog = ErrorDialog(f'ID3 tag not supported.\nTag: {tag_name}\nTag class: {tag_class}\nValue:{value}')
|
||||||
dialog.exec_()
|
# dialog.exec_()
|
||||||
return False
|
# return False
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
dialog = ErrorDialog(f'Invalid ID3 tag. Tag: {tag_name}, Value:{value}')
|
# dialog = ErrorDialog(f'Invalid ID3 tag. Tag: {tag_name}, Value:{value}')
|
||||||
dialog.exec_()
|
# dialog.exec_()
|
||||||
return False
|
# return False
|
||||||
|
pass
|
||||||
|
|
||||||
audio_file.save()
|
audio_file.save()
|
||||||
|
print("ID3 tags updated:")
|
||||||
|
print(get_id3_tags(filepath))
|
||||||
|
print("-----")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
dialog = ErrorDialog(f'An unhandled exception occurred:\n{e}')
|
dialog = ErrorDialog(f"An unhandled exception occurred:\n{e}")
|
||||||
dialog.exec_()
|
dialog.exec_()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user