package sbu.javafx; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.media.Media; import javafx.scene.media.MediaPlayer; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.util.Duration; import org.jaudiotagger.audio.AudioFile; import org.jaudiotagger.audio.AudioFileIO; import org.jaudiotagger.audio.AudioHeader; import org.jaudiotagger.tag.FieldKey; import org.jaudiotagger.tag.Tag; import org.jaudiotagger.tag.images.Artwork; import java.io.File; import java.io.FileOutputStream; import java.util.*; public class MusicController { // ========================================= fxml Fields ============================================ // player panel @FXML private ImageView albumCover; @FXML private Label songTitle, artistName; @FXML private Label currentTime, totalTime; @FXML private Button playPauseBtn, shuffleBtn, repeatBtn; @FXML private Button libraryTabBtn, playlistTabBtn; @FXML private Slider progressSlider, volumeSlider; // library and playlists panel @FXML private VBox libraryPane, playlistsPane, playlistsListPane, playlistDetailPane; @FXML private ListView libraryView; @FXML private ListView playlistsView; @FXML private ListView playlistDetailView; @FXML private Label libraryCount, playlistDetailName; // my fields private MediaPlayer mediaPlayer; private ObservableList library = FXCollections.observableArrayList(); private HashMap> playlists = new HashMap<>(); private List playQueue = new ArrayList<>(); private int currentIndex = -1; private boolean isPlaying = false; private boolean isShuffle = false; private boolean isRepeat = false; // ========================================= Methods ============================================ public void initialize(){ // runs without calling libraryView.setItems(library); // volume volumeSlider.valueProperty().addListener((obs, oldVal, newVal) -> { if(mediaPlayer != null){ mediaPlayer.setVolume(newVal.doubleValue()); } }); // progress slider - seek functionality progressSlider.setOnMouseReleased(e -> { if(mediaPlayer != null){ mediaPlayer.seek(Duration.seconds(progressSlider.getValue())); } }); progressSlider.setMax(100); // library - double click to play libraryView.setOnMouseClicked(e -> { if(e.getClickCount() == 2){ // double click Song selectedSong = libraryView.getSelectionModel().getSelectedItem(); if(selectedSong != null){ playQueue = new ArrayList<>(library); currentIndex = playQueue.indexOf(selectedSong); playSong(selectedSong); } } }); // playlist detail - double click to play playlistDetailView.setOnMouseClicked(e -> { if(e.getClickCount() == 2){ // double click Song selectedSong = playlistDetailView.getSelectionModel().getSelectedItem(); if(selectedSong != null){ playQueue = new ArrayList<>(playlistDetailView.getItems()); currentIndex = playQueue.indexOf(selectedSong); playSong(selectedSong); } } }); // playlists - double click to open playlistsView.setOnMouseClicked(e -> { if(e.getClickCount() == 2){ // double click String selectedPlaylist = playlistsView.getSelectionModel().getSelectedItem(); if(selectedPlaylist != null){ openPlayList(selectedPlaylist); } } }); setupLibraryItems(); } // ========================================= Playback ============================================ private void playSong(Song song) { if(mediaPlayer != null){ mediaPlayer.stop(); mediaPlayer.dispose(); // file not in use } File file = new File(song.getFilePath()); if (!file.exists()) { System.out.println("FILE NOT FOUND!"); return; } Media media = new Media(file.toURI().toString()); mediaPlayer = new MediaPlayer(media); mediaPlayer.setVolume(volumeSlider.getValue()); // UI changes songTitle.setText(song.getTitle()); artistName.setText(song.getArtist()); loadCover(song); // progress bar mediaPlayer.currentTimeProperty().addListener((obs, oldVal, newVal) -> { if(!progressSlider.isValueChanging()){ progressSlider.setValue(newVal.toSeconds()); currentTime.setText(formatTime(newVal.toSeconds())); } }); mediaPlayer.setOnReady(() -> { double total= mediaPlayer.getTotalDuration().toSeconds(); progressSlider.setMax(total); totalTime.setText(formatTime(total)); }); mediaPlayer.setOnEndOfMedia(() -> { if(isRepeat){ mediaPlayer.seek(Duration.ZERO); mediaPlayer.play(); } else{ handleNext(); } }); mediaPlayer.play(); isPlaying = true; playPauseBtn.setText("⏸"); libraryView.refresh(); } private void loadCover(Song song){ try{ if(song.getCoverPath() != null && !song.getCoverPath().isEmpty()){ albumCover.setImage(new Image( new File(song.getCoverPath()).toURI().toString() )); } else{ albumCover.setImage(new Image( Objects.requireNonNull(getClass().getResourceAsStream("/sbu/images/default-cover.png")) )); } } catch(Exception e){ albumCover.setImage(null); } } private String formatTime(double seconds) { int minute = (int) seconds / 60; int second = (int) seconds % 60; return String.format("%d:%02d", minute, second); } private Song createSongFromFile(File file){ String filePath = file.getAbsolutePath(); String title = file.getName(); String artist = "Unknown"; String album = "Unknown"; String coverPath = ""; int duration = 0; try{ AudioFile audioFile = AudioFileIO.read(file); Tag tag = audioFile.getTag(); AudioHeader header = audioFile.getAudioHeader(); if(tag != null){ // title String t = tag.getFirst(FieldKey.TITLE); if(t != null && !t.isBlank()) title = t; // artist String a = tag.getFirst(FieldKey.ARTIST); if(a != null && !a.isBlank()) artist = a; // album String al = tag.getFirst(FieldKey.ALBUM); if(al != null && !al.isBlank()) album = al; // cover List artworks = tag.getArtworkList(); if(artworks != null && !artworks.isEmpty()){ byte[] imageData = artworks.get(0).getBinaryData(); File coverFile = new File( System.getProperty("java.io.tmpdir"), title.replaceAll("\\s+", "_") + "_cover.jpg" ); try(FileOutputStream fos = new FileOutputStream(coverFile)){ fos.write(imageData); coverPath = coverFile.getAbsolutePath(); } } } if(header != null) duration = header.getTrackLength(); } catch(Exception e){ System.out.println("Cannot read metadata, " + e.getMessage()); } return new Song(title, artist, album, filePath, coverPath, duration); } // ========================================= Control buttons ============================================ @FXML public void handlePlayPause() { if(mediaPlayer == null) return; if(isPlaying){ mediaPlayer.pause(); playPauseBtn.setText("▶"); } else{ mediaPlayer.play(); playPauseBtn.setText("⏸"); } isPlaying = !isPlaying; } @FXML public void handleNext() { if(playQueue.isEmpty()) return; if(isShuffle){ currentIndex = (int) (Math.random() * playQueue.size()); } else{ currentIndex = (currentIndex + 1) % playQueue.size(); // when we reach end of the queue it goes to the first one } playSong(playQueue.get(currentIndex)); } @FXML public void handlePrev() { if(playQueue.isEmpty()) return; currentIndex = (currentIndex - 1 + playQueue.size()) % playQueue.size(); playSong(playQueue.get(currentIndex)); } @FXML public void handleShuffle() { isShuffle = !isShuffle; if(isShuffle) shuffleBtn.getStyleClass().add("active-btn"); else shuffleBtn.getStyleClass().remove("active-btn"); } @FXML public void handleRepeat() { isRepeat = !isRepeat; if (isRepeat) repeatBtn.getStyleClass().add("active-btn"); else repeatBtn.getStyleClass().remove("active-btn"); } // ========================================= Library ============================================ @FXML public void handleAddSong() { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Add Songs"); fileChooser.getExtensionFilters().add( new FileChooser.ExtensionFilter("Audio Files", "*.mp3", "*.wav", "*.aac", "*.flac") ); List files = fileChooser.showOpenMultipleDialog(null); if(files == null) return; for(File file : files){ Song song = createSongFromFile(file); library.add(song); } libraryCount.setText(library.size() + " Songs"); } private void setupLibraryItems() { libraryView.setCellFactory(lv -> new ListCell<>() { private final ImageView cover = new ImageView(); private final Label index = new Label(); private final Label title = new Label(); private final Label artist = new Label(); private final Label duration = new Label(); private final VBox info = new VBox(2, title, artist); private final javafx.scene.layout.HBox root = new javafx.scene.layout.HBox(10, index, cover, info, duration); // Context Menu private final ContextMenu contextMenu = new ContextMenu(); private final Menu addToPlaylistMenu = new Menu("Add to Playlist"); { cover.setFitWidth(36); cover.setFitHeight(36); cover.setPreserveRatio(true); index.setStyle("-fx-text-fill: #9E9E9E; -fx-font-size: 11px; -fx-min-width: 20px;"); title.setStyle("-fx-font-size: 13px; -fx-text-fill: #212121;"); artist.setStyle("-fx-font-size: 11px; -fx-text-fill: #757575;"); duration.setStyle("-fx-font-size: 11px; -fx-text-fill: #9E9E9E;"); javafx.scene.layout.HBox.setHgrow(info, javafx.scene.layout.Priority.ALWAYS); root.setAlignment(javafx.geometry.Pos.CENTER_LEFT); root.setPadding(new javafx.geometry.Insets(4, 8, 4, 8)); // Context Menu contextMenu.getItems().add(addToPlaylistMenu); contextMenu.setOnShowing(e -> { addToPlaylistMenu.getItems().clear(); if (playlists.isEmpty()) { MenuItem none = new MenuItem("No playlists yet"); none.setDisable(true); addToPlaylistMenu.getItems().add(none); } else { for (String name : playlists.keySet()) { MenuItem item = new MenuItem(name); item.setOnAction(ev -> { Song song = getItem(); if (song != null) playlists.get(name).add(song); }); addToPlaylistMenu.getItems().add(item); } } }); } @Override protected void updateItem(Song song, boolean empty) { super.updateItem(song, empty); if (empty || song == null) { setGraphic(null); setContextMenu(null); } else { index.setText(String.valueOf(getIndex() + 1)); title.setText(song.getTitle()); artist.setText(song.getArtist()); duration.setText(song.getFormattedDuration()); try { if (song.getCoverPath() != null && !song.getCoverPath().isEmpty()) { cover.setImage(new Image(new File(song.getCoverPath()).toURI().toString(), true)); } else { cover.setImage(new Image( Objects.requireNonNull(getClass().getResourceAsStream("/sbu/images/default-cover.png")))); } } catch (Exception e) { cover.setImage(null); } if (getIndex() == currentIndex && playQueue.equals(new ArrayList<>(library))) { title.setStyle("-fx-font-size: 13px; -fx-text-fill: #1DB954; -fx-font-weight: bold;"); } else { title.setStyle("-fx-font-size: 13px; -fx-text-fill: #212121;"); } setGraphic(root); setContextMenu(contextMenu); } } }); } // ========================================= Playlists ============================================ @FXML public void handleNewPlaylist() { TextInputDialog dialog = new TextInputDialog(); dialog.setTitle("New Playlist"); dialog.setHeaderText(null); dialog.setContentText("Playlist name: "); dialog.showAndWait().ifPresent(name -> { if(!name.isBlank() && !playlists.containsKey(name)){ playlists.put(name, FXCollections.observableArrayList()); playlistsView.setItems( FXCollections.observableArrayList(playlists.keySet()) ); } }); } @FXML private void openPlayList(String name) { playlistDetailName.setText(name); playlistDetailView.setItems(playlists.get(name)); playlistsListPane.setVisible(false); playlistsListPane.setManaged(false); playlistDetailPane.setVisible(true); playlistDetailPane.setManaged(true); } @FXML public void handleBackToPlaylists() { playlistDetailPane.setVisible(false); playlistDetailPane.setManaged(false); playlistsListPane.setVisible(true); playlistsListPane.setManaged(true); } // switching between tabs (library and playlists) @FXML public void showLibrary() { libraryPane.setVisible(true); libraryPane.setManaged(true); playlistsPane.setVisible(false); playlistsPane.setManaged(false); libraryTabBtn.getStyleClass().add("tab-active"); playlistTabBtn.getStyleClass().remove("tab-active"); } @FXML private void showPlaylists() { playlistsPane.setVisible(true); playlistsPane.setManaged(true); libraryPane.setVisible(false); libraryPane.setManaged(false); playlistTabBtn.getStyleClass().add("tab-active"); libraryTabBtn.getStyleClass().remove("tab-active"); } }