light changes

This commit is contained in:
tsi-billypom 2024-05-31 09:01:31 -04:00
parent 55dfc36bf0
commit 04201fcb1d
9 changed files with 107 additions and 130 deletions

View File

@ -27,7 +27,8 @@ python3 main.py
## Todo: ## Todo:
- [ ] Delete songs from library (del key || right-click delete)
- [x] Right-click menu - [x] Right-click menu
- [x] Editable lyrics textbox
- [ ] Delete songs from library (del key || right-click delete)
- [ ] .wav, .ogg, .flac convertor - [ ] .wav, .ogg, .flac convertor
- [ ] Editable lyrics textbox - [ ] Alternatives to Gstreamer?

View File

@ -16,16 +16,14 @@ from PyQt5.QtWidgets import (
QMessageBox, QMessageBox,
QAbstractItemView, QAbstractItemView,
) )
from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal, QTimer from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt, pyqtSignal, QTimer
from components.LyricsWindow import LyricsWindow 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
from utils import get_album_art from utils import get_album_art
from utils import set_id3_tag from utils import set_id3_tag
from utils import delete_and_create_library_database
from subprocess import Popen from subprocess import Popen
from sqlite3 import OperationalError
import logging import logging
import configparser import configparser
import os import os
@ -35,12 +33,13 @@ import shutil
class MusicTable(QTableView): class MusicTable(QTableView):
playPauseSignal = pyqtSignal() playPauseSignal = pyqtSignal()
enterKey = pyqtSignal() enterKey = pyqtSignal()
deleteKey = pyqtSignal()
def __init__(self, parent=None): def __init__(self: QTableView, 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 # Necessary for actions related to cell values
self.model = QStandardItemModel(self)
self.setModel(self.model) # Same as above 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")
@ -56,9 +55,9 @@ class MusicTable(QTableView):
] ]
# id3 names of headers # id3 names of headers
self.id3_headers = [ self.id3_headers = [
"title", "TIT2",
"artist", "TPE1",
"album", "TALB",
"content_type", "content_type",
None, None,
None, None,
@ -75,6 +74,7 @@ class MusicTable(QTableView):
# doubleClicked is a built in event for QTableView - we listen for this event and run set_current_song_filepath # doubleClicked is a built in event for QTableView - we listen for this event and run set_current_song_filepath
self.doubleClicked.connect(self.set_current_song_filepath) self.doubleClicked.connect(self.set_current_song_filepath)
self.enterKey.connect(self.set_current_song_filepath) self.enterKey.connect(self.set_current_song_filepath)
self.deleteKey.connect(self.delete_songs)
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
@ -104,7 +104,7 @@ class MusicTable(QTableView):
reply = QMessageBox.question( reply = QMessageBox.question(
self, self,
"Confirmation", "Confirmation",
"Are you sure you want to delete these songs?", "Remove these songs from the library? (Files stay on your computer)",
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes, QMessageBox.Yes,
) )
@ -139,11 +139,11 @@ class MusicTable(QTableView):
if selected_song_filepath is None: if selected_song_filepath is None:
return return
current_song = self.get_selected_song_metadata() current_song = self.get_selected_song_metadata()
print(f"MusicTable.py | show_lyrics_menu | current song: {current_song}") # print(f"MusicTable.py | show_lyrics_menu | current song: {current_song}")
try: try:
lyrics = current_song["USLT::XXX"].text lyrics = current_song["USLT::XXX"].text
except Exception as e: except Exception as e:
print(f'MusicTable.py | show_lyrics_menu | could not retrieve lyrics | {e}') print(f"MusicTable.py | show_lyrics_menu | could not retrieve lyrics | {e}")
lyrics = "" lyrics = ""
lyrics_window = LyricsWindow(selected_song_filepath, lyrics) lyrics_window = LyricsWindow(selected_song_filepath, lyrics)
lyrics_window.exec_() lyrics_window.exec_()
@ -180,6 +180,33 @@ class MusicTable(QTableView):
else: else:
e.ignore() e.ignore()
def keyPressEvent(self, event):
"""Press a key. Do a thing"""
key = event.key()
if key == Qt.Key_Space: # Spacebar to play/pause
self.toggle_play_pause()
elif key == Qt.Key_Up: # Arrow key navigation
current_index = self.currentIndex()
new_index = self.model.index(
current_index.row() - 1, current_index.column()
)
if new_index.isValid():
self.setCurrentIndex(new_index)
elif key == Qt.Key_Down: # Arrow key navigation
current_index = self.currentIndex()
new_index = self.model.index(
current_index.row() + 1, current_index.column()
)
if new_index.isValid():
self.setCurrentIndex(new_index)
elif key in (Qt.Key_Return, Qt.Key_Enter):
if self.state() != QAbstractItemView.EditingState:
self.enterKey.emit() # Enter key detected
else:
super().keyPressEvent(event)
else: # Default behavior
super().keyPressEvent(event)
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)
@ -222,8 +249,10 @@ class MusicTable(QTableView):
try: try:
# Read file metadata # Read file metadata
audio = ID3(filepath) audio = ID3(filepath)
artist = audio["TIT2"].text[0] if not '' or None else 'Unknown Artist' artist = (
album = audio["TALB"].text[0] if not '' or None else 'Unknown Album' audio["TIT2"].text[0] if not "" or None else "Unknown Artist"
)
album = audio["TALB"].text[0] if not "" or None else "Unknown Album"
# Determine the new path that needs to be made # Determine the new path that needs to be made
new_path = os.path.join( new_path = os.path.join(
target_dir, artist, album, os.path.basename(filepath) target_dir, artist, album, os.path.basename(filepath)
@ -249,33 +278,6 @@ class MusicTable(QTableView):
self, "Reorganization complete", "Files successfully reorganized" self, "Reorganization complete", "Files successfully reorganized"
) )
def keyPressEvent(self, event):
"""Press a key. Do a thing"""
key = event.key()
if key == Qt.Key_Space: # Spacebar to play/pause
self.toggle_play_pause()
elif key == Qt.Key_Up: # Arrow key navigation
current_index = self.currentIndex()
new_index = self.model.index(
current_index.row() - 1, current_index.column()
)
if new_index.isValid():
self.setCurrentIndex(new_index)
elif key == Qt.Key_Down: # Arrow key navigation
current_index = self.currentIndex()
new_index = self.model.index(
current_index.row() + 1, current_index.column()
)
if new_index.isValid():
self.setCurrentIndex(new_index)
elif key in (Qt.Key_Return, Qt.Key_Enter):
if self.state() != QAbstractItemView.EditingState:
self.enterKey.emit() # Enter key detected
else:
super().keyPressEvent(event)
else: # Default behavior
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:

View File

@ -1,4 +1,4 @@
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QLineEdit, QPushButton from PyQt5.QtWidgets import QDialog, QFrame, QVBoxLayout, QLabel, QLineEdit, QPushButton
from PyQt5.QtGui import QFont from PyQt5.QtGui import QFont
@ -6,15 +6,20 @@ 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.setMinimumSize(400, 400)
self.config = config self.config = config
layout = QVBoxLayout() layout = QVBoxLayout()
label = QLabel("Preferences Window") label = QLabel("Preferences")
label.setFont(QFont("Sans", weight=QFont.Bold))
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():
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
layout.addWidget(separator)
category_label = QLabel(f"{category}") category_label = QLabel(f"{category}")
category_label.setFont(QFont("Sans", weight=QFont.Bold)) # bold category category_label.setFont(QFont("Sans", weight=QFont.Bold)) # bold category
category_label.setStyleSheet( category_label.setStyleSheet(

View File

@ -14,7 +14,6 @@ def add_files_to_library(files):
""" """
if not files: if not files:
return [] return []
# 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:
@ -59,7 +58,7 @@ def add_files_to_library(files):
genre, genre,
filename.split(".")[-1], filename.split(".")[-1],
date, date,
bitrate bitrate,
) )
) )
# Check if batch size is reached # Check if batch size is reached

View File

@ -26,12 +26,19 @@ def create_waveform_from_file(file):
start_index = 0 start_index = 0
while start_index < len(audio_data): while start_index < len(audio_data):
end_index = start_index + process_chunk_size end_index = start_index + process_chunk_size
signal = audio_data[start_index:end_index].astype(float) # Get chunk and convert to float signal = audio_data[start_index:end_index].astype(
float
) # Get chunk and convert to float
# Take mean of absolute values per 0.5 seconds # Take mean of absolute values per 0.5 seconds
sub_waveform = np.nanmean( sub_waveform = np.nanmean(
np.pad(np.absolute(signal), (0, ((downsample - (signal.size % downsample)) % downsample)), mode='constant', constant_values=np.NaN).reshape(-1, downsample), np.pad(
axis=1 np.absolute(signal),
(0, ((downsample - (signal.size % downsample)) % downsample)),
mode="constant",
constant_values=np.NaN,
).reshape(-1, downsample),
axis=1,
) )
waveform = np.concatenate((waveform, sub_waveform)) waveform = np.concatenate((waveform, sub_waveform))
@ -39,17 +46,22 @@ def create_waveform_from_file(file):
# Plot waveforms # Plot waveforms
plt.figure(1) plt.figure(1)
plt.plot(waveform, color='blue') plt.plot(waveform, color="blue")
plt.plot(-waveform, color='blue') # Mirrored waveform plt.plot(-waveform, color="blue") # Mirrored waveform
# Fill in area # Fill in area
plt.fill_between(np.arange(len(waveform)), waveform, -waveform, color='blue', alpha=0.5) plt.fill_between(
np.arange(len(waveform)), waveform, -waveform, color="blue", alpha=0.5
)
# Remove decorations, labels, axes, etc # Remove decorations, labels, axes, etc
plt.axis('off') plt.axis("off")
# Graph goes to ends of pic # Graph goes to ends of pic
plt.xlim(0, len(waveform)) plt.xlim(0, len(waveform))
# Save pic # Save pic
plt.savefig('assets/now_playing_waveform.png', dpi=64, bbox_inches='tight', pad_inches=0) plt.savefig(
"assets/now_playing_waveform.png", dpi=64, bbox_inches="tight", pad_inches=0
)
# Show me tho # Show me tho
# plt.show() # plt.show()
# create_waveform_from_file(sys.argv[1]) # create_waveform_from_file(sys.argv[1])

