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:
- [ ] Delete songs from library (del key || right-click delete)
- [x] Right-click menu
- [x] Editable lyrics textbox
- [ ] Delete songs from library (del key || right-click delete)
- [ ] .wav, .ogg, .flac convertor
- [ ] Editable lyrics textbox
- [ ] Alternatives to Gstreamer?

View File

@ -16,16 +16,14 @@ from PyQt5.QtWidgets import (
QMessageBox,
QAbstractItemView,
)
from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal, QTimer
from PyQt5.QtCore import QAbstractItemModel, 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
from utils import get_album_art
from utils import set_id3_tag
from utils import delete_and_create_library_database
from subprocess import Popen
from sqlite3 import OperationalError
import logging
import configparser
import os
@ -35,12 +33,13 @@ import shutil
class MusicTable(QTableView):
playPauseSignal = pyqtSignal()
enterKey = pyqtSignal()
deleteKey = pyqtSignal()
def __init__(self, parent=None):
def __init__(self: QTableView, parent=None):
# QTableView.__init__(self, 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
self.config = configparser.ConfigParser()
self.config.read("config.ini")
@ -56,9 +55,9 @@ class MusicTable(QTableView):
]
# id3 names of headers
self.id3_headers = [
"title",
"artist",
"album",
"TIT2",
"TPE1",
"TALB",
"content_type",
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
self.doubleClicked.connect(self.set_current_song_filepath)
self.enterKey.connect(self.set_current_song_filepath)
self.deleteKey.connect(self.delete_songs)
self.fetch_library()
self.setup_keyboard_shortcuts()
self.model.dataChanged.connect(self.on_cell_data_changed) # editing cells
@ -104,7 +104,7 @@ class MusicTable(QTableView):
reply = QMessageBox.question(
self,
"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,
)
@ -139,11 +139,11 @@ class MusicTable(QTableView):
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}")
# print(f"MusicTable.py | show_lyrics_menu | current song: {current_song}")
try:
lyrics = current_song["USLT::XXX"].text
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_window = LyricsWindow(selected_song_filepath, lyrics)
lyrics_window.exec_()
@ -180,6 +180,33 @@ class MusicTable(QTableView):
else:
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):
"""Setup shortcuts here"""
shortcut = QShortcut(QKeySequence("Ctrl+Shift+R"), self)
@ -222,8 +249,10 @@ class MusicTable(QTableView):
try:
# Read file metadata
audio = ID3(filepath)
artist = audio["TIT2"].text[0] if not '' or None else 'Unknown Artist'
album = audio["TALB"].text[0] if not '' or None else 'Unknown Album'
artist = (
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
new_path = os.path.join(
target_dir, artist, album, os.path.basename(filepath)
@ -249,33 +278,6 @@ class MusicTable(QTableView):
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):
"""Toggles the currently playing song by emitting a signal"""
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
@ -6,15 +6,20 @@ class PreferencesWindow(QDialog):
def __init__(self, config):
super(PreferencesWindow, self).__init__()
self.setWindowTitle("Preferences")
self.setMinimumSize(400, 400)
self.config = config
layout = QVBoxLayout()
label = QLabel("Preferences Window")
label = QLabel("Preferences")
label.setFont(QFont("Sans", weight=QFont.Bold))
layout.addWidget(label)
# Labels & input fields
self.input_fields = {}
for category in self.config.sections():
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
layout.addWidget(separator)
category_label = QLabel(f"{category}")
category_label.setFont(QFont("Sans", weight=QFont.Bold)) # bold category
category_label.setStyleSheet(

View File

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

View File

@ -26,12 +26,19 @@ def create_waveform_from_file(file):
start_index = 0
while start_index < len(audio_data):
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
sub_waveform = np.nanmean(
np.pad(np.absolute(signal), (0, ((downsample - (signal.size % downsample)) % downsample)), mode='constant', constant_values=np.NaN).reshape(-1, downsample),
axis=1
np.pad(
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))
@ -39,17 +46,22 @@ def create_waveform_from_file(file):
# Plot waveforms
plt.figure(1)
plt.plot(waveform, color='blue')
plt.plot(-waveform, color='blue') # Mirrored waveform
plt.plot(waveform, color="blue")
plt.plot(-waveform, color="blue") # Mirrored waveform
# 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
plt.axis('off')
plt.axis("off")
# Graph goes to ends of pic
plt.xlim(0, len(waveform))
# 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
# plt.show()
# create_waveform_from_file(sys.argv[1])

View File

@ -19,7 +19,7 @@ class FFTAnalyser(QtCore.QThread):
calculated_visual = QtCore.pyqtSignal(np.ndarray)
def __init__(self, player: 'MusicPlayer'): # noqa: F821
def __init__(self, player): # noqa: F821
super().__init__()
self.player = player
self.reset_media()
@ -32,7 +32,7 @@ class FFTAnalyser(QtCore.QThread):
def reset_media(self):
"""Resets the media to the currently playing song."""
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:]
if audio_file:
try:
@ -52,13 +52,15 @@ class FFTAnalyser(QtCore.QThread):
"""Calculates the amplitudes used for visualising the media."""
sample_count = int(self.song.frame_rate * 0.05)
start_index = int((self.player.position()/1000) * self.song.frame_rate)
v_sample = self.samples[start_index:start_index+sample_count] # samples to analyse
start_index = int((self.player.position() / 1000) * self.song.frame_rate)
v_sample = self.samples[
start_index : start_index + sample_count
] # samples to analyse
# use FFTs to analyse frequency and amplitudes
fourier = np.fft.fft(v_sample)
freq = np.fft.fftfreq(fourier.size, d=0.05)
amps = 2/v_sample.size * np.abs(fourier)
amps = 2 / v_sample.size * np.abs(fourier)
data = np.array([freq, amps]).T
point_range = 1 / self.resolution
@ -73,17 +75,26 @@ class FFTAnalyser(QtCore.QThread):
if not amps.size:
point_samples.append(0)
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
# array (self.bars) is so that we can fade out the previous amplitudes from
# the past
for n, amp in enumerate(point_samples):
amp *= 2
if (self.points[n] > 0 and amp < self.points[n] or
self.player.state() in (self.player.PausedState, self.player.StoppedState)):
if (
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
elif abs(self.points[n] - amp) > self.visual_delta_threshold:
self.points[n] = amp

View File

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

View File

@ -1,7 +1,7 @@
import os
import DBA
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
config = ConfigParser()
@ -10,57 +10,7 @@ config.read("config.ini")
def scan_for_music():
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 filename in filenames:
if any(filename.lower().endswith(ext) for ext in extensions):
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'))
# for dirpath, dirnames, filenames ...
for _, _, filenames in os.walk(root_dir):
add_files_to_library(filenames)

View File

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