package org.lysty.ui; import java.awt.dnd.DropTarget; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import javax.swing.AbstractAction; import javax.swing.DropMode; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.WindowConstants; import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.table.TableColumnModel; import net.miginfocom.swing.MigLayout; import org.apache.log4j.Logger; import org.lysty.core.AppSettingsManager; import org.lysty.core.PlaylistGenerator; import org.lysty.core.SongPlayer; import org.lysty.core.StrategyFactory; import org.lysty.dao.Song; import org.lysty.dao.SongSelectionProfile; import org.lysty.db.DBHandler; import org.lysty.players.PlayEvent; import org.lysty.players.PlaybackListener; import org.lysty.strategies.StrategyConfiguration; import org.lysty.ui.PlayerPanel.PlayState; import org.lysty.ui.exception.SongNotIndexedException; import org.lysty.ui.model.PlaylistModel; import org.lysty.util.FileUtils; import org.lysty.util.Utils; public class PlaylistPreviewWindow extends LFrame implements PlayPanelListener { private static final int DEFAULT_GENLIST_SIZE = 5; // private static final int genListSize = 5; private static final int DEFAULT_INFINIPLAY_LAST_N_TO_CHECK = 8; private List<Song> list; private PlaylistModel model; private int currentSongIndex; private Set<Song> played; private JScrollPane scrollPane; private JTable table; private PlayerPanel playerPanel; private boolean isRandomized; private SongSelectionProfile selProfile; private List<Song> manuallyAdded; private List<Song> manuallySkipped; private PlaybackListener playbackListener; private Timer timer; static Logger logger = Logger.getLogger(PlaylistPreviewWindow.class); private static PlaylistPreviewWindow self = null; public static PlaylistPreviewWindow getInstance() { if (self == null) { self = new PlaylistPreviewWindow(); } return self; } private PlaylistPreviewWindow() { super("Lysty Media Player"); createUI(); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { updateSettings(); AppSettingsManager.writeAppSettings(); } }); playbackListener = new PlaybackListener() { @Override public void getNotification(PlayEvent event) { if (event.getEventType() == PlayEvent.EventType.SONG_ENDED) { // previous song has ended; playerPanel.setState(PlayState.STOPPED); playNextSong(); } else if (event.getEventType() == PlayEvent.EventType.PLAY_EXCEPTION) { JOptionPane.showMessageDialog(PlaylistPreviewWindow.this, "Error playing song: "); playerPanel.setState(PlayerPanel.PlayState.STOPPED); } else if (event.getEventType() == PlayEvent.EventType.SONG_PAUSED) { playerPanel.setPausedOnFrame(event.getFrame()); } else if (event.getEventType() == PlayEvent.EventType.SONG_STOPPED) { playerPanel.setPausedOnFrame(0); } } }; init(new ArrayList<Song>(), false, null); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } private void loadSettings() { String xStr = AppSettingsManager.getProperty(AppSettingsManager.LS_X); String yStr = AppSettingsManager.getProperty(AppSettingsManager.LS_Y); if (Utils.isNumber(xStr) && Utils.isNumber(yStr)) { setLocation(Integer.parseInt(xStr), Integer.parseInt(yStr)); } } protected void updateSettings() { AppSettingsManager.setProperty(AppSettingsManager.LS_X, getX() + ""); AppSettingsManager.setProperty(AppSettingsManager.LS_Y, getY() + ""); playerPanel.updateSettings(); } private void createUI() { table = new JTable(); model = new PlaylistModel(); table.setModel(model); table.setDragEnabled(true); table.setDropMode(DropMode.INSERT_ROWS); table.setTransferHandler(new TableRowTransferHandler(table)); TableDragDropListener myDragDropListener = new TableDragDropListener( table); new DropTarget(table, myDragDropListener); scrollPane = new JScrollPane(table); table.setFillsViewportHeight(true); TableColumnModel colModel = table.getColumnModel(); colModel.getColumn(0).setPreferredWidth(Integer.MAX_VALUE); table.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { JTable target = (JTable) e.getSource(); int row = target.getSelectedRow(); currentSongIndex = row; stop(); playerPanel.setState(PlayerPanel.PlayState.PLAYING); playerPanel.setCurrentSongProgress(0); play(0); } } @Override public void mouseReleased(MouseEvent e) { int r = table.rowAtPoint(e.getPoint()); if (r >= 0 && r < table.getRowCount()) { table.setRowSelectionInterval(r, r); } else { table.clearSelection(); } final int rowindex = table.getSelectedRow(); if (rowindex < 0) return; if (e.isPopupTrigger() && e.getComponent() instanceof JTable) { JPopupMenu tablePopup = new JPopupMenu(); JMenuItem mnuRem = new JMenuItem(new AbstractAction( "Remove") { @Override public void actionPerformed(ActionEvent e) { if (rowindex == currentSongIndex) { stop(); } model.removeRow(rowindex); } }); JMenuItem mnuMeta = new JMenuItem(new AbstractAction( "View/Edit Meta Data") { @Override public void actionPerformed(ActionEvent arg0) { Song song = model.getSongAt(rowindex); SongMetaDataEditor editor = new SongMetaDataEditor(); editor.init(song); editor.setLocationRelativeTo(PlaylistPreviewWindow.this); } }); tablePopup.add(mnuRem); tablePopup.add(mnuMeta); tablePopup.show(e.getComponent(), e.getX(), e.getY()); } } }); table.setTableHeader(null); createMenu(); loadSettings(); layoutControls(); setToolTips(); this.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); } private void setToolTips() { playerPanel.setToolTips(); } private void createMenu() { JMenuBar menu = new JMenuBar(); JMenu mnuFile = new JMenu("File"); JMenuItem mnuFileSave = new JMenuItem(new AbstractAction("Save") { @Override public void actionPerformed(ActionEvent e) { JFileChooser chooser = new JFileChooser(); chooser.addChoosableFileFilter(new FileNameExtensionFilter( ".m3u", "m3u")); int c = chooser.showSaveDialog(PlaylistPreviewWindow.this); if (c != JFileChooser.APPROVE_OPTION) { return; } File file = chooser.getSelectedFile(); try { if (!file.getName().contains(".")) { // no ext provided. // default to .m3u file = new File(file.getAbsolutePath() + ".m3u"); } boolean success = FileUtils.savePlaylist(list, file); } catch (IOException e1) { logger.error( "Error saving playlist to: " + file.getAbsolutePath(), e1); } } }); JMenuItem mnuAddSong = new JMenuItem(new AbstractAction("Add Songs") { @Override public void actionPerformed(ActionEvent arg0) { JFileChooser chooser = new JFileChooser(); chooser.setMultiSelectionEnabled(true); int c = chooser.showOpenDialog(PlaylistPreviewWindow.this); if (c == JFileChooser.APPROVE_OPTION) { File[] files = chooser.getSelectedFiles(); Song song; for (File file : files) { song = DBHandler.getInstance().getSong(file); if (song == null) { song = new Song(file); } addSongNext(song); } } } }); JMenuItem mnuClear = new JMenuItem( new AbstractAction("Clear Playlist") { @Override public void actionPerformed(ActionEvent e) { stop(); init(new ArrayList<Song>(), false, null); model.fireTableDataChanged(); } }); JMenuItem mnuLoadPlaylist = new JMenuItem(new AbstractAction( "Load Playlist") { @Override public void actionPerformed(ActionEvent arg0) { JFileChooser chooser = new JFileChooser(); chooser.addChoosableFileFilter(new FileNameExtensionFilter( ".m3u", "m3u")); int c = chooser.showOpenDialog(PlaylistPreviewWindow.this); if (c != JFileChooser.APPROVE_OPTION) { return; } File file = chooser.getSelectedFile(); loadFromPlayList(file); } }); JMenuItem mnuExit = new JMenuItem(new AbstractAction("Exit") { @Override public void actionPerformed(ActionEvent e) { System.exit(0); } }); JMenu mnuExport = new JMenu("Export"); JMenuItem mnuExportCopy = new JMenuItem(new AbstractAction( "Copy Files...") { @Override public void actionPerformed(ActionEvent e) { copyFiles(); } }); mnuExport.add(mnuExportCopy); JMenu mnuTools = new JMenu("Tools"); JMenuItem mnuPPEdit = new JMenuItem(new AbstractAction( "Partial Playlist Editor") { @Override public void actionPerformed(ActionEvent e) { PlaylistProfileWindow instance = PlaylistProfileWindow .getInstance(); instance.setVisible(true); instance.setLocation(PlaylistPreviewWindow.this.getX() + PlaylistPreviewWindow.this.getWidth(), PlaylistPreviewWindow.this.getY()); } }); JMenuItem mnuEditMetaData = new JMenuItem(new AbstractAction( "Edit MetaData") { @Override public void actionPerformed(ActionEvent e) { MetaDataEditor.getInstance().createUI(); MetaDataEditor.getInstance().setVisible(true); } }); JMenuItem mnuToolsIndex = new JMenuItem(new AbstractAction("Index...") { @Override public void actionPerformed(ActionEvent e) { Commands.showIndexDialog(PlaylistPreviewWindow.this); } }); JMenuItem mnuToolsSettings = new JMenuItem(new AbstractAction( "Settings") { @Override public void actionPerformed(ActionEvent e) { AppSettingsWindow instance = AppSettingsWindow.getInstance(); instance.init(PlaylistPreviewWindow.this); } }); mnuTools.add(mnuPPEdit); mnuTools.addSeparator(); mnuTools.add(mnuEditMetaData); mnuTools.add(mnuToolsIndex); mnuTools.addSeparator(); mnuTools.add(mnuToolsSettings); mnuFile.add(mnuClear); mnuFile.addSeparator(); mnuFile.add(mnuAddSong); mnuFile.add(mnuLoadPlaylist); mnuFile.addSeparator(); mnuFile.add(mnuFileSave); mnuFile.add(mnuExport); mnuFile.addSeparator(); mnuFile.add(mnuExit); menu.add(mnuFile); menu.add(mnuTools); this.setJMenuBar(menu); } protected void loadFromPlayList(File file) { List<Song> llist = FileUtils.loadPlaylist(file); if (llist != null) model.setList(llist); } protected void copyFiles() { JFileChooser chooser = new JFileChooser(); chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); int c = chooser.showOpenDialog(PlaylistPreviewWindow.this); if (c == JFileChooser.APPROVE_OPTION) { File file = chooser.getSelectedFile(); boolean errors = false; for (Song song : list) { try { org.apache.commons.io.FileUtils.copyFileToDirectory( song.getFile(), file); } catch (Exception e1) { // TODO Auto-generated catch block logger.error( "Error copying file" + song.getFile().getName() + " to " + file.getName(), e1); errors = true; } } JOptionPane.showMessageDialog(this, errors ? "Some files couldn't be copied successfully" : "All Songs copied successfully"); } } public void init(List<Song> songList, boolean startPlay, SongSelectionProfile profile) { setSongs(songList); currentSongIndex = 0; played = new HashSet<Song>(); manuallySkipped = new ArrayList<Song>(); manuallyAdded = new ArrayList<Song>(); if (profile != null) { Iterator<Song> it = profile.getRelPosMap().keySet().iterator(); while (it.hasNext()) { manuallyAdded.add(it.next()); } } if (startPlay) { playerPanel.setState(PlayerPanel.PlayState.PLAYING); play(0); } if (profile != null) setSelectionProfile(profile); } private void setSelectionProfile(SongSelectionProfile profile) { playerPanel.setStrategy(profile.getStrategy()); playerPanel.setCurrentProfileSettings(profile.getStrategyConfig()); } @Override public void dispose() { stop(); super.dispose(); } private void layoutControls() { JPanel panel = new JPanel(new MigLayout("insets 6 6 6 6")); playerPanel = new PlayerPanel(this); panel.add(playerPanel, "span"); panel.add(scrollPane, "span"); this.setContentPane(panel); this.setVisible(true); this.pack(); this.setSize(360, 400); playerPanel.loadLatestSettings(); } public PlayState getCurrentState() { return playerPanel.getState(); } private void playNextSong() { Song song = getNextSong(); if (song == null) { if (requiresProfileUpdateForInfiniPlay()) { selProfile = generateNewProfile(); List<Song> newSongList; try { newSongList = StrategyFactory.getPlaylistByStrategy( selProfile.getStrategy(), selProfile, selProfile.getStrategyConfig(), false, false, list); if (newSongList.isEmpty()) { // couldn't generate songs. inform the user to try other // strategies and update meta data JOptionPane .showMessageDialog( this, "<html>Oops! Lysty was unable to find more songs to go with the playlist. <br>You can try the following:<br><br> 1). Use a different fill method. <br><br> 2). This could be due to inadequate metadata for songs. <br>You can fix it by choosing <i>tools->Edit Metadata</i><br><br> 3). Make sure songs are indexed. You can run the indexer by choosing <i>tools->index</i><br><br></html>", "Unable to generate more songs", JOptionPane.INFORMATION_MESSAGE); } for (Song s : newSongList) { model.addSong(s.getFile(), model.getList().size()); } song = getNextSong(); if (song != null) { currentSongIndex = list.indexOf(song); play(0); } } catch (Exception e) { logger.error("Error creating playlist", e); } } } else { currentSongIndex = list.indexOf(song); play(0); } } /** * Algorithm: Consider the "lastNtoCheck" songs in the current playlist. * take it as baselist. create a partials list that is of length * Math.max(lastNtoCheck,genListSize) add the manually added songs in the * lastNtoCheck to the partials first. the position those songs are added to * are such that the last manually added song goes to the 0th position in * the partials (i.e: rotate the baselist such that last manually added * comes to 0th pos) If less than 50% of the genlist size is filled, add * more randomly chosen songs from the baselist. do the same rotation for * their positions as well. Take partials.sublist(0,genlistsize) and take * that as the relative position map to pass to the fill strategy * * @return */ private SongSelectionProfile generateNewProfile() { SongSelectionProfile profile = new SongSelectionProfile(); String genListSizeStr = AppSettingsManager .getProperty(AppSettingsManager.INFINIPLAY_GENLIST_SIZE); int genListSize = DEFAULT_GENLIST_SIZE; if (Utils.stringNotNullOrEmpty(genListSizeStr)) { genListSize = Integer.parseInt(genListSizeStr); } profile.setSize(genListSize); profile.setSizeType(SongSelectionProfile.SIZE_TYPE_LENGTH); selProfile = new SongSelectionProfile(); PlaylistGenerator strategy = playerPanel.getCurrentStrategy(); selProfile.setStrategy(strategy); StrategyConfiguration currentStrategySettings = playerPanel .getCurrentStrategySettings(); if (currentStrategySettings == null) currentStrategySettings = StrategyFactory .getDefaultOrLastSettings(strategy); selProfile.setStrategyConfig(currentStrategySettings); profile.setStrategy(selProfile.getStrategy()); profile.setStrategyConfig(selProfile.getStrategyConfig()); int infiniPlayLastNtoCheck = DEFAULT_INFINIPLAY_LAST_N_TO_CHECK; String infiniPlayLastNStr = AppSettingsManager .getProperty(AppSettingsManager.INFINIPLAY_LAST_N_TO_CHECK); if (Utils.stringNotNullOrEmpty(infiniPlayLastNStr)) { infiniPlayLastNtoCheck = Integer.parseInt(infiniPlayLastNStr); } List<Song> baseList = list.subList( Math.max(0, list.size() - infiniPlayLastNtoCheck), list.size()); int partialsLen = Math.max(infiniPlayLastNtoCheck, genListSize); List<Song> partials = new ArrayList<Song>(partialsLen); for (int i = 0; i < partialsLen; i++) { partials.add(null); // nullfill } int addedCnt = 0; int distFromEndToLastManualAdd = 0; for (int i = baseList.size() - 1; i >= 0; i--) {// find dist to last // manually added song // from the end of list if (manuallyAdded.contains(baseList.get(i))) { distFromEndToLastManualAdd = baseList.size() - i; break; } } for (int i = 0; i < baseList.size(); i++) { if (manuallyAdded.contains(baseList.get(i))) { // add the manually // addeds partials.set( (i + distFromEndToLastManualAdd) % baseList.size(), baseList.get(i)); addedCnt++; } } int tries = 0; int random; while (addedCnt < genListSize / 2 && tries < genListSize) { // need to add more random chosens from the last N tries++; random = (int) (Math.random() * baseList.size()); if (partials.contains(baseList.get(random))) { continue; } else { partials.set( (random + distFromEndToLastManualAdd) % baseList.size(), baseList.get(random)); addedCnt++; } } profile.setRelPosMap(partials.subList(0, genListSize)); return profile; } private boolean requiresProfileUpdateForInfiniPlay() { if (!playerPanel.getIsInfiniPlay()) return false; // TODO Auto-generated method stub if (currentSongIndex + 1 >= list.size()) return true; return false; } private Song getNextSong() { if (isRandomized) { int rand; int tries = 0; while (true) { rand = (int) (Math.random() * list.size()); currentSongIndex = rand; if (tries >= list.size()) { return null; } if (played.contains(list.get(rand))) { tries++; } else { return list.get(rand); } } } else { if (list.size() <= currentSongIndex + 1) return null; currentSongIndex++; return list.get(currentSongIndex); } } private void setSongs(List<Song> songList) { list = songList; model.setList(list); } @Override public void play(int playFrom) { playSong(currentSongIndex, playFrom); } private void playSong(int index, int playFrom) { if (list.isEmpty()) return; final Song song = list.get(index); playerPanel.setPausedOnFrame(playFrom); played.add(song); table.setRowSelectionInterval(index, index); try { SongPlayer.getInstance().play(song, playFrom, playbackListener); playerPanel.setState(PlayState.PLAYING); playerPanel.setCurrentSong(song, playFrom); } catch (Exception e) { logger.error("Error playing song: " + song.getFile().getName(), e); JOptionPane.showMessageDialog(this, "Cannot play file: " + song.getFile().getName()); playerPanel.setState(PlayerPanel.PlayState.STOPPED); } } @Override public void pause() { SongPlayer.getInstance().pause(); } @Override public void stop() { playerPanel.setState(PlayerPanel.PlayState.STOPPED); playerPanel.setCurrentSongProgress(0); SongPlayer.getInstance().stop(); } @Override public void next() { manuallySkipped.add(list.get(currentSongIndex)); playNextSong(); } @Override public void prev() { currentSongIndex--; if (currentSongIndex < 0) { currentSongIndex = list.size() - 1; } play(0); } @Override public void setInfinyPlay(boolean isInfini) { if (isInfini) { List<Song> allSongs = DBHandler.getInstance().getAllSongs(); if (allSongs == null || allSongs.size() == 0) { String[] options = new String[] { "Yes", "No" }; // no songs in the index int choice = JOptionPane .showOptionDialog( this, "<html>There are no songs currently indexed in the Lysty DB.<br> You need to run the indexer for <i><b>InfiniPlay</b></i> to work.<br><br> Would you like to index songs now?</html>", "Indexer Empty", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, JOptionPane.YES_OPTION); if (choice == JOptionPane.YES_OPTION) { IndexerWindow.getInstance().setVisible(true); IndexerWindow.getInstance().setLocationRelativeTo(this); } } } } public void cancelTimer() { if (timer != null) { timer.cancel(); } } @Override public void setTimer(int time) { if (timer != null) timer.cancel(); timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { try { Utils.shutdown(1); } catch (IOException e) { logger.error("Error shutting down", e); } } }, time); } @Override public void setRandomize(boolean isRandom) { isRandomized = isRandom; } public void addSongNext(Song song) { int pos = 0; if (list == null) { list = new ArrayList<Song>(); currentSongIndex = 0; } if (manuallyAdded == null) manuallyAdded = new ArrayList<Song>(); if (list.isEmpty()) { pos = 0; } else { pos = currentSongIndex + 1; } try { model.addSong(song.getFile(), pos); manuallyAdded.add(song); boolean remAutoAdds = "true" .equalsIgnoreCase(AppSettingsManager .getProperty(AppSettingsManager.REM_UNPLAYED_AUTOGENS_ON_MANUAL_ADD)); if (remAutoAdds) { List<Song> toRem = new ArrayList<Song>(); for (int i = pos + 1; i < list.size(); i++) { if (!manuallyAdded.contains(list.get(i))) { // autoadded toRem.add(list.get(i)); } } for (Song remSong : toRem) { list.remove(remSong); } } } catch (SongNotIndexedException e) { // never reaches since playlistpreview window doesn't expect // file to be indexed logger.error("Song add exception in playlist preview window", e); } return; } public void enqueueSong(Song song) { try { model.addSong(song.getFile(), model.getList().size()); manuallyAdded.add(song); } catch (SongNotIndexedException e) { // never reaches since playlistpreview window doesn't expect // file to be indexed logger.error("Song add exception in playlist preview window", e); } } }