From a78b5c1a51cbf2713dc556f07d17dadd7ce7efed Mon Sep 17 00:00:00 2001 From: tsi-billypom Date: Thu, 30 May 2024 16:30:30 -0400 Subject: [PATCH] file>open files dialog and lyrics testing --- components/LyricsWindow.py | 49 +++++++++++++++++++++++ components/MusicTable.py | 70 +++++++++++++++++++++++++-------- components/PreferencesWindow.py | 19 ++++----- components/__init__.py | 1 + main.py | 45 +++++++++++++++++---- ui.py | 6 ++- ui.ui | 8 +++- utils/add_files_to_library.py | 6 +-- utils/create_blank_mp3.sh | 2 + utils/get_id3_tags.py | 15 +++---- utils/set_id3_tag.py | 13 ++++-- 11 files changed, 185 insertions(+), 49 deletions(-) create mode 100644 components/LyricsWindow.py create mode 100644 utils/create_blank_mp3.sh diff --git a/components/LyricsWindow.py b/components/LyricsWindow.py new file mode 100644 index 0000000..79fce22 --- /dev/null +++ b/components/LyricsWindow.py @@ -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() diff --git a/components/MusicTable.py b/components/MusicTable.py index 92c3cd6..60c0c90 100644 --- a/components/MusicTable.py +++ b/components/MusicTable.py @@ -1,6 +1,7 @@ from mutagen.easyid3 import EasyID3 import DBA from PyQt5.QtGui import ( + QDragMoveEvent, QStandardItem, QStandardItemModel, QKeySequence, @@ -16,6 +17,7 @@ from PyQt5.QtWidgets import ( QAbstractItemView, ) from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal, QTimer +from components.LyricsWindow import LyricsWindow from utils import add_files_to_library from utils import update_song_in_library from utils import get_id3_tags @@ -107,45 +109,79 @@ class MusicTable(QTableView): QMessageBox.Yes, ) if reply: - # selected_rows = self.get_selected_rows() selected_filepaths = self.get_selected_songs_filepaths() - # for index in selected_rows: - # self.model().removeRow(index) + selected_indices = self.get_selected_rows() for file in selected_filepaths: with DBA.DBAccess() as db: db.execute("DELETE FROM library WHERE filepath = ?", (file,)) + for index in selected_indices: + self.model.removeRow(index) 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.pop() path = "/".join(filepath) Popen(["xdg-open", path]) def show_lyrics_menu(self): - pass + """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 + try: + lyrics = current_song["USLT"] + except Exception: + lyrics = "" + lyrics_window = LyricsWindow(selected_song_filepath, lyrics) + lyrics_window.exec_() - def dragEnterEvent(self, event: QDragEnterEvent): - if event.mimeData().hasUrls(): - event.accept() + def dragEnterEvent(self, e: QDragEnterEvent | None): + if e is None: + return + data = e.mimeData() + if data and data.hasUrls(): + e.accept() else: - event.ignore() + e.ignore() - def dragMoveEvent(self, event: QDragEnterEvent): - if event.mimeData().hasUrls(): - event.accept() + def dragMoveEvent(self, e: QDragMoveEvent | None): + if e is None: + return + data = e.mimeData() + if data and data.hasUrls(): + e.accept() else: - event.ignore() + e.ignore() - def dropEvent(self, event: QDropEvent): - if event.mimeData().hasUrls(): + def dropEvent(self, e: QDropEvent | None): + if e is None: + return + data = e.mimeData() + if data and data.hasUrls(): files = [] - for url in event.mimeData().urls(): + for url in data.urls(): if url.isLocalFile(): files.append(url.path()) self.add_files(files) - event.accept() + e.accept() else: - event.ignore() + e.ignore() def setup_keyboard_shortcuts(self): """Setup shortcuts here""" diff --git a/components/PreferencesWindow.py b/components/PreferencesWindow.py index eb2afd3..b5a2470 100644 --- a/components/PreferencesWindow.py +++ b/components/PreferencesWindow.py @@ -1,23 +1,25 @@ from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QLineEdit, QPushButton from PyQt5.QtGui import QFont -import configparser + class PreferencesWindow(QDialog): def __init__(self, config): super(PreferencesWindow, self).__init__() - self.setWindowTitle('Preferences') + self.setWindowTitle("Preferences") self.config = config layout = QVBoxLayout() - label = QLabel('Preferences Window') + label = QLabel("Preferences Window") layout.addWidget(label) # Labels & input fields self.input_fields = {} for category in self.config.sections(): - category_label = QLabel(f'{category}') - category_label.setFont(QFont('', weight=QFont.Bold)) # bold category - category_label.setStyleSheet("text-transform:uppercase;") # uppercase category + category_label = QLabel(f"{category}") + category_label.setFont(QFont("", weight=QFont.Bold)) # bold category + category_label.setStyleSheet( + "text-transform:uppercase;" + ) # uppercase category layout.addWidget(category_label) for key in self.config[category]: label = QLabel(key) @@ -27,7 +29,7 @@ class PreferencesWindow(QDialog): self.input_fields[key] = input_field # Save button - save_button = QPushButton('Save') + save_button = QPushButton("Save") save_button.clicked.connect(self.save_preferences) layout.addWidget(save_button) self.setLayout(layout) @@ -40,8 +42,7 @@ class PreferencesWindow(QDialog): self.config[category][key] = self.input_fields[key].text() # Write the config file - with open('config.ini', 'w') as configfile: + with open("config.ini", "w") as configfile: self.config.write(configfile) self.close() - diff --git a/components/__init__.py b/components/__init__.py index de53109..60c5353 100644 --- a/components/__init__.py +++ b/components/__init__.py @@ -3,3 +3,4 @@ from .AlbumArtGraphicsView import AlbumArtGraphicsView from .AudioVisualizer import AudioVisualizer from .PreferencesWindow import PreferencesWindow from .ErrorDialog import ErrorDialog +from .LyricsWindow import LyricsWindow diff --git a/main.py b/main.py index 51fdb07..35da798 100644 --- a/main.py +++ b/main.py @@ -10,19 +10,20 @@ from configparser import ConfigParser import DBA from ui import Ui_MainWindow from PyQt5.QtWidgets import ( + QFileDialog, QMainWindow, QApplication, QGraphicsScene, QHeaderView, QGraphicsPixmapItem, + QMessageBox, ) from PyQt5.QtCore import QUrl, QTimer, QEvent, Qt, QModelIndex from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe from PyQt5.QtGui import QPixmap, QStandardItemModel from utils import scan_for_music from utils import delete_and_create_library_database -from components import AudioVisualizer -from components import PreferencesWindow +from components import PreferencesWindow, AudioVisualizer # Create ui.py file from Qt Designer # pyuic5 ui.ui -o ui.py @@ -89,14 +90,19 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.on_previous_clicked ) # Click to previous 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.actionPreferencesClicked + self.open_preferences ) # Open preferences menu + # QUICK ACTIONS MENU self.actionScanLibraries.triggered.connect(self.scan_libraries) # Scan library self.actionClearDatabase.triggered.connect( self.clear_database ) # Clear database - ## tableView + ## tableView triggers self.tableView.doubleClicked.connect( self.play_audio_file ) # Listens for the double click event, then plays the song @@ -322,17 +328,42 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): def on_next_clicked(self) -> None: 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.exec_() # Display the preferences window modally def scan_libraries(self) -> None: + """Scans for new files in the configured library folder + Refreshes the datagridview""" scan_for_music() self.tableView.fetch_library() def clear_database(self) -> None: - delete_and_create_library_database() - self.tableView.fetch_library() + """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() + self.tableView.fetch_library() def process_probe(self, buff) -> None: buff.startTime() diff --git a/ui.py b/ui.py index 01273c6..99ef281 100644 --- a/ui.py +++ b/ui.py @@ -154,7 +154,7 @@ class Ui_MainWindow(object): self.verticalLayout_3.setStretch(3, 1) MainWindow.setCentralWidget(self.centralwidget) 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.menuFile = QtWidgets.QMenu(self.menubar) self.menuFile.setObjectName("menuFile") @@ -174,6 +174,9 @@ class Ui_MainWindow(object): self.actionScanLibraries.setObjectName("actionScanLibraries") self.actionClearDatabase = QtWidgets.QAction(MainWindow) self.actionClearDatabase.setObjectName("actionClearDatabase") + self.actionOpenFiles = QtWidgets.QAction(MainWindow) + self.actionOpenFiles.setObjectName("actionOpenFiles") + self.menuFile.addAction(self.actionOpenFiles) self.menuEdit.addAction(self.actionPreferences) self.menuQuick_Actions.addAction(self.actionScanLibraries) self.menuQuick_Actions.addAction(self.actionClearDatabase) @@ -205,5 +208,6 @@ class Ui_MainWindow(object): self.actionPreferences.setStatusTip(_translate("MainWindow", "Open preferences")) self.actionScanLibraries.setText(_translate("MainWindow", "Scan libraries")) self.actionClearDatabase.setText(_translate("MainWindow", "Clear Database")) + self.actionOpenFiles.setText(_translate("MainWindow", "Open file(s)")) from components import AlbumArtGraphicsView, MusicTable from pyqtgraph import PlotWidget diff --git a/ui.ui b/ui.ui index 1c0ef44..caa3623 100644 --- a/ui.ui +++ b/ui.ui @@ -274,13 +274,14 @@ 0 0 1152 - 41 + 21 File + @@ -324,6 +325,11 @@ Clear Database + + + Open file(s) + + diff --git a/utils/add_files_to_library.py b/utils/add_files_to_library.py index 3cc6297..1f933ce 100644 --- a/utils/add_files_to_library.py +++ b/utils/add_files_to_library.py @@ -14,15 +14,15 @@ def add_files_to_library(files): """ if not files: return [] - print(f"utils/add_files_to_library: {files}") + # print(f"utils/add_files_to_library: {files}") extensions = config.get("settings", "extensions").split(",") insert_data = [] # To store data for batch insert for filepath in files: if any(filepath.lower().endswith(ext) for ext in extensions): filename = filepath.split("/")[-1] audio = get_id3_tags(filepath) - print('add_files_to_library audio:') - print(audio) + # print("add_files_to_library audio:") + # print(audio) # Skip if no title is found (but should never happen if "title" not in audio: continue diff --git a/utils/create_blank_mp3.sh b/utils/create_blank_mp3.sh new file mode 100644 index 0000000..942d9fc --- /dev/null +++ b/utils/create_blank_mp3.sh @@ -0,0 +1,2 @@ +#!/bin/bash +ffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t 5 -q:a 9 -acodec libmp3lame out.mp3 diff --git a/utils/get_id3_tags.py b/utils/get_id3_tags.py index 7206308..8665b33 100644 --- a/utils/get_id3_tags.py +++ b/utils/get_id3_tags.py @@ -20,24 +20,25 @@ def get_id3_tags(file): is_easy_id3 = False audio = {} - print('get_id3_tags audio:') - print(audio) + # print('get_id3_tags audio:') + # print(audio) # Check if all tags are empty tags_are_empty = all(not values for values in audio.values()) if tags_are_empty: # split on / to get just the filename # 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 - 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 - audio.save() # type: ignore + if is_easy_id3: # i can ignore this error because of this check + audio.save() # type: ignore return audio + # import sys # my_file = sys.argv[1] # data = get_id3_tags(my_file) diff --git a/utils/set_id3_tag.py b/utils/set_id3_tag.py index 924a59d..44a8187 100644 --- a/utils/set_id3_tag.py +++ b/utils/set_id3_tag.py @@ -84,12 +84,12 @@ def set_id3_tag(filepath: str, tag_name: str, value: str): Args: filepath: path to the mp3 file tag_name: common name of the ID3 tag - value: valut to set for the tag + value: value to set for the tag Returns: True / False""" 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: @@ -105,11 +105,16 @@ def set_id3_tag(filepath: str, tag_name: str, value: str): if tdat_tag: # update TDAT if we have it 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 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, Frame): audio_file.tags.add(tag_class(encoding=3, text=value)) # Add the tag + print(f"AAAAAAAAAAAAAA") else: # dialog = ErrorDialog(f'ID3 tag not supported.\nTag: {tag_name}\nTag class: {tag_class}\nValue:{value}') # dialog.exec_() @@ -122,9 +127,9 @@ def set_id3_tag(filepath: str, tag_name: str, value: str): pass audio_file.save() - print("ID3 tags updated:") + print("set_id3_tag.py | ID3 tags updated:") print(get_id3_tags(filepath)) - print("-----") + print("set_id3_tag.py | -----") return True except Exception as e: