/* * Copyright (C) 2006-2014 Gabriel Burca (gburca dash virtmus at ebixio dot com) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package com.ebixio.virtmus; import com.ebixio.util.Log; import com.ebixio.util.PropertyChangeSupportUnique; import com.ebixio.util.WeakPropertyChangeListener; import com.ebixio.virtmus.options.Options; import com.ebixio.virtmus.stats.StatsLogger; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.FilenameFilter; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.prefs.PreferenceChangeEvent; import java.util.prefs.PreferenceChangeListener; import java.util.prefs.Preferences; import javax.swing.JOptionPane; import org.openide.util.NbPreferences; /** * The set of all the PlayLists that VirtMus knows about. * @author Gabriel Burca <gburca dash virtmus at ebixio dot com> */ public class PlayListSet implements PreferenceChangeListener, PropertyChangeListener { private static PlayListSet instance; public final List<PlayList> playLists = Collections.synchronizedList(new ArrayList<PlayList>()); /** If this is true, {@link #PROP_ALL_PL_LOADED} has already been fired. */ public Boolean allPlayListsLoaded = new Boolean(false); private int playListsLoading = 0; private PropertyChangeSupportUnique propertyChangeSupport; private final Object pcsMutex = new Object(); /** The property fired when all PlayLists have been loaded */ public static final String PROP_ALL_PL_LOADED = "allPlayListsLoaded"; /** The property fired when all songs have been loaded */ public static final String PROP_ALL_SONGS_LOADED= "allSongsLoaded"; /** The property fired when a new PlayList has been added */ public static final String PROP_NEW_PL_ADDED = "newPlayListAdded"; /** The property fired when a PlayList has been deleted */ public static final String PROP_PL_DELETED = "playListDeleted"; private PlayListSet() { } private void init() { Preferences pref = NbPreferences.forModule(MainApp.class); pref.addPreferenceChangeListener(this); propertyChangeSupport = new PropertyChangeSupportUnique(this); addAllPlayLists(false); } /** * @return A new PlayListSet instance. */ public static synchronized PlayListSet findInstance() { if (instance == null) { instance = new PlayListSet(); instance.init(); } return instance; } public boolean isDirty() { synchronized (playLists) { for (PlayList pl : playLists) { if (pl.isDirty()) { Log.log("Dirty PlayList: " + pl.getName()); return true; } synchronized (pl.songs) { for (Song s : pl.songs) { if (s.isDirty()) { Log.log("Dirty Song: " + s.getName()); return true; } } } } } return false; } public void saveAll() { synchronized(playLists) { for (PlayList pl: playLists) pl.saveAll(); } MainApp.setStatusText("Save All finished."); } public boolean replacePlayList(PlayList replace, PlayList with) { synchronized (playLists) { int idx = playLists.lastIndexOf(replace); if (idx < 0) { return false; } else { playLists.remove(idx); playLists.add(idx, with); this.fire(PROP_NEW_PL_ADDED, replace, with); return true; } } } /** Adds a new PlayList to the set. * @param pl The PlayList to add * @return true if it was successfully added (if not null). */ public boolean addPlayList(PlayList pl) { if (pl != null) { playLists.add(pl); this.fire(PROP_NEW_PL_ADDED, null, pl); return true; } return false; } public boolean deletePlayList(PlayList pl) { if (pl.type != PlayList.Type.Normal || pl.getSourceFile() == null) return false; playLists.remove(pl); try { if (pl.getSourceFile().delete()) { fire(PROP_PL_DELETED, pl, null); return true; } } catch (Exception e) { // Ignore exception } return false; } public PlayList getPlayList(PlayList.Type type) { if (type == PlayList.Type.Default) { return playLists.get(0); } else if (type == PlayList.Type.AllSongs) { return playLists.get(1); } else { return null; } } /** * Finds all the PlayLists on disk and loads them. * @param clearSongs If true, discards all songs so they get re-loaded when * the PlayList is re-created. This would effectively refresh everything. */ public void addAllPlayLists(final boolean clearSongs) { MainApp.setStatusText("Re-loading all PlayLists"); Thread t = new AddPlayLists(NbPreferences.forModule(MainApp.class), clearSongs); t.start(); } @Override public void preferenceChange(PreferenceChangeEvent evt) { switch (evt.getKey()) { case Options.OptSongDir: Log.log("Preference SongDir changed"); /* We need the synchronization because we could be in the middle of AddPlayLists.run() when the preferences are changed, and playLists.get(1) might not exist yet (or might be something other than AllSongs). */ synchronized(AddPlayLists.class) { if (!promptForSave("Save all changes before loading new song directory?")) return; playLists.get(1).addAllSongs(new File(evt.getNewValue()), true); } break; case Options.OptPlayListDir: addAllPlayLists(false); break; } } /** * * @param msg * @return false if user selected "Cancel" */ private boolean promptForSave(String msg) { if (isDirty()) { int returnVal = JOptionPane.showConfirmDialog(null, "You have unsaved changes. " + msg, "Changes exist in currently loaded playlists or songs.", JOptionPane.YES_NO_CANCEL_OPTION); switch (returnVal) { case JOptionPane.YES_OPTION: saveAll(); break; case JOptionPane.CANCEL_OPTION: return false; case JOptionPane.NO_OPTION: default: break; } } return true; } // <editor-fold defaultstate="collapsed" desc=" Property Change Listener "> /** Listeners will be notified of any changes to the set of PlayLists. * @param pcl A property change listener to add */ public void addPropertyChangeListener (PropertyChangeListener pcl) { synchronized(pcsMutex) { propertyChangeSupport.addPropertyChangeListener(pcl); } } /** Listeners will be notified of changes to the set of PlayLists. * @param propertyName A property such as * {@link #PROP_NEW_PL_ADDED}, {@link #PROP_PL_DELETED}, * {@link #PROP_ALL_PL_LOADED}, or {@link #PROP_ALL_SONGS_LOADED}. * @param pcl The property change listener to be notified when the given * property changes. */ public void addPropertyChangeListener(String propertyName, PropertyChangeListener pcl) { synchronized(pcsMutex) { propertyChangeSupport.addPropertyChangeListener(propertyName, pcl); } } public void removePropertyChangeListener(PropertyChangeListener pcl) { synchronized(pcsMutex) { propertyChangeSupport.removePropertyChangeListener(pcl); } } private void fire(String propertyName, Object old, Object nue) { synchronized(pcsMutex) { propertyChangeSupport.firePropertyChange(propertyName, old, nue); } } // </editor-fold> /** * Monitors PlayList loading. Only after ALL PlayLists have finished loading * their songs do we fire "all songs loaded". * @param evt A PlayList property change event */ @Override public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName() == null) return; switch (evt.getPropertyName()) { case PlayList.PROP_LOADED: if ((boolean)evt.getNewValue()) { decrPlayListsLoading(); if (getPlayListsLoading() == 0) { fire(PROP_ALL_SONGS_LOADED, false, true); } } break; } } // <editor-fold defaultstate="collapsed" desc=" PlayList loading count "> /** * Call this every time a PlayList starts loading. */ public synchronized void incrPlayListsLoading() { playListsLoading += 1; } /** * Call this every time a PlayList finished loading. */ public synchronized void decrPlayListsLoading() { playListsLoading -= 1; } /** * This tell us if there are any PlayLists still loading. * @return The number of PlayLists currently being loaded. */ public synchronized int getPlayListsLoading() { return playListsLoading; } // </editor-fold> // <editor-fold defaultstate="collapsed" desc=" Filesystem moves "> /** Update PlayLists so they all point to the new song. * * A PlayList could reference the same song more than once. * @param oldS The old location of the song file * @param newS The new location of the song file * @return The number of PlayLists that were impacted by the move. */ public int movedSong(File oldS, File newS) { int updated = 0; for (PlayList pl: PlayListSet.findInstance().playLists) { for (Song s: pl.songs) { if (s.getSourceFile() != null && s.getSourceFile().equals(oldS)) { s.setSourceFile(newS); updated++; } } pl.save(); // Only saves if isDirty } return updated; } /** Update Songs so they all point to the new location of a PDF file. * * @param oldPdf The old location of the PDF file * @param newPdf The new location of the PDF file * @return The number of songs that were impacted by the move. */ public int movedPdf(File oldPdf, File newPdf) { int updated = 0; for (PlayList pl: PlayListSet.findInstance().playLists) { for (Song s: pl.songs) { boolean needsSave = false; for (MusicPage mp: s.pageOrder) { if (mp.imgSrc.getSourceFile().equals(oldPdf)) { mp.imgSrc.setSourceFile(newPdf); needsSave = true; } } if (needsSave) { s.save(); updated++; } } } return updated; } // </editor-fold> class AddPlayLists extends Thread { Preferences pref; boolean clearSongs; public AddPlayLists(Preferences pref, boolean clearSongs) { this.pref = pref; this.clearSongs = clearSongs; setName("addPlayLists"); setPriority(Thread.MIN_PRIORITY); } @Override public void run() { PlayList pl; // Syncrhonized to block re-loads if there is one in progress synchronized (AddPlayLists.class) { if (!promptForSave("Save all changes before loading new playlists?")) return; playLists.clear(); // Discard all the songs so they get re-loaded when the playlist is re-created if (clearSongs) Song.clearInstantiated(); pl = new PlayList("Default Play List"); pl.type = PlayList.Type.Default; playLists.add(pl); fire(PROP_NEW_PL_ADDED, null, pl); File dir = new File(pref.get(Options.OptPlayListDir, "")); if (dir.exists() && dir.canRead() && dir.isDirectory()) { FilenameFilter filter = new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.endsWith(".playlist.xml"); } }; for (File f : Utils.listFiles(dir, filter, true)) { incrPlayListsLoading(); pl = PlayList.deserialize(f, new WeakPropertyChangeListener(PlayListSet.this, pl)); if (pl == null) { decrPlayListsLoading(); } else { playLists.add(pl); fire(PROP_NEW_PL_ADDED, null, pl); } } } pl = new PlayList("All Songs"); pl.type = PlayList.Type.AllSongs; incrPlayListsLoading(); pl.addPropertyChangeListener(PlayList.PROP_LOADED, new WeakPropertyChangeListener(PlayListSet.this, pl)); pl.addAllSongs(new File(pref.get(Options.OptSongDir, "")), true); playLists.add(pl); fire(PROP_NEW_PL_ADDED, null, pl); LogRecord rec = new LogRecord(Level.INFO, "VirtMus PlayLists"); rec.setParameters(new Object[] {playLists.size()}); StatsLogger.getLogger().log(rec); Collections.sort(playLists); synchronized(allPlayListsLoaded) { // They're not fully loaded until their songs are also loaded. fire(PROP_ALL_PL_LOADED, null, playLists); allPlayListsLoaded = true; } MainApp.setStatusText("Finished loading all PlayLists"); } } } }