diff --git a/.gitignore b/.gitignore index 9542e24..043bda2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ __pycache__/ # Ignore List for Files *.log test.py -venv/ \ No newline at end of file +venv/ diff --git a/README.md b/README.md index a1c8ce9..1e7b99d 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,7 @@ A feature-rich music player application with playlist management, playback contr ## Features -### TODO: -[Moved here](https://github.com/users/vorlie/projects/3/views/1) +### TODO: [Click here](https://github.com/users/vorlie/projects/3/views/1) ### Core Features @@ -67,6 +66,7 @@ A feature-rich music player application with playlist management, playback contr - **Playlists and Songs Display:** - **Playlist List:** Show available playlists and their song counts. - **Song List:** Display the list of songs in the currently loaded playlist. + - **Song information**: For displaying the currently playing song's artist(s), title, album and genre. - **Controls and Layouts:** - **Control Buttons:** Various buttons for playback control, playlist management, and external actions. @@ -90,6 +90,89 @@ A feature-rich music player application with playlist management, playback contr - **Console Logging:** Output logs to the console. - **File Logging:** Save logs to a rotating file for persistent records. +## Playlist Maker + +The `PlaylistMaker` class provides a user-friendly interface for creating and managing playlists. It allows users to select folders containing songs, add songs manually, and save playlists in JSON format. + +### Features + +- **Select Folder**: Automatically populate the playlist with songs from a selected folder. +- **Add Songs Manually**: Enter song details manually including artist, title, YouTube ID, and song path. +- **Save Playlist**: Save the created playlist in JSON format. +- **Open Existing Playlist**: Load and edit an existing playlist. +- **Edit Songs**: Double-click on a song entry to edit its details. +- **Delete Songs**: Select a song and press the Delete key to remove it. + +### How to Use + +1. **Select Folder**: + - Click on the "Select Folder" button to choose a folder containing MP3 files. + - The application will automatically process the folder and add all MP3 files matching the naming pattern to the playlist. + +2. **Add Songs Manually**: + - Fill in the "Artist", "Title", "YouTube ID" (optional), and "Song Path" fields. + - Click the "Add Song" button to add the song to the playlist. + +3. **Save Playlist**: + - Enter a name for the playlist in the "Enter playlist name" field. + - Add a playlist image if you want to. It's optional after all. + - Click the "Save Playlist" button to save the playlist as a JSON file. + +4. **Open Existing Playlist**: + - Click the "Open Existing Playlist" button to load a playlist. + - Select the JSON file of the playlist you want to open. + +5. **Edit and Delete Songs**: + - To edit a song, double-click on the corresponding cell in the table. + - To delete a song, select the row and press the Delete key. + +### Naming Scheme + +For automatic recognition, the song files in the selected folder should follow this naming scheme: +- **Artist**: The name of the artist. +- **Title**: The title of the song. +- **YouTube ID**: (Optional) The YouTube ID for the song. + ``` + Artist - Title [YouTube ID].mp3 + ``` + **Example(s)**: + ``` + Artist - Title [dQw4w9WgXcQ].mp3 + Artist (feat. Artist) - Title (Bass Boosted) [dQw4w9WgXcQ].mp3 + ``` + +### Example playlist json result + +```json +{ + "playlist_name": "default", + "playlist_large_image_key":"https://i.pinimg.com/236x/42/43/03/424303bef006eb35803ae00505248d7a.jpg", + "song_count": 2, + "songs": [ + { + "artist": "Artist 1", + "title": "Title 1", + "youtube_id": "dQw4w9WgXcQ", + "path": "C:\\Users\\USER\\Music\\FOLDER\\Artist 1 - Title 1 [dQw4w9WgXcQ].mp3", + "playlist": "default", + "album": "Album 1", + "genre": "Genre 2", + "picture_path": "C:\\Users\\USER\\Music\\FOLDER\\picture.jpg" + }, + { + "artist": "Artist 2", + "title": "Title 2", + "youtube_id": "dQw4w9WgXcQ", + "path": "C:\\Users\\USER\\Music\\FOLDER\\Artist 2 - Title 2 [dQw4w9WgXcQ].mp3", + "playlist": "default", + "album": "Album 2", + "genre": "Genre 2", + "picture_path": "C:\\Users\\USER\\Music\\FOLDER\\picture.jpg" + } + ] +} +``` + ## Installation 1. **Clone the repository:** @@ -158,6 +241,7 @@ A feature-rich music player application with playlist management, playback contr icon=['icon.ico'], ) ``` + ## Usage 1. **Run the application:** @@ -219,83 +303,6 @@ A feature-rich music player application with playlist management, playback contr - In `musicPlayer.py` there are commented logging configurations. Uncomment them to use them. - Logs are written to `combined_app.log` for application and discord logs. -## Playlist Maker - -The `PlaylistMaker` class provides a user-friendly interface for creating and managing playlists. It allows users to select folders containing songs, add songs manually, and save playlists in JSON format. - -### Features - -- **Select Folder**: Automatically populate the playlist with songs from a selected folder. -- **Add Songs Manually**: Enter song details manually including artist, title, YouTube ID, and song path. -- **Save Playlist**: Save the created playlist in JSON format. -- **Open Existing Playlist**: Load and edit an existing playlist. -- **Edit Songs**: Double-click on a song entry to edit its details. -- **Delete Songs**: Select a song and press the Delete key to remove it. - -### How to Use - -1. **Select Folder**: - - Click on the "Select Folder" button to choose a folder containing MP3 files. - - The application will automatically process the folder and add all MP3 files matching the naming pattern to the playlist. - -2. **Add Songs Manually**: - - Fill in the "Artist", "Title", "YouTube ID" (optional), and "Song Path" fields. - - Click the "Add Song" button to add the song to the playlist. - -3. **Save Playlist**: - - Enter a name for the playlist in the "Enter playlist name" field. - - Add a playlist image if you want to. It's optional after all. - - Click the "Save Playlist" button to save the playlist as a JSON file. - -4. **Open Existing Playlist**: - - Click the "Open Existing Playlist" button to load a playlist. - - Select the JSON file of the playlist you want to open. - -5. **Edit and Delete Songs**: - - To edit a song, double-click on the corresponding cell in the table. - - To delete a song, select the row and press the Delete key. - -### Naming Scheme - -For automatic recognition, the song files in the selected folder should follow this naming scheme: -- **Artist**: The name of the artist. -- **Title**: The title of the song. -- **YouTube ID**: (Optional) The YouTube ID for the song. - ``` - Artist - Title [YouTube ID].mp3 - ``` - **Example(s)**: - ``` - Artist - Title [dQw4w9WgXcQ].mp3 - Artist (feat. Artist) - Title (Bass Boosted) [dQw4w9WgXcQ].mp3 - ``` - -### Example playlist json result - -```json -{ - "playlist_name": "name_test", - "playlist_large_image_key":"https://i.pinimg.com/236x/42/43/03/424303bef006eb35803ae00505248d7a.jpg", - "song_count": 2, - "songs": [ - { - "artist": "Artist 1", - "title": "Title 1", - "youtube_id": "dQw4w9WgXcQ", - "path": "C:\\Users\\USER\\Music\\FOLDER\\Artist 1 - Title 1 [dQw4w9WgXcQ].mp3", - "playlist": "name_test" - }, - { - "artist": "Artist 2", - "title": "Title 2", - "youtube_id": "dQw4w9WgXcQ", - "path": "C:\\Users\\USER\\Music\\FOLDER\\Artist 2 - Title 2 [dQw4w9WgXcQ].mp3", - "playlist": "name_test" - } - ] -} -``` - ## Troubleshooting - **Ensure all dependencies are installed:** diff --git a/config.json b/config.json index b37f64b..cfdf721 100644 --- a/config.json +++ b/config.json @@ -18,7 +18,7 @@ "1": "Specify the directory where your playlists are saved. You can change this path if needed.", "2": "The default value is 'playlists'." }, - "root_playlist_folder": "playlists", + "root_playlist_folder": "C:\\Users\\karol\\Documents\\IotaPlayerFiles", "default_playlist_comment": "Enter the name of the default playlist that will be loaded initially. You can modify this name as required.", "default_playlist": "default", "colorization_color_comment": { diff --git a/core/discordIntegration.py b/core/discordIntegration.py index 49a3f76..b8f6810 100644 --- a/core/discordIntegration.py +++ b/core/discordIntegration.py @@ -11,7 +11,7 @@ class DiscordIntegration(QObject): def __init__(self): super().__init__() # Load config - with open('config.json', 'r') as f: + with open('config.json', 'r', encoding='utf-8') as f: self.config = json.load(f) self.client_id = self.config.get('discord_client_id', '1150680286649143356') self.large_image_key = self.config.get('large_image_key', 'default_image') diff --git a/core/musicPlayer.py b/core/musicPlayer.py index bcadc1c..cf3acd8 100644 --- a/core/musicPlayer.py +++ b/core/musicPlayer.py @@ -4,7 +4,7 @@ QFileDialog, QLabel, QSlider, QHBoxLayout, QSizePolicy, QSpacerItem, QMessageBox ) from utils import hex_to_rgba -from PyQt5.QtGui import QIcon +from PyQt5.QtGui import QIcon, QPixmap, QPainter, QPainterPath from PyQt5.QtCore import QTimer, Qt from pygame import mixer from pynput import keyboard @@ -18,19 +18,21 @@ class MusicPlayer(QMainWindow): def __init__(self, settings, icon_path, config_path, theme, normal): super().__init__() self.icon_path = icon_path + self.clr_theme = theme + self.clr_nrm = normal if os.path.exists(icon_path): self.setWindowIcon(QIcon(icon_path)) self.setWindowTitle("Iota Player • Music Player") - self.setMinimumSize(800, 600) + self.setMinimumSize(1000, 600) try: - with open('config.json', 'r') as f: + with open('config.json', 'r', encoding='utf-8') as f: self.config = json.load(f) except FileNotFoundError: self.config = settings print(settings) try: - with open('config.json', 'w') as f: + with open('config.json', 'w', encoding='utf-8') as f: json.dump(self.config, f, indent=4) except IOError as e: print(f"Error writing to config file: {e}") @@ -110,10 +112,10 @@ def initUI(self): # Main widget and layout self.central_widget = QWidget() self.setCentralWidget(self.central_widget) - self.main_layout = QVBoxLayout() # Changed to vertical layout + self.main_layout = QVBoxLayout() self.central_widget.setLayout(self.main_layout) - # Top Layout: Left Frame (Playlist) and Right Frame (Song List) + # Top Layout: Left Frame (Playlist), Middle Frame (Song List), and Right Frame (Song Info) self.top_layout = QHBoxLayout() self.main_layout.addLayout(self.top_layout) @@ -171,26 +173,104 @@ def initUI(self): self.two_button_layout.addWidget(self.loop_button) self.one_button_layout.addWidget(self.youtube_button) - # Right Frame Layout (Song List) - self.right_frame = QWidget() - self.right_frame_layout = QVBoxLayout() - self.right_frame_layout.setContentsMargins(0, 0, 0, 0) - self.right_frame_layout.setSpacing(10) - self.right_frame.setLayout(self.right_frame_layout) - self.top_layout.addWidget(self.right_frame) - self.right_frame_scnd_layout = QHBoxLayout() - self.right_frame_layout.addLayout(self.right_frame_scnd_layout) + # left frame sliders + self.sliders_layout = QVBoxLayout() + self.left_frame_layout.addLayout(self.sliders_layout) + self.time_label = QLabel("00:00 / 00:00") + self.sliders_layout.addWidget(self.time_label) + + self.progress_bar = QSlider(Qt.Horizontal) + self.progress_bar.setRange(0, 100) + self.sliders_layout.addWidget(self.progress_bar) + + self.volume_label = QLabel("Volume:") + self.sliders_layout.addWidget(self.volume_label) + + self.volume_slider = QSlider(Qt.Horizontal) + self.volume_slider.setMinimum(0) + self.volume_slider.setMaximum(100) + self.volume_slider.setValue(100) # Start with full volume + self.volume_slider.setTickPosition(QSlider.TicksBelow) + self.volume_slider.setTickInterval(10) + self.volume_slider.setSingleStep(1) + self.volume_slider.valueChanged.connect(self.adjust_volume) + self.sliders_layout.addWidget(self.volume_slider) + + # Middle Frame Layout (Song List) + self.middle_frame = QWidget() + self.middle_frame_layout = QVBoxLayout() + self.middle_frame_layout.setContentsMargins(0, 0, 0, 0) + self.middle_frame_layout.setSpacing(10) + self.middle_frame.setLayout(self.middle_frame_layout) + self.top_layout.addWidget(self.middle_frame) + + self.middle_frame_scnd_layout = QHBoxLayout() + self.middle_frame_layout.addLayout(self.middle_frame_scnd_layout) + # Song List, Settings self.song_list_label = QLabel("Song List:") self.settings_button = QPushButton("Settings") self.settings_button.setFixedWidth(70) - self.right_frame_scnd_layout.addWidget(self.song_list_label) - self.right_frame_scnd_layout.addWidget(self.settings_button) + self.middle_frame_scnd_layout.addWidget(self.song_list_label) + self.middle_frame_scnd_layout.addWidget(self.settings_button) self.song_list = QListWidget() self.song_list.currentItemChanged.connect(self.play_selected_song) - self.right_frame_layout.addWidget(self.song_list) - + self.middle_frame_layout.addWidget(self.song_list) + + # Right Frame Layout (Song Info) + self.right_frame = QWidget() + self.right_frame_layout = QVBoxLayout() + self.right_frame_layout.setContentsMargins(0,0,0,0) + self.right_frame_layout.setSpacing(10) + self.right_frame.setLayout(self.right_frame_layout) + self.right_frame.setFixedWidth(270) + + # Apply the background color and border + if self.clr_theme == "dark": + self.right_frame.setStyleSheet(f"background-color: {hex_to_rgba(self.clr_nrm, 0.1)}; border: 1px solid #3f4042; border-radius: 5px;") + else: + self.right_frame.setStyleSheet(f"background-color: {hex_to_rgba(self.clr_nrm, 0.1)}; border: 1px solid #dadce0; border-radius: 5px;") + + self.top_layout.addWidget(self.right_frame, alignment=Qt.AlignTop | Qt.AlignRight) + + # Song Info (Song picture, title, author, etc.) + self.song_picture = QLabel() + self.song_picture.setContentsMargins(0, 9, 0, 0) + self.song_picture.setPixmap(QPixmap("").scaled(250, 250, Qt.KeepAspectRatio)) + self.song_picture.setStyleSheet("border: none; background-color: none;") + + self.right_frame_layout.addWidget(self.song_picture, alignment=Qt.AlignCenter) + + # Style labels + if self.clr_theme == "dark": + label_stylesheet = "border: none; background: none; color: #FFFFFF;" + else: + label_stylesheet = "border: none; background: none; color: #000000;" + + self.song_title_label = QLabel("Title:") + self.song_title_label.setAlignment(Qt.AlignLeft) + self.song_title_label.setContentsMargins(10, 0, 0, 0) + self.song_title_label.setStyleSheet(label_stylesheet) + self.right_frame_layout.addWidget(self.song_title_label) + + self.song_author_label = QLabel("Author:") + self.song_author_label.setAlignment(Qt.AlignLeft) + self.song_author_label.setContentsMargins(10, 0, 0, 0) + self.song_author_label.setStyleSheet(label_stylesheet) + self.right_frame_layout.addWidget(self.song_author_label) + + self.song_album_label = QLabel("Album:") + self.song_album_label.setAlignment(Qt.AlignLeft) + self.song_album_label.setContentsMargins(10, 0, 0, 0) + self.song_album_label.setStyleSheet(label_stylesheet) + self.right_frame_layout.addWidget(self.song_album_label) + + self.song_genre_label = QLabel("Genre:") + self.song_genre_label.setAlignment(Qt.AlignLeft) + self.song_genre_label.setContentsMargins(10, 0, 0, 10) + self.song_genre_label.setStyleSheet(label_stylesheet) + self.right_frame_layout.addWidget(self.song_genre_label) # Bottom Layout: Music Control, Progress, Volume self.bottom_layout = QVBoxLayout() @@ -231,37 +311,6 @@ def initUI(self): # Add layout to the bottom layout or your main layout self.bottom_layout.addLayout(self.music_control_layout) - # Song Info, Progress and Volume - self.info_layout = QHBoxLayout() - self.center_sil_layout = QHBoxLayout() - - # Centered song info label - self.bottom_layout.addLayout(self.center_sil_layout) - self.song_info_label = QLabel("No song playing") - self.song_info_label.setAlignment(Qt.AlignCenter) - self.center_sil_layout.addWidget(self.song_info_label) - - self.bottom_layout.addLayout(self.info_layout) - self.time_label = QLabel("00:00 / 00:00") - self.info_layout.addWidget(self.time_label) - - self.progress_bar = QSlider(Qt.Horizontal) - self.progress_bar.setRange(0, 100) - self.info_layout.addWidget(self.progress_bar) - - self.volume_label = QLabel("Volume:") - self.info_layout.addWidget(self.volume_label) - - self.volume_slider = QSlider(Qt.Horizontal) - self.volume_slider.setMinimum(0) - self.volume_slider.setMaximum(100) - self.volume_slider.setValue(100) # Start with full volume - self.volume_slider.setTickPosition(QSlider.TicksBelow) - self.volume_slider.setTickInterval(10) - self.volume_slider.setSingleStep(1) - self.volume_slider.valueChanged.connect(self.adjust_volume) - self.info_layout.addWidget(self.volume_slider) - self.discord_status_label = QLabel("Discord Status: Disconnected") self.bottom_layout.addWidget(self.discord_status_label) @@ -279,6 +328,7 @@ def initUI(self): self.playlist_maker_button.clicked.connect(self.open_playlist_maker) self.settings_button.clicked.connect(self.open_settings) + def on_start(self): time.sleep(0.2) self.load_playlist(self.config['default_playlist']) @@ -304,7 +354,7 @@ def get_playlist_names(self): if f.endswith('.json'): playlist_path = os.path.join(playlist_folder, f) try: - with open(playlist_path, 'r') as file: + with open(playlist_path, 'r', encoding='utf-8') as file: data = json.load(file) name = data.get("playlist_name", os.path.splitext(f)[0]) playlist_image = data.get("playlist_large_image_key", None) @@ -638,10 +688,10 @@ def highlight_current_song(self): def update_song_info(self): if self.current_song: self.song_info_var = f"{self.current_song['artist']} - {self.current_song['title']}" - self.song_info_label.setText(self.song_info_var) + self.update_right_frame_info() # Update the right frame with the song info else: self.song_info_var = "Nothing is playing" - self.song_info_label.setText(self.song_info_var) + self.clear_right_frame_info() # Clear the right frame info # Temporarily disconnect the signal while updating the selection self.song_list.currentItemChanged.disconnect(self.play_selected_song) @@ -706,6 +756,32 @@ def update_song_info(self): None # Duration ) + def update_right_frame_info(self): + """Update the right frame with the current song's information.""" + if self.current_song: + # Update the song picture + picture_path = self.current_song.get('picture_path', 'default.png') + if os.path.exists(picture_path): + self.song_picture.setPixmap(QPixmap(picture_path).scaled(250, 250, Qt.KeepAspectRatio)) + else: + self.song_picture.setPixmap(QPixmap("default.png").scaled(250, 250, Qt.KeepAspectRatio)) + + # Update the labels with song information + self.song_title_label.setText("Title: " + self.current_song.get('title', 'Unknown Title')) + self.song_author_label.setText("Author: " + self.current_song.get('artist', 'Unknown Artist')) + self.song_album_label.setText("Album: " + self.current_song.get('album', 'Unknown Album')) + self.song_genre_label.setText("Genre: " + self.current_song.get('genre', 'Unknown Genre')) + else: + self.clear_right_frame_info() + + def clear_right_frame_info(self): + """Clear the information displayed in the right frame.""" + self.song_picture.setPixmap(QPixmap("default.png").scaled(250, 250, Qt.KeepAspectRatio)) + self.song_title_label.setText("Title:") + self.song_author_label.setText("Author:") + self.song_album_label.setText("Album:") + self.song_genre_label.setText("Genre:") + def update_progress(self): if mixer.music.get_busy(): elapsed_time = mixer.music.get_pos() // 1000 diff --git a/core/playlistMaker.py b/core/playlistMaker.py index e39e73b..aa29fd8 100644 --- a/core/playlistMaker.py +++ b/core/playlistMaker.py @@ -8,9 +8,9 @@ def __init__(self): self.playlists = {} self.shuffle_states = {} self.shuffled_songs = {} - """Initialize PlaylistManager with the directory where playlists are stored.""" + try: - with open('./config.json', 'r') as f: + with open('./config.json', 'r', encoding='utf-8') as f: config = json.load(f) except FileNotFoundError: print("Configuration file not found. Using default settings.") @@ -20,7 +20,6 @@ def __init__(self): config = {"root_playlist_folder": "playlists"} self.playlists_dir = config.get('root_playlist_folder', 'playlists') - # Ensure the playlists directory exists if not os.path.exists(self.playlists_dir): os.makedirs(self.playlists_dir) print(f"Created playlists directory: {self.playlists_dir}") @@ -29,25 +28,21 @@ def load_playlist(self, playlist_name): """Load a playlist by name from the playlists directory.""" playlist_path = os.path.join(self.playlists_dir, f"{playlist_name}.json") - # Check if the playlist file exists if not os.path.isfile(playlist_path): raise FileNotFoundError(f"Playlist file not found: {playlist_path}") - # Load the playlist data try: - with open(playlist_path, 'r') as file: + with open(playlist_path, 'r', encoding='utf-8') as file: data = json.load(file) except json.JSONDecodeError: raise ValueError(f"Error decoding JSON in playlist file: {playlist_path}") except IOError as e: raise IOError(f"Error reading playlist file: {playlist_path}") from e - # Extract playlist name and songs - playlist_name = data.get("playlist_name", playlist_name) # Fallback to provided playlist_name + playlist_name = data.get("playlist_name", playlist_name) playlist_image = data.get("playlist_large_image_key", None) songs = data.get("songs", []) - # Store the playlist and reset shuffle states self.playlists[playlist_name] = songs self.shuffle_states[playlist_name] = False self.shuffled_songs[playlist_name] = [] @@ -70,16 +65,14 @@ def __init__(self, icon_path): self.setWindowTitle("Iota Player • Playlist Maker") if os.path.exists(icon_path): self.setWindowIcon(QIcon(icon_path)) - self.setGeometry(100, 100, 1200, 800) + self.setGeometry(100, 100, 1700, 800) with open('config.json', 'r') as f: self.config = json.load(f) - self.initUI() def initUI(self): - # Main layout self.layout = QHBoxLayout() self.setLayout(self.layout) @@ -92,7 +85,6 @@ def initUI(self): self.left_frame.setFixedWidth(300) self.layout.addWidget(self.left_frame, alignment=Qt.AlignTop | Qt.AlignLeft) - # Folder selection self.folder_label = QLabel("Select folder containing songs:") self.left_layout.addWidget(self.folder_label) @@ -104,7 +96,6 @@ def initUI(self): self.select_folder_button.clicked.connect(self.select_folder) self.left_layout.addWidget(self.select_folder_button) - # Playlist name input self.playlist_name_label = QLabel("Enter playlist name:") self.left_layout.addWidget(self.playlist_name_label) @@ -112,7 +103,6 @@ def initUI(self): self.playlist_name_input.setPlaceholderText("Playlist Name") self.left_layout.addWidget(self.playlist_name_input) - # Discord image key input self.discord_large_image_key = QLabel("Set a playlist image (Discord presence):") self.left_layout.addWidget(self.discord_large_image_key) @@ -120,7 +110,6 @@ def initUI(self): self.discord_large_image_key_input.setPlaceholderText("Use an image URL or an image key") self.left_layout.addWidget(self.discord_large_image_key_input) - # Add songs manually self.add_songs_label = QLabel("Add songs manually:") self.left_layout.addWidget(self.add_songs_label) @@ -132,6 +121,18 @@ def initUI(self): self.title_input.setPlaceholderText("Title") self.left_layout.addWidget(self.title_input) + self.album_input = QLineEdit() + self.album_input.setPlaceholderText("Album") + self.left_layout.addWidget(self.album_input) + + self.genre_input = QLineEdit() + self.genre_input.setPlaceholderText("Genre") + self.left_layout.addWidget(self.genre_input) + + self.picture_path_input = QLineEdit() + self.picture_path_input.setPlaceholderText("Picture Path") + self.left_layout.addWidget(self.picture_path_input) + self.youtube_id_input = QLineEdit() self.youtube_id_input.setPlaceholderText("YouTube ID") self.left_layout.addWidget(self.youtube_id_input) @@ -140,7 +141,6 @@ def initUI(self): self.path_input.setPlaceholderText("Song Path") self.left_layout.addWidget(self.path_input) - # Buttons for adding song and saving playlist self.button_layout = QHBoxLayout() self.left_layout.addLayout(self.button_layout) @@ -161,7 +161,7 @@ def initUI(self): Notes: