file>open files dialog and lyrics testing
This commit is contained in:
parent
767ce3fa9c
commit
a78b5c1a51
49
components/LyricsWindow.py
Normal file
49
components/LyricsWindow.py
Normal 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()
|
||||
@ -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):
|
||||
"""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"""
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -3,3 +3,4 @@ from .AlbumArtGraphicsView import AlbumArtGraphicsView
|
||||
from .AudioVisualizer import AudioVisualizer
|
||||
from .PreferencesWindow import PreferencesWindow
|
||||
from .ErrorDialog import ErrorDialog
|
||||
from .LyricsWindow import LyricsWindow
|
||||
|
||||
41
main.py
41
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,15 +328,40 @@ 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:
|
||||
"""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()
|
||||
|
||||
|
||||
6
ui.py
6
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
|
||||
|
||||
8
ui.ui
8
ui.ui
@ -274,13 +274,14 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1152</width>
|
||||
<height>41</height>
|
||||
<height>21</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuFile">
|
||||
<property name="title">
|
||||
<string>File</string>
|
||||
</property>
|
||||
<addaction name="actionOpenFiles"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuEdit">
|
||||
<property name="title">
|
||||
@ -324,6 +325,11 @@
|
||||
<string>Clear Database</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionOpenFiles">
|
||||
<property name="text">
|
||||
<string>Open file(s)</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
|
||||
@ -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
|
||||
|
||||
2
utils/create_blank_mp3.sh
Normal file
2
utils/create_blank_mp3.sh
Normal 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
|
||||
@ -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
|
||||
return audio
|
||||
|
||||
|
||||
# import sys
|
||||
# my_file = sys.argv[1]
|
||||
# data = get_id3_tags(my_file)
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user