From d131100876428879a0aed18709b2018252846c15 Mon Sep 17 00:00:00 2001 From: tsi-billypom Date: Wed, 4 Sep 2024 15:01:46 -0400 Subject: [PATCH] export playlists better --- bak.main2 | 668 ----------------------------- components/ExportPlaylistWindow.py | 112 +++-- main.py | 2 - sample_config.ini | 3 +- utils/__init__.py | 1 + utils/get_reorganize_vars.py | 27 ++ 6 files changed, 116 insertions(+), 697 deletions(-) delete mode 100644 bak.main2 create mode 100644 utils/get_reorganize_vars.py diff --git a/bak.main2 b/bak.main2 deleted file mode 100644 index 1cfdc23..0000000 --- a/bak.main2 +++ /dev/null @@ -1,668 +0,0 @@ -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, - QInputDialog, - 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, - CreatePlaylistWindow, -) - - -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.8, padding=0) # y axis range - # Remove axis labels and decorations - # self.PlotWidget.setLogMode(False, False) - self.PlotWidget.getAxis("bottom").setTicks([]) # Remove x-axis ticks - self.PlotWidget.getAxis("bottom").setLabel("") # Remove x-axis label - 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 - self.actionNewPlaylist.triggered.connect(self.create_playlist) - # 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.actionNewPlaylist = QtWidgets.QAction(MainWindow) - self.actionNewPlaylist.setObjectName("actionNewPlaylist") - self.actionDeleteDatabase = QtWidgets.QAction(MainWindow) - self.actionDeleteDatabase.setObjectName("actionDeleteDatabase") - self.menuFile.addAction(self.actionOpenFiles) - self.menuFile.addAction(self.actionNewPlaylist) - 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.actionNewPlaylist.setText(_translate("MainWindow", "New playlist")) - 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: - if self.playbackSlider.isSliderDown(): - # Prevents slider from updating when dragging - return - # 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("main.py open_files() | file names chosen") - print(filenames) - self.tableView.add_files(filenames) - - def create_playlist(self) -> None: - """Creates a database record for a playlist, given a name""" - create_playlist_window = CreatePlaylistWindow() - create_playlist_window.exec_() - - 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.load_music_table() - - 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.load_music_table() - - 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.load_music_table() - - 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.load_music_table() - - 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_()) diff --git a/components/ExportPlaylistWindow.py b/components/ExportPlaylistWindow.py index 3c90e2c..592f31d 100644 --- a/components/ExportPlaylistWindow.py +++ b/components/ExportPlaylistWindow.py @@ -1,5 +1,6 @@ import logging from PyQt5.QtWidgets import ( + QCheckBox, QDialog, QHBoxLayout, QLineEdit, @@ -11,7 +12,10 @@ from PyQt5.QtWidgets import ( ) from PyQt5.QtGui import QFont from configparser import ConfigParser +from utils import get_id3_tags, get_reorganize_vars +from components import ErrorDialog import DBA +import os class ExportPlaylistWindow(QDialog): @@ -20,69 +24,125 @@ class ExportPlaylistWindow(QDialog): self.setWindowTitle("Export playlist") config = ConfigParser() config.read("config.ini") + self.relative_path = config.get("directories", "playlist_relative_path") self.export_path = config.get("directories", "playlist_export_path") + self.selected_playlist_name = "my-playlist.m3u" + self.current_m3u_path = self.export_path + self.current_relative_path = self.relative_path layout = QVBoxLayout() - # button_layout = QHBoxLayout() + # Header label + label = QLabel("Playlists") + label.setFont(QFont("Sans", weight=QFont.Bold)) + layout.addWidget(label) + + # Get playlists from db playlist_dict = {} with DBA.DBAccess() as db: data = db.query("SELECT id, name from playlist;", ()) for row in data: playlist_dict[row[0]] = row[1] + # Playlist list widget self.item_dict = {} - self.listWidget = QListWidget(self) + self.playlist_listWidget = QListWidget(self) for i, (k, v) in enumerate(playlist_dict.items()): - item_text = f"{i} | {v}" + # item_text = f"{i} | {v}" + item_text = v item = QListWidgetItem(item_text) - self.listWidget.addItem(item) + self.playlist_listWidget.addItem(item) self.item_dict[item_text] = k - # add ui elements to window - label = QLabel("Playlists") - label.setFont(QFont("Sans", weight=QFont.Bold)) - layout.addWidget(label) - layout.addWidget(self.listWidget) + layout.addWidget(self.playlist_listWidget) - # Export path label - label = QLabel("Export path") + # Relative path checkbox widget + self.checkbox = QCheckBox(text="Use relative paths?") + self.checkbox.setChecked(True) + layout.addWidget(self.checkbox) + + # Relative export path label + label = QLabel("Relative path") label.setFont(QFont("Sans", weight=QFont.Bold)) # bold category label.setStyleSheet("text-transform:lowercase;") # uppercase category + layout.addWidget(label) - # Export path - self.input = QLineEdit(self.export_path) - layout.addWidget(self.input) + # Relative export path line edit widget + self.relative_path_input = QLineEdit(self.relative_path) + layout.addWidget(self.relative_path_input) + + # Playlist file save path label + label = QLabel("Playlist file path") + label.setFont(QFont("Sans", weight=QFont.Bold)) + label.setStyleSheet("text-transform:lowercase;") + layout.addWidget(label) + + # Playlist file save path line edit widget + self.m3u_path_input = QLineEdit(self.export_path) + layout.addWidget(self.m3u_path_input) # Save button - save_button = QPushButton("Export") - save_button.clicked.connect(self.save) - layout.addWidget(save_button) + self.save_button = QPushButton("Export") + layout.addWidget(self.save_button) + + # Signals + self.save_button.clicked.connect(self.save) + self.checkbox.toggled.connect(self.relative_path_input.setEnabled) + self.playlist_listWidget.currentItemChanged.connect( + self.set_current_selected_playlist + ) self.setLayout(layout) self.show() + def set_current_selected_playlist(self) -> None: + self.selected_playlist_name = ( + self.playlist_listWidget.selectedItems()[0].text() + ".m3u" + ) + print(self.selected_playlist_name) + def save(self) -> None: """Exports the chosen database playlist to a .m3u file""" - selected_items = [item.text() for item in self.listWidget.selectedItems()] + selected_items = [ + item.text() for item in self.playlist_listWidget.selectedItems() + ] selected_db_ids = [self.item_dict[item] for item in selected_items] - value = self.input.text() - if value == "" or value is None: + relative_path = self.relative_path_input.text() + m3u_path = self.m3u_path_input.text() + if m3u_path == "" or m3u_path is None: self.close() return for playlist in selected_db_ids: - print(type(playlist)) - print(playlist) try: with DBA.DBAccess() as db: - selected_song_paths = db.query( + db_paths = db.query( "SELECT s.filepath FROM song_playlist as sp JOIN song as s ON s.id = sp.song_id WHERE sp.playlist_id = ?;", (playlist,), - ) + )[0] except Exception as e: logging.error( f"ExportPlaylistWindow.py save() | could not retrieve playlist songs: {e}" ) - # FIXME: make this write to a .m3u file, also - # need to consider relative paths + config for that + error_dialog = ErrorDialog( + "Could not get songs from this playlist. noooooo" + ) + error_dialog.exec() + self.close() + # Gather playlist song paths + write_paths = [] + if self.checkbox.isChecked: + # Relative paths + for song in db_paths: + artist, album = get_reorganize_vars(song) + write_path = os.path.join( + relative_path, artist, album, song.split("/")[-1] + ) + write_paths.append(write_path) + else: + # Normal paths + for song in db_paths: + write_paths.append(song) + # Write playlist file + for line in write_paths: + pass self.close() def cancel(self) -> None: diff --git a/main.py b/main.py index 8932075..f39a31f 100644 --- a/main.py +++ b/main.py @@ -438,7 +438,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): if __name__ == "__main__": - print("its main") # First run initialization if not os.path.exists("config.ini"): # Create config file from sample @@ -449,7 +448,6 @@ if __name__ == "__main__": db_path = db_name.split("/") db_path.pop() db_path_as_string = "/".join(db_path) - print(f"db_path_as_string: {db_path_as_string}") if not os.path.exists(db_path_as_string): os.makedirs(db_path_as_string) # Create database on first run diff --git a/sample_config.ini b/sample_config.ini index 7ae1601..4385409 100644 --- a/sample_config.ini +++ b/sample_config.ini @@ -6,7 +6,8 @@ database = db/library.db # Useful paths to have stored library = /path/to/music reorganize_destination = /where/to/reorganize/to -playlist_export_path = /where/to/export/to +playlist_export_path = /where/to/export/playlist/file/to +playlist_relative_path = /relative/suffix/for/playlist/items [settings] # Which file types are scanned diff --git a/utils/__init__.py b/utils/__init__.py index af84b8f..b676f56 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -4,6 +4,7 @@ from .initialize_db import initialize_db from .safe_get import safe_get from .get_album_art import get_album_art from .get_id3_tags import get_id3_tags +from .get_reorganize_vars import get_reorganize_vars from .set_id3_tag import set_id3_tag from .delete_song_id_from_database import delete_song_id_from_database from .delete_and_create_library_database import delete_and_create_library_database diff --git a/utils/get_reorganize_vars.py b/utils/get_reorganize_vars.py new file mode 100644 index 0000000..844c97c --- /dev/null +++ b/utils/get_reorganize_vars.py @@ -0,0 +1,27 @@ +from mutagen.id3 import ID3 + + +def get_reorganize_vars(filepath: str) -> tuple[str, str]: + """ + Takes in a path to an audio file + returns the (artisr, album) as a tuple of strings + + if no artist or album or ID3 tags at all are found, + function will return ("Unknown Artist", "Unknown Album") + """ + audio = ID3(filepath) + try: + artist = str(audio["TPE1"].text[0]) + if artist == "": + artist = "Unknown Artist" + except KeyError: + artist = "Unknown Artist" + + try: + album = str(audio["TALB"].text[0]) + if album == "": + album = "Unknown Album" + except KeyError: + album = "Unknown Album" + + return artist, album