file>open files dialog and lyrics testing

This commit is contained in:
tsi-billypom 2024-05-30 16:30:30 -04:00
parent 767ce3fa9c
commit a78b5c1a51
11 changed files with 185 additions and 49 deletions

View File

@ -0,0 +1,49 @@
from PyQt5.QtWidgets import (
QDialog,
QPlainTextEdit,
QVBoxLayout,
QLabel,
QPushButton,
)
from PyQt5.QtGui import QFont
from utils import set_id3_tag
class LyricsWindow(QDialog):
def __init__(self, song_filepath, lyrics):
super(LyricsWindow, self).__init__()
self.setWindowTitle("Lyrics")
self.lyrics = lyrics
self.song_filepath = song_filepath
self.input_field = ""
layout = QVBoxLayout()
# label = QLabel("Lyrics")
# layout.addWidget(label)
# Labels & input fields
self.input_fields = {}
lyrics_label = QLabel("Lyrics")
lyrics_label.setFont(QFont("Sans", weight=QFont.Bold)) # bold category
lyrics_label.setStyleSheet("text-transform:uppercase;") # uppercase category
layout.addWidget(lyrics_label)
self.input_field = QPlainTextEdit(self.lyrics)
layout.addWidget(self.input_field)
# Save button
save_button = QPushButton("Save")
save_button.clicked.connect(self.save)
layout.addWidget(save_button)
self.setLayout(layout)
def save(self):
"""Saves the current lyrics text to the USLT/lyrics ID3 tag"""
success = set_id3_tag(
filepath=self.song_filepath,
tag_name="lyrics",
value=self.input_field.toPlainText(),
)
if success:
print("success! yay")
else:
print("NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN")
self.close()

View File

@ -1,6 +1,7 @@
from mutagen.easyid3 import EasyID3 from mutagen.easyid3 import EasyID3
import DBA import DBA
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QDragMoveEvent,
QStandardItem, QStandardItem,
QStandardItemModel, QStandardItemModel,
QKeySequence, QKeySequence,
@ -16,6 +17,7 @@ from PyQt5.QtWidgets import (
QAbstractItemView, QAbstractItemView,
) )
from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal, QTimer from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal, QTimer
from components.LyricsWindow import LyricsWindow
from utils import add_files_to_library from utils import add_files_to_library
from utils import update_song_in_library from utils import update_song_in_library
from utils import get_id3_tags from utils import get_id3_tags
@ -107,45 +109,79 @@ class MusicTable(QTableView):
QMessageBox.Yes, QMessageBox.Yes,
) )
if reply: if reply:
# selected_rows = self.get_selected_rows()
selected_filepaths = self.get_selected_songs_filepaths() selected_filepaths = self.get_selected_songs_filepaths()
# for index in selected_rows: selected_indices = self.get_selected_rows()
# self.model().removeRow(index)
for file in selected_filepaths: for file in selected_filepaths:
with DBA.DBAccess() as db: with DBA.DBAccess() as db:
db.execute("DELETE FROM library WHERE filepath = ?", (file,)) db.execute("DELETE FROM library WHERE filepath = ?", (file,))
for index in selected_indices:
self.model.removeRow(index)
def open_directory(self): def open_directory(self):
"""Opens the currently selected song in the system file manager"""
if self.get_selected_song_filepath() is None:
QMessageBox.warning(
self,
"File does not exist",
"No file is selected, or the file does not exist",
QMessageBox.Ok,
QMessageBox.Ok,
)
return
filepath = self.get_selected_song_filepath().split("/") filepath = self.get_selected_song_filepath().split("/")
filepath.pop() filepath.pop()
path = "/".join(filepath) path = "/".join(filepath)
Popen(["xdg-open", path]) Popen(["xdg-open", path])
def show_lyrics_menu(self): def show_lyrics_menu(self):
"""Shows the lyrics for the currently selected song"""
selected_song_filepath = self.get_selected_song_filepath()
if selected_song_filepath is None:
return
current_song = self.get_selected_song_metadata()
print(f"MusicTable.py | show_lyrics_menu | current song: {current_song}")
try:
lyrics = current_song["lyrics"]
except Exception:
pass pass
try:
lyrics = current_song["USLT"]
except Exception:
lyrics = ""
lyrics_window = LyricsWindow(selected_song_filepath, lyrics)
lyrics_window.exec_()
def dragEnterEvent(self, event: QDragEnterEvent): def dragEnterEvent(self, e: QDragEnterEvent | None):
if event.mimeData().hasUrls(): if e is None:
event.accept() return
data = e.mimeData()
if data and data.hasUrls():
e.accept()
else: else:
event.ignore() e.ignore()
def dragMoveEvent(self, event: QDragEnterEvent): def dragMoveEvent(self, e: QDragMoveEvent | None):
if event.mimeData().hasUrls(): if e is None:
event.accept() return
data = e.mimeData()
if data and data.hasUrls():
e.accept()
else: else:
event.ignore() e.ignore()
def dropEvent(self, event: QDropEvent): def dropEvent(self, e: QDropEvent | None):
if event.mimeData().hasUrls(): if e is None:
return
data = e.mimeData()
if data and data.hasUrls():
files = [] files = []
for url in event.mimeData().urls(): for url in data.urls():
if url.isLocalFile(): if url.isLocalFile():
files.append(url.path()) files.append(url.path())
self.add_files(files) self.add_files(files)
event.accept() e.accept()
else: else:
event.ignore() e.ignore()
def setup_keyboard_shortcuts(self): def setup_keyboard_shortcuts(self):
"""Setup shortcuts here""" """Setup shortcuts here"""

