light changes
This commit is contained in:
parent
55dfc36bf0
commit
04201fcb1d
@ -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?
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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])
|
||||
@ -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:
|
||||
@ -53,7 +53,9 @@ class FFTAnalyser(QtCore.QThread):
|
||||
|
||||
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
|
||||
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)
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user