Nodesigner (#1)

* moved ui and app logic into 1 main file

* typing and reorganization

* batch metadata editor window start

* track number functionality
This commit is contained in:
billy 2024-07-31 18:12:44 -04:00 committed by GitHub
parent 027cc3f664
commit 76b1e13cf1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 731 additions and 20 deletions

View File

@ -1,8 +1,8 @@
from PyQt5 import QtWidgets
import numpy as np import numpy as np
from PyQt5 import QtWidgets
from utils import FFTAnalyser from utils import FFTAnalyser
class AudioVisualizer(QtWidgets.QWidget): class AudioVisualizer(QtWidgets.QWidget):
"""_Audio Visualizer component_ """_Audio Visualizer component_
@ -12,6 +12,7 @@ class AudioVisualizer(QtWidgets.QWidget):
Returns: Returns:
_type_: _description_ _type_: _description_
""" """
def __init__(self, media_player): def __init__(self, media_player):
super().__init__() super().__init__()
self.media_player = media_player self.media_player = media_player

View File

@ -0,0 +1,47 @@
from PyQt5.QtWidgets import (
QDialog,
QFrame,
QVBoxLayout,
QLabel,
QLineEdit,
QPushButton,
)
from PyQt5.QtGui import QFont
from mutagen.id3 import ID3
class MetadataWindow(QDialog):
def __init__(self, songs: ID3 | dict):
super(MetadataWindow, self).__init__()
self.setWindowTitle("Edit metadata")
self.setMinimumSize(400, 400)
layout = QVBoxLayout()
label = QLabel("Edit metadata")
label.setFont(QFont("Sans", weight=QFont.Bold))
layout.addWidget(label)
# Labels and categories and stuff
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
layout.addWidget(separator)
category_label = QLabel("Edit metadata")
category_label.setFont(QFont("Sans", weight=QFont.Bold)) # bold category
category_label.setStyleSheet("text-transform:uppercase;") # uppercase category
# Editable fields
label = QLabel("Title")
input_field = QLineEdit({songs["TPE1"]})
layout.addWidget(label)
layout.addWidget(input_field)
# Save button
save_button = QPushButton("Save")
save_button.clicked.connect(self.save)
layout.addWidget(save_button)
self.setLayout(layout)
def save(self):
"""Save changes made to metadata for each song in dict"""
pass
self.close()

View File

@ -49,6 +49,7 @@ class MusicTable(QTableView):
"title", "title",
"artist", "artist",
"album", "album",
"track_number",
"genre", "genre",
"codec", "codec",
"year", "year",
@ -59,9 +60,10 @@ class MusicTable(QTableView):
"TIT2", "TIT2",
"TPE1", "TPE1",
"TALB", "TALB",
"TRCK",
"content_type", "content_type",
None, None,
None, "TDRC",
None, None,
] ]
# db names of headers # db names of headers
@ -327,7 +329,7 @@ class MusicTable(QTableView):
try: try:
with DBA.DBAccess() as db: with DBA.DBAccess() as db:
data = db.query( data = db.query(
"SELECT id, title, artist, album, genre, codec, album_date, filepath FROM song;", "SELECT id, title, artist, album, track_number, genre, codec, album_date, filepath FROM song;",
(), (),
) )
except Exception as e: except Exception as e:

652
new.py Normal file
View File