View File

@ -1,23 +1,25 @@
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QLineEdit, QPushButton from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QLineEdit, QPushButton
from PyQt5.QtGui import QFont from PyQt5.QtGui import QFont
import configparser
class PreferencesWindow(QDialog): class PreferencesWindow(QDialog):
def __init__(self, config): def __init__(self, config):
super(PreferencesWindow, self).__init__() super(PreferencesWindow, self).__init__()
self.setWindowTitle('Preferences') self.setWindowTitle("Preferences")
self.config = config self.config = config
layout = QVBoxLayout() layout = QVBoxLayout()
label = QLabel('Preferences Window') label = QLabel("Preferences Window")
layout.addWidget(label) layout.addWidget(label)
# Labels & input fields # Labels & input fields
self.input_fields = {} self.input_fields = {}
for category in self.config.sections(): for category in self.config.sections():
category_label = QLabel(f'{category}') category_label = QLabel(f"{category}")
category_label.setFont(QFont('', weight=QFont.Bold)) # bold category category_label.setFont(QFont("", weight=QFont.Bold)) # bold category
category_label.setStyleSheet("text-transform:uppercase;") # uppercase category category_label.setStyleSheet(
"text-transform:uppercase;"
) # uppercase category
layout.addWidget(category_label) layout.addWidget(category_label)
for key in self.config[category]: for key in self.config[category]:
label = QLabel(key) label = QLabel(key)
@ -27,7 +29,7 @@ class PreferencesWindow(QDialog):
self.input_fields[key] = input_field self.input_fields[key] = input_field
# Save button # Save button
save_button = QPushButton('Save') save_button = QPushButton("Save")
save_button.clicked.connect(self.save_preferences) save_button.clicked.connect(self.save_preferences)
layout.addWidget(save_button) layout.addWidget(save_button)
self.setLayout(layout) self.setLayout(layout)
@ -40,8 +42,7 @@ class PreferencesWindow(QDialog):
self.config[category][key] = self.input_fields[key].text() self.config[category][key] = self.input_fields[key].text()
# Write the config file # Write the config file
with open('config.ini', 'w') as configfile: with open("config.ini", "w") as configfile:
self.config.write(configfile) self.config.write(configfile)
self.close() self.close()

View File

@ -3,3 +3,4 @@ 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
from .LyricsWindow import LyricsWindow

41
main.py
View File