View File

@ -19,7 +19,7 @@ class FFTAnalyser(QtCore.QThread):
calculated_visual = QtCore.pyqtSignal(np.ndarray) calculated_visual = QtCore.pyqtSignal(np.ndarray)
def __init__(self, player: 'MusicPlayer'): # noqa: F821 def __init__(self, player): # noqa: F821
super().__init__() super().__init__()
self.player = player self.player = player
self.reset_media() self.reset_media()
@ -32,7 +32,7 @@ class FFTAnalyser(QtCore.QThread):
def reset_media(self): def reset_media(self):
"""Resets the media to the currently playing song.""" """Resets the media to the currently playing song."""
audio_file = self.player.currentMedia().canonicalUrl().path() audio_file = self.player.currentMedia().canonicalUrl().path()
if os.name == 'nt' and audio_file.startswith('/'): if os.name == "nt" and audio_file.startswith("/"):
audio_file = audio_file[1:] audio_file = audio_file[1:]
if audio_file: if audio_file:
try: try:
@ -53,7 +53,9 @@ class FFTAnalyser(QtCore.QThread):
sample_count = int(self.song.frame_rate * 0.05) sample_count = int(self.song.frame_rate * 0.05)
start_index = int((self.player.position() / 1000) * self.song.frame_rate) start_index = int((self.player.position() / 1000) * self.song.frame_rate)
v_sample = self.samples[start_index:start_index+sample_count] # samples to analyse v_sample = self.samples[
start_index : start_index + sample_count
] # samples to analyse
# use FFTs to analyse frequency and amplitudes # use FFTs to analyse frequency and amplitudes
fourier = np.fft.fft(v_sample) fourier = np.fft.fft(v_sample)
@ -73,17 +75,26 @@ class FFTAnalyser(QtCore.QThread):
if not amps.size: if not amps.size:
point_samples.append(0) point_samples.append(0)
else: else:
point_samples.append(amps.max()*((1+self.sensitivity/10+(self.sensitivity-1)/10)**(n/50))) point_samples.append(
amps.max()
* (
(1 + self.sensitivity / 10 + (self.sensitivity - 1) / 10)
** (n / 50)
)
)
# Add the point_samples to the self.points array, the reason we have a separate # Add the point_samples to the self.points array, the reason we have a separate
# array (self.bars) is so that we can fade out the previous amplitudes from # array (self.bars) is so that we can fade out the previous amplitudes from
# the past # the past
for n, amp in enumerate(point_samples): for n, amp in enumerate(point_samples):
amp *= 2 amp *= 2
if (self.points[n] > 0 and amp < self.points[n] or if (
self.player.state() in (self.player.PausedState, self.player.StoppedState)): self.points[n] > 0
and amp < self.points[n]
or self.player.state()
in (self.player.PausedState, self.player.StoppedState)
):
self.points[n] -= self.points[n] / 10 # fade out self.points[n] -= self.points[n] / 10 # fade out
elif abs(self.points[n] - amp) > self.visual_delta_threshold: elif abs(self.points[n] - amp) > self.visual_delta_threshold:
self.points[n] = amp self.points[n] = amp

View File

@ -14,7 +14,7 @@ def get_id3_tags(file):
try: try:
audio = ID3(file) audio = ID3(file)
except: except Exception:
audio = {} audio = {}
# Check if all tags are empty # Check if all tags are empty
@ -24,10 +24,10 @@ def get_id3_tags(file):
frame = TIT2(encoding=3, text=[title]) frame = TIT2(encoding=3, text=[title])
audio["TIT2"] = frame audio["TIT2"] = frame
except Exception as e: except Exception as e:
print(f'get_id3_tags.py | Exception: {e}') print(f"get_id3_tags.py | Exception: {e}")
pass pass
try: try:
audio.save() # type: ignore audio.save() # type: ignore
except: except Exception:
pass pass
return audio return audio

View File

@ -1,7 +1,7 @@
import os import os
import DBA import DBA
from configparser import ConfigParser from configparser import ConfigParser
from utils import get_id3_tags from utils import add_files_to_library, get_id3_tags
from utils import safe_get from utils import safe_get
config = ConfigParser() config = ConfigParser()
@ -10,57 +10,7 @@ config.read("config.ini")
def scan_for_music(): def scan_for_music():
root_dir = config.get("directories", "library") root_dir = config.get("directories", "library")
extensions = config.get("settings", "extensions").split(",")
insert_data = [] # To store data for batch insert
for dirpath, dirnames, filenames in os.walk(root_dir): # for dirpath, dirnames, filenames ...
for filename in filenames: for _, _, filenames in os.walk(root_dir):
if any(filename.lower().endswith(ext) for ext in extensions): add_files_to_library(filenames)
filepath = os.path.join(dirpath, filename)
audio = get_id3_tags(filepath)
if "title" not in audio:
return
# Append data tuple to insert_data list
insert_data.append(
(
filepath,
safe_get(audio, "title", [])[0],
safe_get(audio, "album", [])[0] if "album" in audio else None,
safe_get(audio, "artist", [])[0] if "artist" in audio else None,
",".join(safe_get(audio, "genre", [])) if "genre" in audio else None,
filename.split(".")[-1],
safe_get(audio, "date", [])[0] if "date" in audio else None,
safe_get(audio, "bitrate", [])[0] if "birate" in audio else None,
)
)
# Check if batch size is reached
if len(insert_data) >= 1000:
with DBA.DBAccess() as db:
db.executemany(
"INSERT OR IGNORE INTO library (filepath, title, album, artist, genre, codec, album_date, bitrate) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
insert_data,
)
insert_data = [] # Reset the insert_data list
# Insert any remaining data
if insert_data:
with DBA.DBAccess() as db:
db.executemany(
"INSERT OR IGNORE INTO library (filepath, title, album, artist, genre, codec, album_date, bitrate) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
insert_data,
)
# id int unsigned auto_increment,
# title varchar(255),
# album varchar(255),
# artist varchar(255),
# genre varchar(255),
# codec varchar(15),
# album_date date,
# bitrate int unsigned,
# date_added TIMESTAMP default CURRENT_TIMESTAMP,
# scan_for_music(config.get('directories', 'library1'))

View File

@ -1,11 +1,8 @@
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 utils.handle_year_and_date_id3_tag import handle_year_and_date_id3_tag
from mutagen.id3 import ID3 from mutagen.id3 import ID3
from mutagen.id3._util import ID3NoHeaderError from mutagen.id3._util import ID3NoHeaderError
from mutagen.mp3 import MP3 from mutagen.id3._frames import (
from mutagen.easyid3 import EasyID3
from mutagen.id3 import (
Frame, Frame,
TIT2, TIT2,
TPE1, TPE1,