@ -0,0 +1,652 @@
import os
import configparser
import sys
import logging
from subprocess import run
import qdarktheme
from pyqtgraph import PlotWidget
from pyqtgraph import mkBrush
from mutagen.id3 import ID3
from mutagen.id3._frames import APIC
from configparser import ConfigParser
import DBA
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import (
QFileDialog,
QMainWindow,
QApplication,
QGraphicsScene,
QHeaderView,
QGraphicsPixmapItem,
QMessageBox,
)
from PyQt5.QtCore import QUrl, QTimer, Qt
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe
from PyQt5.QtGui import QCloseEvent, QPixmap
from utils import scan_for_music, delete_and_create_library_database, initialize_db
from components import (
PreferencesWindow,
AudioVisualizer,
AlbumArtGraphicsView,
MusicTable,
)
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
global stopped
stopped = False
# Vars
self.setupUi(self)
self.setWindowTitle("MusicPom")
self.selected_song_filepath: str | None = None
self.current_song_filepath: str | None = None
self.current_song_metadata: ID3 | dict | None = None
self.current_song_album_art: bytes | None = None
self.album_art_scene: QGraphicsScene = QGraphicsScene()
self.config: ConfigParser = configparser.ConfigParser()
self.player: QMediaPlayer = QMediaPlayer() # Audio player object
self.probe: QAudioProbe = QAudioProbe() # Gets audio data
self.audio_visualizer: AudioVisualizer = AudioVisualizer(self.player)
self.current_volume: int = 50
# Initialization
self.config.read("config.ini")
self.player.setVolume(self.current_volume)
# Audio probe for processing audio signal in real time
self.probe.setSource(self.player)
self.probe.audioBufferProbed.connect(self.process_probe)
# Slider Timer (realtime playback feedback horizontal bar)
self.timer: QTimer = QTimer(self) # Audio timing things
self.timer.start(
150
) # 150ms update interval solved problem with drag seeking halting playback
self.timer.timeout.connect(self.move_slider)
# Graphics plot
self.PlotWidget.setXRange(0, 100, padding=0) # x axis range
self.PlotWidget.setYRange(0, 0.3, padding=0) # y axis range
self.PlotWidget.getAxis("bottom").setTicks([]) # Remove x-axis ticks
self.PlotWidget.getAxis("bottom").setLabel("") # Remove x-axis label
self.PlotWidget.setLogMode(False, False)
# Remove y-axis labels and decorations
self.PlotWidget.getAxis("left").setTicks([]) # Remove y-axis ticks
self.PlotWidget.getAxis("left").setLabel("") # Remove y-axis label
# _____________
# | |
# | CONNECTIONS |
# | CONNECTIONS |
# | CONNECTIONS |
# |_____________|
# FIXME: moving the slider while playing is happening frequently causes playback to halt - the pyqtgraph is also affected by this
# so it must be affecting our QMediaPlayer as well
# self.playbackSlider.sliderMoved[int].connect(
# lambda: self.player.setPosition(self.playbackSlider.value())
# )
self.playbackSlider.sliderReleased.connect(
lambda: self.player.setPosition(self.playbackSlider.value())
) # maybe sliderReleased works better than sliderMoved
self.volumeSlider.sliderMoved[int].connect(
lambda: self.volume_changed()
) # Move slider to adjust volume
# Playback controls
self.playButton.clicked.connect(self.on_play_clicked)
self.prevButton.clicked.connect(self.on_previous_clicked)
self.nextButton.clicked.connect(self.on_next_clicked)
# FILE MENU
self.actionOpenFiles.triggered.connect(self.open_files) # Open files window
# EDIT MENU
self.actionPreferences.triggered.connect(
self.open_preferences
) # Open preferences menu
# VIEW MENU
# QUICK ACTIONS MENU
self.actionScanLibraries.triggered.connect(self.scan_libraries)
self.actionDeleteLibrary.triggered.connect(self.clear_database)
self.actionDeleteDatabase.triggered.connect(self.delete_database)
## Music Table | self.tableView triggers
# Listens for the double click event, then plays the song
self.tableView.doubleClicked.connect(self.play_audio_file)
# Listens for the enter key event, then plays the song
self.tableView.enterKey.connect(self.play_audio_file)
# Spacebar for toggle play/pause
self.tableView.playPauseSignal.connect(self.on_play_clicked)
# Album Art | self.albumGraphicsView
self.albumGraphicsView.albumArtDropped.connect(
self.set_album_art_for_selected_songs
)
self.albumGraphicsView.albumArtDeleted.connect(
self.delete_album_art_for_selected_songs
)
self.tableView.viewport().installEventFilter(
self
) # for drag & drop functionality
# set column widths
table_view_column_widths = str(self.config["table"]["column_widths"]).split(",")
for i in range(self.tableView.model.columnCount()):
self.tableView.setColumnWidth(i, int(table_view_column_widths[i]))
# dont extend last column past table view border
self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.tableView.horizontalHeader().setStretchLastSection(False)
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(1152, 894)
MainWindow.setStatusTip("")
# Main
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
#
self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.hLayoutHead = QtWidgets.QHBoxLayout()
self.hLayoutHead.setObjectName("hLayoutHead")
self.vlayoutAlbumArt = QtWidgets.QVBoxLayout()
self.vlayoutAlbumArt.setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
self.vlayoutAlbumArt.setObjectName("vlayoutAlbumArt")
self.albumGraphicsView = AlbumArtGraphicsView(self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.albumGraphicsView.sizePolicy().hasHeightForWidth()
)
self.albumGraphicsView.setSizePolicy(sizePolicy)
self.albumGraphicsView.setMinimumSize(QtCore.QSize(200, 200))
self.albumGraphicsView.setMaximumSize(QtCore.QSize(16777215, 16777215))
self.albumGraphicsView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.albumGraphicsView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.albumGraphicsView.setHorizontalScrollBarPolicy(
QtCore.Qt.ScrollBarAlwaysOff
)
self.albumGraphicsView.setSizeAdjustPolicy(
QtWidgets.QAbstractScrollArea.AdjustIgnored
)
self.albumGraphicsView.setInteractive(False)
self.albumGraphicsView.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter)
self.albumGraphicsView.setViewportUpdateMode(
QtWidgets.QGraphicsView.FullViewportUpdate
)
self.albumGraphicsView.setObjectName("albumGraphicsView")
self.vlayoutAlbumArt.addWidget(self.albumGraphicsView)
self.hLayoutHead.addLayout(self.vlayoutAlbumArt)
self.vLayoutSongDetails = QtWidgets.QVBoxLayout()
self.vLayoutSongDetails.setObjectName("vLayoutSongDetails")
self.artistLabel = QtWidgets.QLabel(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(24)
font.setBold(True)
font.setWeight(75)
self.artistLabel.setFont(font)
self.artistLabel.setObjectName("artistLabel")
self.vLayoutSongDetails.addWidget(self.artistLabel)
self.titleLabel = QtWidgets.QLabel(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(18)
self.titleLabel.setFont(font)
self.titleLabel.setObjectName("titleLabel")
self.vLayoutSongDetails.addWidget(self.titleLabel)
self.albumLabel = QtWidgets.QLabel(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(16)
font.setBold(False)
font.setItalic(True)
font.setWeight(50)
self.albumLabel.setFont(font)
self.albumLabel.setObjectName("albumLabel")
self.vLayoutSongDetails.addWidget(self.albumLabel)
self.hLayoutHead.addLayout(self.vLayoutSongDetails)
self.vLayoutPlaybackVisuals = QtWidgets.QVBoxLayout()
self.vLayoutPlaybackVisuals.setObjectName("vLayoutPlaybackVisuals")
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.playbackSlider = QtWidgets.QSlider(self.centralwidget)
self.playbackSlider.setOrientation(QtCore.Qt.Horizontal)
self.playbackSlider.setObjectName("playbackSlider")
self.horizontalLayout.addWidget(self.playbackSlider)
self.startTimeLabel = QtWidgets.QLabel(self.centralwidget)
self.startTimeLabel.setObjectName("startTimeLabel")
self.horizontalLayout.addWidget(self.startTimeLabel)
self.slashLabel = QtWidgets.QLabel(self.centralwidget)
self.slashLabel.setObjectName("slashLabel")
self.horizontalLayout.addWidget(self.slashLabel)
self.endTimeLabel = QtWidgets.QLabel(self.centralwidget)
self.endTimeLabel.setObjectName("endTimeLabel")
self.horizontalLayout.addWidget(self.endTimeLabel)
self.vLayoutPlaybackVisuals.addLayout(self.horizontalLayout)
self.PlotWidget = PlotWidget(self.centralwidget)
self.PlotWidget.setObjectName("PlotWidget")
self.vLayoutPlaybackVisuals.addWidget(self.PlotWidget)
self.hLayoutHead.addLayout(self.vLayoutPlaybackVisuals)
self.hLayoutHead.setStretch(0, 1)
self.hLayoutHead.setStretch(1, 4)
self.hLayoutHead.setStretch(2, 6)
self.verticalLayout_3.addLayout(self.hLayoutHead)
self.hLayoutMusicTable = QtWidgets.QHBoxLayout()
self.hLayoutMusicTable.setObjectName("hLayoutMusicTable")
self.tableView = MusicTable(self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(1)
sizePolicy.setHeightForWidth(self.tableView.sizePolicy().hasHeightForWidth())
self.tableView.setSizePolicy(sizePolicy)
self.tableView.setMaximumSize(QtCore.QSize(32000, 32000))
self.tableView.setAcceptDrops(True)
self.tableView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.tableView.setSizeAdjustPolicy(
QtWidgets.QAbstractScrollArea.AdjustToContents
)
self.tableView.setEditTriggers(
QtWidgets.QAbstractItemView.AnyKeyPressed
| QtWidgets.QAbstractItemView.EditKeyPressed
)
self.tableView.setAlternatingRowColors(True)
self.tableView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.tableView.setSortingEnabled(True)
self.tableView.setObjectName("tableView")
self.tableView.horizontalHeader().setCascadingSectionResizes(True)
self.tableView.horizontalHeader().setStretchLastSection(True)
self.tableView.verticalHeader().setVisible(False)
self.hLayoutMusicTable.addWidget(self.tableView)
self.verticalLayout_3.addLayout(self.hLayoutMusicTable)
self.hLayoutControls = QtWidgets.QHBoxLayout()
self.hLayoutControls.setObjectName("hLayoutControls")
self.prevButton = QtWidgets.QPushButton(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(28)
self.prevButton.setFont(font)
self.prevButton.setObjectName("prevButton")
self.hLayoutControls.addWidget(self.prevButton)
self.playButton = QtWidgets.QPushButton(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(28)
self.playButton.setFont(font)
self.playButton.setObjectName("playButton")
self.hLayoutControls.addWidget(self.playButton)
self.nextButton = QtWidgets.QPushButton(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(28)
self.nextButton.setFont(font)
self.nextButton.setObjectName("nextButton")
self.hLayoutControls.addWidget(self.nextButton)
self.verticalLayout_3.addLayout(self.hLayoutControls)
self.hLayoutControls2 = QtWidgets.QHBoxLayout()
self.hLayoutControls2.setObjectName("hLayoutControls2")
self.volumeSlider = QtWidgets.QSlider(self.centralwidget)
self.volumeSlider.setMaximum(100)
self.volumeSlider.setProperty("value", 50)
self.volumeSlider.setOrientation(QtCore.Qt.Horizontal)
self.volumeSlider.setObjectName("volumeSlider")
self.hLayoutControls2.addWidget(self.volumeSlider)
self.verticalLayout_3.addLayout(self.hLayoutControls2)
self.verticalLayout_3.setStretch(0, 3)
self.verticalLayout_3.setStretch(1, 8)
self.verticalLayout_3.setStretch(2, 1)
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.setObjectName("menubar")
self.menuFile = QtWidgets.QMenu(self.menubar)
self.menuFile.setObjectName("menuFile")
self.menuEdit = QtWidgets.QMenu(self.menubar)
self.menuEdit.setObjectName("menuEdit")
self.menuView = QtWidgets.QMenu(self.menubar)
self.menuView.setObjectName("menuView")
self.menuQuick_Actions = QtWidgets.QMenu(self.menubar)
self.menuQuick_Actions.setObjectName("menuQuick_Actions")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.actionPreferences = QtWidgets.QAction(MainWindow)
self.actionPreferences.setObjectName("actionPreferences")
self.actionScanLibraries = QtWidgets.QAction(MainWindow)
self.actionScanLibraries.setObjectName("actionScanLibraries")
self.actionDeleteLibrary = QtWidgets.QAction(MainWindow)
self.actionDeleteLibrary.setObjectName("actionDeleteLibrary")
self.actionOpenFiles = QtWidgets.QAction(MainWindow)
self.actionOpenFiles.setObjectName("actionOpenFiles")
self.actionDeleteDatabase = QtWidgets.QAction(MainWindow)
self.actionDeleteDatabase.setObjectName("actionDeleteDatabase")
self.menuFile.addAction(self.actionOpenFiles)
self.menuEdit.addAction(self.actionPreferences)
self.menuQuick_Actions.addAction(self.actionScanLibraries)
self.menuQuick_Actions.addAction(self.actionDeleteLibrary)
self.menuQuick_Actions.addAction(self.actionDeleteDatabase)
self.menubar.addAction(self.menuFile.menuAction())
self.menubar.addAction(self.menuEdit.menuAction())
self.menubar.addAction(self.menuView.menuAction())
self.menubar.addAction(self.menuQuick_Actions.menuAction())
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.artistLabel.setText(_translate("MainWindow", "artist"))
self.titleLabel.setText(_translate("MainWindow", "song title"))
self.albumLabel.setText(_translate("MainWindow", "album"))
self.startTimeLabel.setText(_translate("MainWindow", "00:00"))
self.slashLabel.setText(_translate("MainWindow", "/"))
self.endTimeLabel.setText(_translate("MainWindow", "00:00"))
self.prevButton.setText(_translate("MainWindow", "⏮️"))
self.playButton.setText(_translate("MainWindow", "▶️"))
self.nextButton.setText(_translate("MainWindow", "⏭️"))
self.menuFile.setTitle(_translate("MainWindow", "File"))
self.menuEdit.setTitle(_translate("MainWindow", "Edit"))
self.menuView.setTitle(_translate("MainWindow", "View"))
self.menuQuick_Actions.setTitle(_translate("MainWindow", "Quick-Actions"))
self.actionPreferences.setText(_translate("MainWindow", "Preferences"))
self.actionPreferences.setStatusTip(
_translate("MainWindow", "Open preferences")
)
self.actionScanLibraries.setText(_translate("MainWindow", "Scan libraries"))
self.actionDeleteLibrary.setText(_translate("MainWindow", "Delete Library"))
self.actionOpenFiles.setText(_translate("MainWindow", "Open file(s)"))
self.actionDeleteDatabase.setText(_translate("MainWindow", "Delete Database"))
def closeEvent(self, a0: QCloseEvent | None) -> None:
"""Save settings when closing the application"""
# MusicTable/tableView column widths
list_of_column_widths = []
for i in range(self.tableView.model.columnCount()):
list_of_column_widths.append(str(self.tableView.columnWidth(i)))
column_widths_as_string = ",".join(list_of_column_widths)
self.config["table"]["column_widths"] = column_widths_as_string
# Save the config
with open("config.ini", "w") as configfile:
self.config.write(configfile)
super().closeEvent(a0)
def load_album_art(self, album_art_data) -> None:
"""Displays the album art for the currently playing track in the GraphicsView"""
if self.current_song_album_art:
# Clear the scene
try:
self.album_art_scene.clear()
except Exception:
pass
# Reset the scene
self.albumGraphicsView.setScene(None)
# Create pixmap for album art
pixmap = QPixmap()
pixmap.loadFromData(album_art_data)
# Create a QGraphicsPixmapItem for more control over pic
pixmapItem = QGraphicsPixmapItem(pixmap)
pixmapItem.setTransformationMode(
Qt.SmoothTransformation
) # For better quality scaling
# Add pixmap item to the scene
self.album_art_scene.addItem(pixmapItem)
# Set the scene
self.albumGraphicsView.setScene(self.album_art_scene)
# Adjust the album art scaling
self.adjustPixmapScaling(pixmapItem)
def adjustPixmapScaling(self, pixmapItem) -> None:
"""Adjust the scaling of the pixmap item to fit the QGraphicsView, maintaining aspect ratio"""
viewWidth = self.albumGraphicsView.width()
viewHeight = self.albumGraphicsView.height()
pixmapSize = pixmapItem.pixmap().size()
# Calculate scaling factor while maintaining aspect ratio
scaleX = viewWidth / pixmapSize.width()
scaleY = viewHeight / pixmapSize.height()
scaleFactor = min(scaleX, scaleY)
# Apply scaling to the pixmap item
pixmapItem.setScale(scaleFactor)
def set_album_art_for_selected_songs(self, album_art_path: str) -> None:
"""Sets the ID3 tag APIC (album art) for all selected song filepaths"""
selected_songs = self.tableView.get_selected_songs_filepaths()
for song in selected_songs:
print(f"updating album art for {song}")
self.update_album_art_for_song(song, album_art_path)
def update_album_art_for_song(
self, song_file_path: str, album_art_path: str
) -> None:
"""Updates the ID3 tag APIC (album art) for 1 song"""
# audio = MP3(song_file_path, ID3=ID3)
audio = ID3(song_file_path)
# Remove existing APIC Frames (album art)
audio.delall("APIC")
# Add the album art
with open(album_art_path, "rb") as album_art_file:
if album_art_path.endswith(".jpg") or album_art_path.endswith(".jpeg"):
audio.add(
APIC(
encoding=3, # 3 = utf-8
mime="image/jpeg",
type=3, # 3 = cover image
desc="Cover",
data=album_art_file.read(),
)
)
elif album_art_path.endswith(".png"):
audio.add(
APIC(
encoding=3, # 3 = utf-8
mime="image/png",
type=3, # 3 = cover image
desc="Cover",
data=album_art_file.read(),
)
)
audio.save()
def delete_album_art_for_selected_songs(self) -> None:
"""Handles deleting the ID3 tag APIC (album art) for all selected songs"""
filepaths = self.tableView.get_selected_songs_filepaths()
for file in filepaths:
# delete APIC data
try:
audio = ID3(file)
if "APIC:" in audio:
del audio["APIC"]
audio.save()
except Exception as e:
print(f"Error processing {file}: {e}")
def play_audio_file(self) -> None:
"""Start playback of tableView.current_song_filepath track & moves playback slider"""
self.current_song_metadata = self.tableView.get_current_song_metadata()
self.current_song_album_art = self.tableView.get_current_song_album_art()
# read the file
url = QUrl.fromLocalFile(self.tableView.get_current_song_filepath())
content = QMediaContent(url) # load the audio content
self.player.setMedia(content) # what content to play
self.player.play() # play
self.move_slider() # mover
# assign metadata
artist = (
self.current_song_metadata["TPE1"][0]
if "artist" in self.current_song_metadata
else None
)
album = (
self.current_song_metadata["TALB"][0]
if "album" in self.current_song_metadata
else None
)
title = self.current_song_metadata["TIT2"][0]
# edit labels
self.artistLabel.setText(artist)
self.albumLabel.setText(album)
self.titleLabel.setText(title)
# set album artwork
self.load_album_art(self.current_song_album_art)
def on_play_clicked(self) -> None:
"""Updates the Play & Pause buttons when clicked"""
if self.player.state() == QMediaPlayer.State.PlayingState:
self.player.pause()
self.playButton.setText("▶️")
else:
if self.player.state() == QMediaPlayer.State.PausedState:
self.player.play()
self.playButton.setText("⏸️")
else:
self.play_audio_file()
self.playButton.setText("⏸️")
def on_previous_clicked(self) -> None:
""""""
print("previous")
def on_next_clicked(self) -> None:
print("next")
def update_audio_visualization(self) -> None:
"""Handles upading points on the pyqtgraph visual"""
self.clear_audio_visualization()
y = self.audio_visualizer.get_amplitudes()
x = [i for i in range(len(y))]
self.PlotWidget.plot(x, y, fillLevel=0, fillBrush=mkBrush("b"))
self.PlotWidget.show()
def clear_audio_visualization(self) -> None:
self.PlotWidget.clear()
def move_slider(self) -> None:
"""Handles moving the playback slider"""
if stopped:
return
else:
# Update the slider
if self.player.state() == QMediaPlayer.State.PlayingState:
self.playbackSlider.setMinimum(0)
self.playbackSlider.setMaximum(self.player.duration())
slider_position = self.player.position()
self.playbackSlider.setValue(slider_position)
current_minutes, current_seconds = divmod(slider_position / 1000, 60)
duration_minutes, duration_seconds = divmod(
self.player.duration() / 1000, 60
)
self.startTimeLabel.setText(
f"{int(current_minutes):02d}:{int(current_seconds):02d}"
)
self.endTimeLabel.setText(
f"{int(duration_minutes):02d}:{int(duration_seconds):02d}"
)
def volume_changed(self) -> None:
"""Handles volume changes"""
try:
self.current_volume = self.volumeSlider.value()
self.player.setVolume(self.current_volume)
except Exception as e:
print(f"Changing volume error: {e}")
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()
def delete_database(self) -> None:
"""Deletes the entire database"""
reply = QMessageBox.question(
self,
"Confirmation",
"Delete database?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes,
)
if reply:
initialize_db()
self.tableView.fetch_library()
def reinitialize_database(self) -> None:
"""Clears all tables in database and recreates"""
reply = QMessageBox.question(
self,
"Confirmation",
"Recreate the database?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes,
)
if reply:
initialize_db()
self.tableView.fetch_library()
def process_probe(self, buff) -> None:
buff.startTime()
self.update_audio_visualization()
if __name__ == "__main__":
# First run initialization
if not os.path.exists("config.ini"):
# Create config file from sample
run(["cp", "sample_config.ini", "config.ini"])
config = ConfigParser()
config.read("config.ini")
db_name = config.get("db", "database")
db_path = db_name.split("/")
db_path.pop()
path_as_string = "/".join(db_path)
if not os.path.exists(path_as_string):
os.makedirs(path_as_string)
# Create database on first run
with DBA.DBAccess() as db:
with open("utils/init.sql", "r") as file:
lines = file.read()
for statement in lines.split(";"):
print(f"executing [{statement}]")
db.execute(statement, ())
# logging setup
logging.basicConfig(filename="musicpom.log", encoding="utf-8", level=logging.DEBUG)
# Allow for dynamic imports of my custom classes and utilities
project_root = os.path.abspath(os.path.dirname(__file__))
sys.path.append(project_root)
# Start the app
app = QApplication(sys.argv)
print(f"main.py app: {app}")
# Dark theme >:3
qdarktheme.setup_theme()
# Show the UI
ui = MainWindow()
ui.show()
sys.exit(app.exec_())

View File

@ -1,3 +1,4 @@
from .fft_analyser import FFTAnalyser
from .id3_timestamp_to_datetime import id3_timestamp_to_datetime from .id3_timestamp_to_datetime import id3_timestamp_to_datetime
from .initialize_db import initialize_db from .initialize_db import initialize_db
from .safe_get import safe_get from .safe_get import safe_get
@ -7,6 +8,5 @@ from .set_id3_tag import set_id3_tag
from .delete_and_create_library_database import delete_and_create_library_database from .delete_and_create_library_database import delete_and_create_library_database
from .update_song_in_library import update_song_in_library from .update_song_in_library import update_song_in_library
from .scan_for_music import scan_for_music from .scan_for_music import scan_for_music
from .fft_analyser import FFTAnalyser
from .add_files_to_library import add_files_to_library from .add_files_to_library import add_files_to_library
from .handle_year_and_date_id3_tag import handle_year_and_date_id3_tag from .handle_year_and_date_id3_tag import handle_year_and_date_id3_tag

View File

@ -34,6 +34,10 @@ def add_files_to_library(files):
album = audio["TALB"].text[0] album = audio["TALB"].text[0]
except KeyError: except KeyError:
album = "" album = ""
try:
track_number = audio["TRCK"].text[0]
except KeyError:
track_number = 0
try: try:
genre = audio["TCON"].text[0] genre = audio["TCON"].text[0]
except KeyError: except KeyError:
@ -54,6 +58,7 @@ def add_files_to_library(files):
title, title,
album, album,
artist, artist,
track_number,
genre, genre,
filename.split(".")[-1], filename.split(".")[-1],
date, date,
@ -64,7 +69,7 @@ def add_files_to_library(files):
if len(insert_data) >= 1000: if len(insert_data) >= 1000:
with DBA.DBAccess() as db: with DBA.DBAccess() as db:
db.executemany( db.executemany(
"INSERT OR IGNORE INTO song (filepath, title, album, artist, genre, codec, album_date, bitrate) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", "INSERT OR IGNORE INTO song (filepath, title, album, artist, track_number, genre, codec, album_date, bitrate) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
insert_data, insert_data,
) )
insert_data = [] # Reset the insert_data list insert_data = [] # Reset the insert_data list
@ -72,7 +77,7 @@ def add_files_to_library(files):
if insert_data: if insert_data:
with DBA.DBAccess() as db: with DBA.DBAccess() as db:
db.executemany( db.executemany(
"INSERT OR IGNORE INTO song (filepath, title, album, artist, genre, codec, album_date, bitrate) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", "INSERT OR IGNORE INTO song (filepath, title, album, artist, track_number, genre, codec, album_date, bitrate) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
insert_data, insert_data,
) )
return True return True

View File

@ -7,6 +7,7 @@ CREATE TABLE song(
title varchar(255), title varchar(255),
album varchar(255), album varchar(255),
artist varchar(255), artist varchar(255),
track_number integer,
genre varchar(255), genre varchar(255),
codec varchar(15), codec varchar(15),
album_date date, album_date date,

View File

@ -1,14 +1,9 @@
# Credit # Credit
# https://github.com/ravenkls/MilkPlayer/blob/master/audio/fft_analyser.py # https://github.com/ravenkls/MilkPlayer/blob/master/audio/fft_analyser.py
# std
import time import time
import os import os
# qt
from PyQt5 import QtCore from PyQt5 import QtCore
# other
from pydub import AudioSegment from pydub import AudioSegment
import numpy as np import numpy as np
from scipy.ndimage.filters import gaussian_filter1d from scipy.ndimage.filters import gaussian_filter1d

View File

@ -5,11 +5,14 @@ import os
def get_id3_tags(file): def get_id3_tags(file):
"""Get the ID3 tags for an audio file """Get the ID3 tags for an audio file
# Parameters # Parameters
`file` | str | Fully qualified path to file `file` | str | Fully qualified path to file
# Returns # Returns
dict of all id3 tags dict of all id3 tags
if all tags are empty, at minimum fill in the 'title' at minimum we will get the filename as a title.[ID3:TIT2]
""" """
try: try:
@ -20,6 +23,7 @@ def get_id3_tags(file):
# Check if all tags are empty # Check if all tags are empty
# tags_are_empty = all(not values for values in audio.values()) # tags_are_empty = all(not values for values in audio.values())
try: try:
if audio["TIT2"] is None:
title = os.path.splitext(os.path.basename(file))[0] title = os.path.splitext(os.path.basename(file))[0]
frame = TIT2(encoding=3, text=[title]) frame = TIT2(encoding=3, text=[title])
audio["TIT2"] = frame audio["TIT2"] = frame

View File

@ -9,6 +9,7 @@ CREATE TABLE song(
title varchar(255), title varchar(255),
album varchar(255), album varchar(255),
artist varchar(255), artist varchar(255),
track_number integer,
genre varchar(255), genre varchar(255),
codec varchar(15), codec varchar(15),
album_date date, album_date date,

View File

@ -1,5 +1,5 @@
import logging import logging
from components.ErrorDialog import ErrorDialog from components import ErrorDialog
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
@ -87,15 +87,16 @@ def set_id3_tag(filepath: str, tag_name: str, value: str):
Returns: Returns:
True / False""" True / False"""
# print( print(
# f"set_id3_tag.py | filepath: {filepath} | tag_name: {tag_name} | value: {value}" f"set_id3_tag.py | filepath: {filepath} | tag_name: {tag_name} | value: {value}"
# ) )
try: try:
try: # Load existing tags try: # Load existing tags
audio_file = ID3(filepath) audio_file = ID3(filepath)
except ID3NoHeaderError: # Create new tags if none exist except ID3NoHeaderError: # Create new tags if none exist
audio_file = ID3() audio_file = ID3()
# Date handling - TDRC vs TYER+TDAT
if tag_name == "album_date": if tag_name == "album_date":
tyer_tag, tdat_tag = handle_year_and_date_id3_tag(value) tyer_tag, tdat_tag = handle_year_and_date_id3_tag(value)
# always update TYER # always update TYER
@ -103,6 +104,7 @@ def set_id3_tag(filepath: str, tag_name: str, value: str):
if tdat_tag: if tdat_tag:
# update TDAT if we have it # update TDAT if we have it
audio_file.add(tdat_tag) audio_file.add(tdat_tag)
# Lyrics
elif tag_name == "lyrics" or tag_name == "USLT": elif tag_name == "lyrics" or tag_name == "USLT":
try: try:
audio = ID3(filepath) audio = ID3(filepath)
@ -116,6 +118,7 @@ def set_id3_tag(filepath: str, tag_name: str, value: str):
audio.add(frame) audio.add(frame)
audio.save() audio.save()
return True return True
# Other
elif tag_name in id3_tag_mapping: # Tag accounted for elif tag_name in id3_tag_mapping: # Tag accounted for
tag_class = id3_tag_mapping[tag_name] tag_class = id3_tag_mapping[tag_name]
if issubclass(tag_class, Frame): if issubclass(tag_class, Frame):