@ -10,19 +10,20 @@ from configparser import ConfigParser
import DBA import DBA
from ui import Ui_MainWindow from ui import Ui_MainWindow
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QFileDialog,
QMainWindow, QMainWindow,
QApplication, QApplication,
QGraphicsScene, QGraphicsScene,
QHeaderView, QHeaderView,
QGraphicsPixmapItem, QGraphicsPixmapItem,
QMessageBox,
) )
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
from utils import scan_for_music 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 PreferencesWindow, AudioVisualizer
from components import PreferencesWindow
# 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
@ -89,14 +90,19 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.on_previous_clicked self.on_previous_clicked
) # Click to previous song ) # Click to previous song
self.nextButton.clicked.connect(self.on_next_clicked) # Click to next song self.nextButton.clicked.connect(self.on_next_clicked) # Click to next song
# FILE MENU
self.actionOpenFiles.triggered.connect(self.open_files) # Open files window
# EDIT MENU
# VIEW MENU
self.actionPreferences.triggered.connect( self.actionPreferences.triggered.connect(
self.actionPreferencesClicked self.open_preferences
) # Open preferences menu ) # Open preferences menu
# QUICK ACTIONS MENU
self.actionScanLibraries.triggered.connect(self.scan_libraries) # Scan library self.actionScanLibraries.triggered.connect(self.scan_libraries) # Scan library
self.actionClearDatabase.triggered.connect( self.actionClearDatabase.triggered.connect(
self.clear_database self.clear_database
) # Clear database ) # Clear database
## tableView ## tableView triggers
self.tableView.doubleClicked.connect( self.tableView.doubleClicked.connect(
self.play_audio_file self.play_audio_file
) # Listens for the double click event, then plays the song ) # Listens for the double click event, then plays the song
@ -322,15 +328,40 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
def on_next_clicked(self) -> None: def on_next_clicked(self) -> None:
print("next") print("next")
def actionPreferencesClicked(self) -> None: def open_files(self) -> None:
"""Opens the open files window"""
open_files_window = QFileDialog(
self, "Open file(s)", ".", "Audio files (*.mp3)"
)
# QFileDialog.FileMode enum { AnyFile, ExistingFile, Directory, ExistingFiles }
open_files_window.setFileMode(QFileDialog.ExistingFiles)
open_files_window.exec_()
filenames = open_files_window.selectedFiles()
print("file names chosen")
print(filenames)
self.tableView.add_files(filenames)
def open_preferences(self) -> None:
"""Opens the preferences window"""
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) -> None: def scan_libraries(self) -> None:
"""Scans for new files in the configured library folder
Refreshes the datagridview"""
scan_for_music() scan_for_music()
self.tableView.fetch_library() self.tableView.fetch_library()
def clear_database(self) -> None: def clear_database(self) -> None:
"""Clears all songs from the database"""
reply = QMessageBox.question(
self,
"Confirmation",
"Clear all songs from database?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes,
)
if reply:
delete_and_create_library_database() delete_and_create_library_database()
self.tableView.fetch_library() self.tableView.fetch_library()

6
ui.py
View File

@ -154,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, 41)) self.menubar.setGeometry(QtCore.QRect(0, 0, 1152, 21))
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")
@ -174,6 +174,9 @@ class Ui_MainWindow(object):
self.actionScanLibraries.setObjectName("actionScanLibraries") self.actionScanLibraries.setObjectName("actionScanLibraries")
self.actionClearDatabase = QtWidgets.QAction(MainWindow) self.actionClearDatabase = QtWidgets.QAction(MainWindow)
self.actionClearDatabase.setObjectName("actionClearDatabase") self.actionClearDatabase.setObjectName("actionClearDatabase")
self.actionOpenFiles = QtWidgets.QAction(MainWindow)
self.actionOpenFiles.setObjectName("actionOpenFiles")
self.menuFile.addAction(self.actionOpenFiles)
self.menuEdit.addAction(self.actionPreferences) self.menuEdit.addAction(self.actionPreferences)
self.menuQuick_Actions.addAction(self.actionScanLibraries) self.menuQuick_Actions.addAction(self.actionScanLibraries)
self.menuQuick_Actions.addAction(self.actionClearDatabase) self.menuQuick_Actions.addAction(self.actionClearDatabase)
@ -205,5 +208,6 @@ 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"))
self.actionOpenFiles.setText(_translate("MainWindow", "Open file(s)"))
from components import AlbumArtGraphicsView, MusicTable from components import AlbumArtGraphicsView, MusicTable
from pyqtgraph import PlotWidget from pyqtgraph import PlotWidget

8
ui.ui
View File

@ -274,13 +274,14 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1152</width> <width>1152</width>
<height>41</height> <height>21</height>
</rect> </rect>
</property> </property>
<widget class="QMenu" name="menuFile"> <widget class="QMenu" name="menuFile">
<property name="title"> <property name="title">
<string>File</string> <string>File</string>
</property> </property>
<addaction name="actionOpenFiles"/>
</widget> </widget>
<widget class="QMenu" name="menuEdit"> <widget class="QMenu" name="menuEdit">
<property name="title"> <property name="title">
@ -324,6 +325,11 @@
<string>Clear Database</string> <string>Clear Database</string>
</property> </property>
</action> </action>
<action name="actionOpenFiles">
<property name="text">
<string>Open file(s)</string>
</property>
</action>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>

