diff --git a/README.md b/README.md index ae5c468..59887ba 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,31 @@ PyQt5 music player for Linux inspired by MusicBee & iTunes ## Installation: clone the repo -``` +```bash git clone https://github.com/billypom/musicpom ``` + install system packages -``` +```bash sudo apt install ffmpeg, python3-pyqt5 ``` + create environment -``` +```bash cd musicpom virtualenv venv cd .. cd musicpom pip install -r requirements.txt ``` -run the program + +create ui.py from ui.ui +```bash +pyuic5 ui.ui -o ui.py ``` + +run +```bash python3 main.py ``` @@ -28,12 +36,13 @@ python3 main.py - [x] right-click menu - [x] editable lyrics window -- [x] batch metadata changer (red text on fields that have differing info) +- [x] batch metadata changer (red highlight fields that have differing info) - [x] playlists - [ ] delete songs from library (del key || right-click delete) - [ ] .wav, .ogg, .flac convertor - [ ] FIXME: dbaccess is instantiated for every track being reorganized - [ ] automatic "radio" based on artist or genre - [ ] search bar, full text search on song, artist, album +- [ ] when table is focused, start typing to match against the primary sort column - [ ] "installer" - put files in /opt? script to install and uninstall - [ ] .deb package? diff --git a/components/AlbumArtGraphicsView.py b/components/AlbumArtGraphicsView.py index 0fb83ca..20abe4c 100644 --- a/components/AlbumArtGraphicsView.py +++ b/components/AlbumArtGraphicsView.py @@ -1,5 +1,6 @@ import os import tempfile +from logging import debug from PyQt5.QtWidgets import ( QGraphicsPixmapItem, QGraphicsScene, @@ -56,7 +57,7 @@ class AlbumArtGraphicsView(QGraphicsView): if urls: first_url = urls[0].toLocalFile() if first_url.lower().endswith((".png", ".jpg", ".jpeg")): - print(f"dropped {first_url}") + debug(f"dropped {first_url}") self.albumArtDropped.emit( first_url ) # emit signal that album art was dropped diff --git a/components/LyricsWindow.py b/components/LyricsWindow.py index 40362c2..b20f5a9 100644 --- a/components/LyricsWindow.py +++ b/components/LyricsWindow.py @@ -8,6 +8,7 @@ from PyQt5.QtWidgets import ( from PyQt5.QtGui import QFont from components.ErrorDialog import ErrorDialog from utils import set_id3_tag +from logging import debug class LyricsWindow(QDialog): @@ -37,7 +38,7 @@ class LyricsWindow(QDialog): value=self.input_field.toPlainText(), ) if success: - print("lyrical success! yay") + debug("lyrical success! yay") else: error_dialog = ErrorDialog("Could not save lyrics :( sad") error_dialog.exec() diff --git a/components/MetadataWindow.py b/components/MetadataWindow.py index 989ea31..1bb0ce9 100644 --- a/components/MetadataWindow.py +++ b/components/MetadataWindow.py @@ -15,6 +15,7 @@ from utils.get_id3_tags import get_id3_tags from utils.set_id3_tag import set_id3_tag from utils.update_song_in_database import update_song_in_database from utils.id3_tag_mapping import id3_tag_mapping +from logging import debug # import re @@ -74,8 +75,8 @@ class MetadataWindow(QDialog): tag_sets[tag].append(song_data[tag].text[0]) except KeyError: pass - print("tag sets:") - print(tag_sets) + debug("tag sets:") + debug(tag_sets) # UI Creation current_layout = QHBoxLayout() diff --git a/components/MusicTable.py b/components/MusicTable.py index 1941eaf..3081648 100644 --- a/components/MusicTable.py +++ b/components/MusicTable.py @@ -43,7 +43,7 @@ from utils.get_id3_tags import get_id3_tags from utils.get_album_art import get_album_art from utils import set_id3_tag from subprocess import Popen -import logging +from logging import debug, error import configparser import os import shutil @@ -146,7 +146,7 @@ class MusicTable(QTableView): # in order to sort the data more effectively & have more control over UI refreshes. # Disconnect these signals to prevent unnecessary loads - logging.info("sort_table_by_multiple_columns()") + debug("sort_table_by_multiple_columns()") self.disconnect_data_changed() self.disconnect_layout_changed() sort_orders = [] @@ -168,7 +168,7 @@ class MusicTable(QTableView): # `len(config_sort_orders)` number of SELECTs for i in reversed(range(len(sort_orders))): if sort_orders[i] is not None: - logging.info(f"sorting column {i} by {sort_orders[i]}") + debug(f"sorting column {i} by {sort_orders[i]}") self.sortByColumn(i, sort_orders[i]) self.connect_data_changed() @@ -298,7 +298,7 @@ class MusicTable(QTableView): try: self.model2.removeRow(index) except Exception as e: - logging.info(f" delete_songs() failed | {e}") + debug(f" delete_songs() failed | {e}") self.connect_data_changed() def open_directory(self): @@ -343,7 +343,7 @@ class MusicTable(QTableView): else: raise RuntimeError("No USLT tags found in song metadata") except Exception as e: - logging.error(f"show_lyrics_menu() | could not retrieve lyrics | {e}") + error(f"show_lyrics_menu() | could not retrieve lyrics | {e}") lyrics = "" lyrics_window = LyricsWindow(selected_song_filepath, lyrics) lyrics_window.exec_() @@ -370,7 +370,7 @@ class MusicTable(QTableView): if e is None: return data = e.mimeData() - logging.info(f"dropEvent data: {data}") + debug(f"dropEvent data: {data}") if data and data.hasUrls(): directories = [] files = [] @@ -384,8 +384,8 @@ class MusicTable(QTableView): # append 1 file files.append(path) e.accept() - print(f"directories: {directories}") - print(f"files: {files}") + debug(f"directories: {directories}") + debug(f"files: {files}") if directories: worker = Worker(self.get_audio_files_recursively, directories) worker.signals.signal_progress.connect(self.handle_progress) @@ -444,7 +444,7 @@ class MusicTable(QTableView): def on_cell_data_changed(self, topLeft: QModelIndex, bottomRight: QModelIndex): """Handles updating ID3 tags when data changes in a cell""" - logging.info("on_cell_data_changed") + debug("on_cell_data_changed") if isinstance(self.model2, QStandardItemModel): # get the ID of the row that was edited id_index = self.model2.index(topLeft.row(), 0) # ID is column 0, always @@ -458,7 +458,7 @@ class MusicTable(QTableView): # update the ID3 information user_input_data = topLeft.data() edited_column_name = self.database_columns[topLeft.column()] - logging.info(f"edited column name: {edited_column_name}") + debug(f"edited column name: {edited_column_name}") response = set_id3_tag(filepath, edited_column_name, user_input_data) if response: # Update the library with new metadata @@ -518,9 +518,9 @@ class MusicTable(QTableView): "UPDATE song SET filepath = ? WHERE filepath = ?", (new_path, filepath), ) - logging.info(f"reorganize_files() | Moved: {filepath} -> {new_path}") + debug(f"reorganize_files() | Moved: {filepath} -> {new_path}") except Exception as e: - logging.warning( + error( f"reorganize_files() | Error moving file: {filepath} | {e}" ) # Draw the rest of the owl @@ -539,7 +539,7 @@ class MusicTable(QTableView): - Drag & Drop song(s) on tableView - File > Open > List of song(s) """ - logging.info(f"add files, files: {files}") + debug(f"add files, files: {files}") worker = Worker(add_files_to_library, files) worker.signals.signal_progress.connect(self.qapp.handle_progress) worker.signals.signal_finished.connect(self.load_music_table) @@ -547,7 +547,7 @@ class MusicTable(QTableView): threadpool = self.qapp.threadpool threadpool.start(worker) else: - logging.warning("Application window could not be found") + error("Application window could not be found") def load_music_table(self, *playlist_id): """ @@ -566,7 +566,7 @@ class MusicTable(QTableView): # Fetch playlist data selected_playlist_id = playlist_id[0] try: - logging.info( + debug( f"load_music_table() | selected_playlist_id: {selected_playlist_id}" ) with DBA.DBAccess() as db: @@ -575,7 +575,7 @@ class MusicTable(QTableView): (selected_playlist_id,), ) except Exception as e: - logging.warning(f"load_music_table() | Unhandled exception: {e}") + error(f"load_music_table() | Unhandled exception: {e}") return else: # Load the library # Fetch playlist data @@ -586,7 +586,7 @@ class MusicTable(QTableView): (), ) except Exception as e: - logging.warning(f"load_music_table() | Unhandled exception: {e}") + error(f"load_music_table() | Unhandled exception: {e}") return # Populate the model for row_data in data: @@ -612,7 +612,7 @@ class MusicTable(QTableView): def restore_scroll_position(self) -> None: """Restores the scroll position""" - logging.info("restore_scroll_position") + debug("restore_scroll_position") # QTimer.singleShot( # 100, # lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position), diff --git a/components/PlaylistsPane.py b/components/PlaylistsPane.py index 374f826..f7b1631 100644 --- a/components/PlaylistsPane.py +++ b/components/PlaylistsPane.py @@ -1,6 +1,7 @@ from PyQt5.QtWidgets import QListWidget, QTreeWidget, QTreeWidgetItem from PyQt5.QtCore import pyqtSignal import DBA +from logging import debug class PlaylistWidgetItem(QTreeWidgetItem): @@ -36,7 +37,7 @@ class PlaylistsPane(QTreeWidget): def playlist_clicked(self, item): if isinstance(item, PlaylistWidgetItem): - print(f"ID: {item.id}, name: {item.text(0)}") + debug(f"ID: {item.id}, name: {item.text(0)}") self.playlist_db_id_choice = item.id self.playlistChoiceSignal.emit(int(item.id)) elif item.text(0).lower() == "all songs": diff --git a/create_ui.sh b/create_ui.sh new file mode 100755 index 0000000..d5a89f3 --- /dev/null +++ b/create_ui.sh @@ -0,0 +1 @@ +pyuic5 ui.ui -o ui.py diff --git a/main.py b/main.py index 412e346..d0eff83 100644 --- a/main.py +++ b/main.py @@ -10,6 +10,7 @@ from mutagen.id3._frames import APIC from configparser import ConfigParser import traceback import DBA +from logging import debug from ui import Ui_MainWindow from PyQt5.QtWidgets import ( QFileDialog, @@ -290,7 +291,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): """Start playback of `tableView.current_song_filepath` & moves playback slider""" # get metadata self.current_song_metadata = self.tableView.get_current_song_metadata() - print(f'current_song_metadata: {self.current_song_metadata}') # read the file url = QUrl.fromLocalFile(self.tableView.get_current_song_filepath()) # load the audio content @@ -369,7 +369,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): # delete APIC data try: audio = ID3(file) - print(audio) + debug(audio) if "APIC:" in audio: del audio["APIC:"] logging.info("Deleting album art") @@ -426,12 +426,14 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): try: self.current_volume = self.volumeSlider.value() self.player.setVolume(self.current_volume) + self.volumeLabel = str(self.current_volume) except Exception as e: logging.error(f"main.py volume_changed() | Changing volume error: {e}") def speed_changed(self, rate: int) -> None: """Handles playback speed changes""" self.player.setPlaybackRate(rate / 50) + self.speedLabel.setText(str(round(rate/50, 2))) def on_play_clicked(self) -> None: """Updates the Play & Pause buttons when clicked""" diff --git a/show_id3_tags.py b/show_id3_tags.py index c2f4983..54580e8 100644 --- a/show_id3_tags.py +++ b/show_id3_tags.py @@ -1,34 +1,37 @@ import sys -from mutagen.id3 import ID3, APIC +from mutagen.id3 import ID3 +# i just added the below line - APIC was originally in the above +# but pyright yelled at me... +from mutagen.id3._frames import APIC import os -import logging +from logging import debug def print_id3_tags(file_path): - """Prints all ID3 tags for a given audio file.""" + """Prints all ID3 tags for a given audio file to debug out.""" try: audio = ID3(file_path) except Exception as e: - print(f"Error reading ID3 tags: {e}") + debug(f"Error reading ID3 tags: {e}") return for tag in audio.keys(): # Special handling for APIC frames (attached pictures) if isinstance(audio[tag], APIC): - print( + debug( f"{tag}: Picture, MIME type: {audio[tag].mime}, Description: {audio[tag].desc}" ) else: - print(f"{tag}: {audio[tag].pprint()}") + debug(f"{tag}: {audio[tag].pprint()}") def main(): if len(sys.argv) != 2: - print("Usage: python show_id3_tags.py /path/to/audio/file") + debug("Usage: python show_id3_tags.py /path/to/audio/file") sys.exit(1) file_path = sys.argv[1] if not os.path.exists(file_path): - print("File does not exist.") + debug("File does not exist.") sys.exit(1) print_id3_tags(file_path) diff --git a/ui.py b/ui.py index 5b47b14..f4ad8d4 100644 --- a/ui.py +++ b/ui.py @@ -81,15 +81,51 @@ class Ui_MainWindow(object): self.playbackSlider.setOrientation(QtCore.Qt.Horizontal) self.playbackSlider.setObjectName("playbackSlider") self.horizontalLayout.addWidget(self.playbackSlider) + self.timeHorizontalLayout2 = QtWidgets.QHBoxLayout() + self.timeHorizontalLayout2.setObjectName("timeHorizontalLayout2") self.startTimeLabel = QtWidgets.QLabel(self.centralwidget) + font = QtGui.QFont() + font.setFamily("Monospace") + font.setItalic(False) + self.startTimeLabel.setFont(font) self.startTimeLabel.setObjectName("startTimeLabel") - self.horizontalLayout.addWidget(self.startTimeLabel) + self.timeHorizontalLayout2.addWidget(self.startTimeLabel) self.slashLabel = QtWidgets.QLabel(self.centralwidget) + font = QtGui.QFont() + font.setFamily("Monospace") + font.setItalic(False) + self.slashLabel.setFont(font) self.slashLabel.setObjectName("slashLabel") - self.horizontalLayout.addWidget(self.slashLabel) + self.timeHorizontalLayout2.addWidget(self.slashLabel) self.endTimeLabel = QtWidgets.QLabel(self.centralwidget) + font = QtGui.QFont() + font.setFamily("Monospace") + font.setBold(True) + font.setItalic(False) + font.setWeight(75) + self.endTimeLabel.setFont(font) self.endTimeLabel.setObjectName("endTimeLabel") - self.horizontalLayout.addWidget(self.endTimeLabel) + self.timeHorizontalLayout2.addWidget(self.endTimeLabel) + self.horizontalLayout.addLayout(self.timeHorizontalLayout2) + self.speedSlider = QtWidgets.QSlider(self.centralwidget) + self.speedSlider.setMinimum(0) + self.speedSlider.setMaximum(100) + self.speedSlider.setSingleStep(1) + self.speedSlider.setProperty("value", 50) + self.speedSlider.setOrientation(QtCore.Qt.Horizontal) + self.speedSlider.setInvertedAppearance(False) + self.speedSlider.setTickPosition(QtWidgets.QSlider.TicksAbove) + self.speedSlider.setObjectName("speedSlider") + self.horizontalLayout.addWidget(self.speedSlider) + self.speedLabel = QtWidgets.QLabel(self.centralwidget) + font = QtGui.QFont() + font.setFamily("Monospace") + self.speedLabel.setFont(font) + self.speedLabel.setObjectName("speedLabel") + self.horizontalLayout.addWidget(self.speedLabel) + self.horizontalLayout.setStretch(0, 4) + self.horizontalLayout.setStretch(2, 4) + self.horizontalLayout.setStretch(3, 1) self.vLayoutPlaybackVisuals.addLayout(self.horizontalLayout) self.PlotWidget = PlotWidget(self.centralwidget) self.PlotWidget.setObjectName("PlotWidget") @@ -127,6 +163,8 @@ class Ui_MainWindow(object): self.hLayoutControls = QtWidgets.QHBoxLayout() self.hLayoutControls.setSpacing(6) self.hLayoutControls.setObjectName("hLayoutControls") + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.hLayoutControls.addItem(spacerItem) self.previousButton = QtWidgets.QPushButton(self.centralwidget) font = QtGui.QFont() font.setPointSize(28) @@ -145,6 +183,13 @@ class Ui_MainWindow(object): self.nextButton.setFont(font) self.nextButton.setObjectName("nextButton") self.hLayoutControls.addWidget(self.nextButton) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.hLayoutControls.addItem(spacerItem1) + self.hLayoutControls.setStretch(0, 1) + self.hLayoutControls.setStretch(1, 2) + self.hLayoutControls.setStretch(2, 2) + self.hLayoutControls.setStretch(3, 2) + self.hLayoutControls.setStretch(4, 1) self.verticalLayout_3.addLayout(self.hLayoutControls) self.hLayoutControls2 = QtWidgets.QHBoxLayout() self.hLayoutControls2.setSpacing(6) @@ -153,22 +198,21 @@ class Ui_MainWindow(object): self.volumeSlider.setMaximum(100) self.volumeSlider.setProperty("value", 50) self.volumeSlider.setOrientation(QtCore.Qt.Horizontal) + self.volumeSlider.setTickPosition(QtWidgets.QSlider.TicksAbove) self.volumeSlider.setObjectName("volumeSlider") self.hLayoutControls2.addWidget(self.volumeSlider) + self.volumeLabel = QtWidgets.QLabel(self.centralwidget) + self.volumeLabel.setObjectName("volumeLabel") + self.hLayoutControls2.addWidget(self.volumeLabel) + spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.hLayoutControls2.addItem(spacerItem2) + self.pushButton = QtWidgets.QPushButton(self.centralwidget) + self.pushButton.setObjectName("pushButton") + self.hLayoutControls2.addWidget(self.pushButton) + self.hLayoutControls2.setStretch(0, 1) + self.hLayoutControls2.setStretch(2, 4) + self.hLayoutControls2.setStretch(3, 1) self.verticalLayout_3.addLayout(self.hLayoutControls2) - self.hLayoutControls3 = QtWidgets.QHBoxLayout() - self.hLayoutControls3.setObjectName("hLayoutControls3") - self.speedSlider = QtWidgets.QSlider(self.centralwidget) - self.speedSlider.setMinimum(0) - self.speedSlider.setMaximum(100) - self.speedSlider.setSingleStep(1) - self.speedSlider.setProperty("value", 50) - self.speedSlider.setOrientation(QtCore.Qt.Horizontal) - self.speedSlider.setInvertedAppearance(False) - self.speedSlider.setTickPosition(QtWidgets.QSlider.NoTicks) - self.speedSlider.setObjectName("speedSlider") - self.hLayoutControls3.addWidget(self.speedSlider) - self.verticalLayout_3.addLayout(self.hLayoutControls3) self.verticalLayout_3.setStretch(0, 20) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) @@ -227,9 +271,12 @@ class Ui_MainWindow(object): self.startTimeLabel.setText(_translate("MainWindow", "00:00")) self.slashLabel.setText(_translate("MainWindow", "/")) self.endTimeLabel.setText(_translate("MainWindow", "00:00")) + self.speedLabel.setText(_translate("MainWindow", "1.00")) self.previousButton.setText(_translate("MainWindow", "⏮️")) self.playButton.setText(_translate("MainWindow", "▶️")) self.nextButton.setText(_translate("MainWindow", "⏭️")) + self.volumeLabel.setText(_translate("MainWindow", "50")) + self.pushButton.setText(_translate("MainWindow", "nothing")) self.menuFile.setTitle(_translate("MainWindow", "File")) self.menuEdit.setTitle(_translate("MainWindow", "Edit")) self.menuView.setTitle(_translate("MainWindow", "View")) diff --git a/ui.ui b/ui.ui index 9e62a72..60b0a9d 100644 --- a/ui.ui +++ b/ui.ui @@ -17,7 +17,7 @@ - + @@ -126,7 +126,7 @@ - + @@ -135,23 +135,84 @@ - - - 00:00 + + + + + + Monospace + false + + + + 00:00 + + + + + + + + Monospace + false + + + + / + + + + + + + + Monospace + 75 + false + true + + + + 00:00 + + + + + + + + + 0 + + + 100 + + + 1 + + + 50 + + + Qt::Horizontal + + + false + + + QSlider::TicksAbove - - - / + + + + Monospace + - - - - - 00:00 + 1.00 @@ -217,10 +278,23 @@ - + 6 + + + + Qt::Horizontal + + + + 40 + 20 + + + + @@ -257,10 +331,23 @@ + + + + Qt::Horizontal + + + + 40 + 20 + + + + - + 6 @@ -275,34 +362,35 @@ Qt::Horizontal + + QSlider::TicksAbove + - - - - - - - 0 - - - 100 - - - 1 - - - 50 + + + 50 + + + + Qt::Horizontal - - false + + + 40 + 20 + - - QSlider::NoTicks + + + + + + nothing diff --git a/utils/delete_and_create_library_database.py b/utils/delete_and_create_library_database.py index 52caa51..9dadb5f 100644 --- a/utils/delete_and_create_library_database.py +++ b/utils/delete_and_create_library_database.py @@ -1,4 +1,5 @@ import DBA +from logging import debug def delete_and_create_library_database(): @@ -6,6 +7,6 @@ def delete_and_create_library_database(): with open("utils/delete_and_create_library.sql", "r") as file: lines = file.read() for statement in lines.split(";"): - print(f"executing [{statement}]") + debug(f"executing [{statement}]") with DBA.DBAccess() as db: db.execute(statement, ()) diff --git a/utils/fft_analyser.py b/utils/fft_analyser.py index 3a6140c..d9881a4 100644 --- a/utils/fft_analyser.py +++ b/utils/fft_analyser.py @@ -7,6 +7,7 @@ from PyQt5 import QtCore from pydub import AudioSegment import numpy as np from scipy.ndimage.filters import gaussian_filter1d +from logging import info class FFTAnalyser(QtCore.QThread): @@ -57,7 +58,7 @@ class FFTAnalyser(QtCore.QThread): freq = np.fft.fftfreq(fourier.size, d=0.05) amps = 2 / v_sample.size * np.abs(fourier) data = np.array([freq, amps]).T - # print(data) + # info(data) point_range = 1 / self.resolution point_samples = [] diff --git a/utils/get_album_art.py b/utils/get_album_art.py index f88c473..7d99c17 100644 --- a/utils/get_album_art.py +++ b/utils/get_album_art.py @@ -1,5 +1,5 @@ from mutagen.id3 import ID3 -import logging +from logging import debug, error def get_album_art(file: str) -> bytes: @@ -18,7 +18,7 @@ def get_album_art(file: str) -> bytes: if audio.getall("APIC"): return audio.getall("APIC")[0].data except Exception as e: - print(f"Error retrieving album art: {e}") + error(f"Error retrieving album art: {e}") with open(default_image_path, "rb") as f: - logging.info("loading placeholder album art") + debug("loading placeholder album art") return f.read() diff --git a/utils/initialize_db.py b/utils/initialize_db.py index f6fccca..8a1a919 100644 --- a/utils/initialize_db.py +++ b/utils/initialize_db.py @@ -1,4 +1,5 @@ import DBA +from logging import debug def initialize_db(): @@ -6,6 +7,6 @@ def initialize_db(): with open("utils/init.sql", "r") as file: lines = file.read() for statement in lines.split(";"): - print(f"executing [{statement}]") + debug(f"executing [{statement}]") with DBA.DBAccess() as db: db.execute(statement, ())