View File

@ -14,15 +14,15 @@ def add_files_to_library(files):
""" """
if not files: if not files:
return [] return []
print(f"utils/add_files_to_library: {files}") # print(f"utils/add_files_to_library: {files}")
extensions = config.get("settings", "extensions").split(",") extensions = config.get("settings", "extensions").split(",")
insert_data = [] # To store data for batch insert insert_data = [] # To store data for batch insert
for filepath in files: for filepath in files:
if any(filepath.lower().endswith(ext) for ext in extensions): if any(filepath.lower().endswith(ext) for ext in extensions):
filename = filepath.split("/")[-1] filename = filepath.split("/")[-1]
audio = get_id3_tags(filepath) audio = get_id3_tags(filepath)
print('add_files_to_library audio:') # print("add_files_to_library audio:")
print(audio) # print(audio)
# Skip if no title is found (but should never happen # Skip if no title is found (but should never happen
if "title" not in audio: if "title" not in audio:
continue continue

View File

@ -0,0 +1,2 @@
#!/bin/bash
ffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t 5 -q:a 9 -acodec libmp3lame out.mp3

View File

@ -20,24 +20,25 @@ def get_id3_tags(file):
is_easy_id3 = False is_easy_id3 = False
audio = {} audio = {}
print('get_id3_tags audio:') # print('get_id3_tags audio:')
print(audio) # print(audio)
# Check if all tags are empty # Check if all tags are empty
tags_are_empty = all(not values for values in audio.values()) tags_are_empty = all(not values for values in audio.values())
if tags_are_empty: if tags_are_empty:
# split on / to get just the filename # split on / to get just the filename
# os.path.splitext to get name without extension # os.path.splitext to get name without extension
audio['title'] = [os.path.splitext(file.split('/')[-1])[0]] audio["title"] = [os.path.splitext(file.split("/")[-1])[0]]
if audio['title'] is None: # I guess a song could have other tags if audio["title"] is None: # I guess a song could have other tags
# without a title, so i make sure to have title # without a title, so i make sure to have title
audio['title'] = [os.path.splitext(file.split('/')[-1])[0]] audio["title"] = [os.path.splitext(file.split("/")[-1])[0]]
if is_easy_id3: # i can ignore this error because of this check if is_easy_id3: # i can ignore this error because of this check
audio.save() # type: ignore audio.save() # type: ignore
return audio return audio
# import sys # import sys
# my_file = sys.argv[1] # my_file = sys.argv[1]
# data = get_id3_tags(my_file) # data = get_id3_tags(my_file)

View File

@ -84,12 +84,12 @@ def set_id3_tag(filepath: str, tag_name: str, value: str):
Args: Args:
filepath: path to the mp3 file filepath: path to the mp3 file
tag_name: common name of the ID3 tag tag_name: common name of the ID3 tag
value: valut to set for the tag value: value to set for the tag
Returns: Returns:
True / False""" True / False"""
print( print(
f"set_id3_tag(): filepath: {filepath} | tag_name: {tag_name} | value: {value}" f"set_id3_tag.py | filepath: {filepath} | tag_name: {tag_name} | value: {value}"
) )
try: try:
@ -105,11 +105,16 @@ def set_id3_tag(filepath: str, tag_name: str, value: str):
if tdat_tag: if tdat_tag:
# update TDAT if we have it # update TDAT if we have it
audio_file.tags.add(tdat_tag) audio_file.tags.add(tdat_tag)
elif tag_name == "lyrics":
print("lyrics..........")
audio_file.tags.add(USLT(encoding=3, lang="eng", desc="desc", text=value))
elif tag_name in id3_tag_mapping: # Tag accounted for elif tag_name in id3_tag_mapping: # Tag accounted for
tag_class = id3_tag_mapping[tag_name] tag_class = id3_tag_mapping[tag_name]
print(f"set_id3_tag.py | tag_class: {tag_class}")
# 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
print(f"AAAAAAAAAAAAAA")
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_()
@ -122,9 +127,9 @@ def set_id3_tag(filepath: str, tag_name: str, value: str):
pass pass
audio_file.save() audio_file.save()
print("ID3 tags updated:") print("set_id3_tag.py | ID3 tags updated:")
print(get_id3_tags(filepath)) print(get_id3_tags(filepath))
print("-----") print("set_id3_tag.py | -----")
return True return True
except Exception as e: except Exception as e: