From 7a2a5bed25db872bda99b2e5a2861e264aeae1ae Mon Sep 17 00:00:00 2001 From: mdb Date: Wed, 18 Aug 2004 04:23:00 +0000 Subject: [PATCH] The Big Fat RoboDJ revamp. Rewrote the server using Presents; rewrote the chooser to interface using distributed objects; implemented 90% of the music importing process (just need to upload the data to the server and stuff the new music records in the database). git-svn-id: https://samskivert.googlecode.com/svn/trunk@1489 6335cc39-0255-0410-8fd6-9bcaacd3b74c --- projects/robodj/build.xml | 45 ++- .../src/java/robodj/chooser/Chooser.java | 134 ++++++- .../robodj/chooser/ChooserController.java | 40 +-- .../src/java/robodj/chooser/ChooserFrame.java | 19 +- .../java/robodj/chooser/EntryController.java | 24 +- .../robodj/chooser/PlaylistController.java | 212 ++++------- .../java/robodj/chooser/PlaylistPanel.java | 26 +- .../src/java/robodj/client/DJService.java | 16 + .../src/java/robodj/data/DJMarshaller.java | 35 ++ .../robodj/src/java/robodj/data/DJObject.dobj | 128 +++++++ .../robodj/src/java/robodj/data/DJObject.java | 176 ++++++++++ .../src/java/robodj/data/PlaylistEntry.java | 71 ++++ .../java/robodj/importer/EntryUploader.java | 49 +++ .../java/robodj/importer/FinishedPanel.java | 10 +- .../robodj/importer/ImportMusicPanel.java | 329 ++++++++++++++++++ .../src/java/robodj/importer/Importer.java | 5 +- .../java/robodj/importer/ImporterPanel.java | 30 ++ .../src/java/robodj/importer/RipPanel.java | 20 +- .../java/robodj/importer/SongTableModel.java | 191 ++++++++++ .../{InsertCDPanel.java => StartPanel.java} | 37 +- .../robodj/importer/UploadMusicPanel.java | 191 ++++++++++ .../src/java/robodj/server/DJDispatcher.java | 52 +++ .../src/java/robodj/server/DJProvider.java | 24 ++ .../src/java/robodj/server/MusicDaemon.java | 105 ++++++ .../src/java/robodj/server/MusicPlayer.java | 189 ++++++++++ .../src/java/robodj/util/RDJPrefsPanel.java | 5 + 26 files changed, 1902 insertions(+), 261 deletions(-) create mode 100644 projects/robodj/src/java/robodj/client/DJService.java create mode 100644 projects/robodj/src/java/robodj/data/DJMarshaller.java create mode 100644 projects/robodj/src/java/robodj/data/DJObject.dobj create mode 100644 projects/robodj/src/java/robodj/data/DJObject.java create mode 100644 projects/robodj/src/java/robodj/data/PlaylistEntry.java create mode 100644 projects/robodj/src/java/robodj/importer/EntryUploader.java create mode 100644 projects/robodj/src/java/robodj/importer/ImportMusicPanel.java create mode 100644 projects/robodj/src/java/robodj/importer/SongTableModel.java rename projects/robodj/src/java/robodj/importer/{InsertCDPanel.java => StartPanel.java} (51%) create mode 100644 projects/robodj/src/java/robodj/importer/UploadMusicPanel.java create mode 100644 projects/robodj/src/java/robodj/server/DJDispatcher.java create mode 100644 projects/robodj/src/java/robodj/server/DJProvider.java create mode 100644 projects/robodj/src/java/robodj/server/MusicDaemon.java create mode 100644 projects/robodj/src/java/robodj/server/MusicPlayer.java diff --git a/projects/robodj/build.xml b/projects/robodj/build.xml index e98f37aa..f77fc9e6 100644 --- a/projects/robodj/build.xml +++ b/projects/robodj/build.xml @@ -9,6 +9,7 @@ + @@ -21,22 +22,51 @@ - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + @@ -55,7 +85,7 @@ - @@ -68,7 +98,6 @@ windowtitle="${app.name} API" doctitle="${app.name} API" overview="${src.dir}/${doc.overview}" - bottom="Copyright © 2002 ${copyright.holder}. All Rights Reserved." destdir="${javadoc.dir}"> @@ -82,7 +111,7 @@ + basedir="${classes.dir}"/> diff --git a/projects/robodj/src/java/robodj/chooser/Chooser.java b/projects/robodj/src/java/robodj/chooser/Chooser.java index 95300739..6e10ed5c 100644 --- a/projects/robodj/src/java/robodj/chooser/Chooser.java +++ b/projects/robodj/src/java/robodj/chooser/Chooser.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.util.Properties; import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; import javax.swing.UIManager; import com.samskivert.io.PersistenceException; @@ -18,7 +19,16 @@ import com.samskivert.util.ConfigUtil; import com.samskivert.util.PropertiesUtil; import com.samskivert.util.RunAnywhere; +import com.threerings.presents.client.Client; +import com.threerings.presents.client.ClientObserver; +import com.threerings.presents.dobj.DObject; +import com.threerings.presents.dobj.ObjectAccessException; +import com.threerings.presents.dobj.Subscriber; +import com.threerings.presents.net.UsernamePasswordCreds; + import robodj.Log; +import robodj.client.DJService; +import robodj.data.DJObject; import robodj.repository.Model; import robodj.repository.Repository; import robodj.util.ErrorUtil; @@ -31,16 +41,19 @@ import robodj.util.ServerControl; * collection and managing the playlist. */ public class Chooser + implements Client.Invoker, ClientObserver, Subscriber { public static Repository repository; public static Model model; - public static ServerControl scontrol; + public static Client client; + + public static DJObject djobj; public static ChooserFrame frame; - public static void main (String[] args) + public void init () { boolean error = false; @@ -85,8 +98,11 @@ public class Chooser "No music server host specified in configuration."); } // establish a connection with the music server - scontrol = new ServerControl( - host, RDJPrefs.getMusicDaemonPort()); + client = new Client(new UsernamePasswordCreds(), this); + client.addClientObserver(this); + client.setServer(host, Client.DEFAULT_SERVER_PORT); + client.logon(); + } catch (IOException ioe) { String errmsg = "Unable to communicate with music " + "server on host '" + RDJPrefs.getMusicDaemonHost() + "' "; @@ -98,12 +114,110 @@ public class Chooser break; } + } - // create our primary user interface frame, center the frame in - // the screen and show it - frame = new ChooserFrame(); - frame.setSize(660, frame.getToolkit().getScreenSize().height - 60); - SwingUtil.centerWindow(frame); - frame.setVisible(true); + // documentation inherited from interface + public void clientDidLogon (Client lclient) + { + // subscribe to our DJObject + DJService djsvc = (DJService)client.requireService(DJService.class); + djsvc.getDJOid(client, new DJService.ResultListener() { + public void requestProcessed (Object result) { + int djoid = ((Integer)result).intValue(); + client.getDObjectManager().subscribeToObject( + djoid, Chooser.this); + } + public void requestFailed (String cause) { + ErrorUtil.reportError("Failed to fetch DJ oid: " + cause, null); + System.exit(-1); + } + }); + } + + // documentation inherited from interface + public void clientObjectDidChange (Client client) + { + } + + // documentation inherited from interface + public void clientDidLogoff (Client client) + { + System.exit(-1); + } + + // documentation inherited from interface + public void clientFailedToLogon (Client lclient, Exception cause) + { + if (ErrorUtil.reportError("Failed to connect to server", cause)) { + System.exit(-1); + } else { + Thread t = new Thread() { + public void run () { + RDJPrefsPanel.display(true); + client.setServer(RDJPrefs.getMusicDaemonHost(), + Client.DEFAULT_SERVER_PORT); + client.logon(); + } + }; + t.start(); + } + } + + // documentation inherited from interface + public void clientConnectionFailed (Client client, Exception cause) + { + ErrorUtil.reportError("Lost connection to server", cause); + System.exit(-1); + } + + // documentation inherited from interface + public boolean clientWillLogoff (Client client) + { + return true; + } + + // documentation inherited from interface + public void invokeLater (Runnable run) + { + SwingUtilities.invokeLater(run); + } + + // documentation inherited from interface + public void objectAvailable (DObject object) + { + djobj = (DJObject)object; + displayInterface(); + } + + // documentation inherited from interface + public void requestFailed (int oid, ObjectAccessException cause) + { + ErrorUtil.reportError("Failed to fetch DJ object", cause); + System.exit(-1); + } + + protected void displayInterface () + { + if (frame == null) { + // create our primary user interface frame, center the frame + // in the screen and show it + frame = new ChooserFrame(); + frame.setSize(660, frame.getToolkit().getScreenSize().height - 60); + SwingUtil.centerWindow(frame); + frame.setVisible(true); + } else { + frame.toFront(); + } + } + + public static void exit () + { + client.logoff(false); + } + + public static void main (String[] args) + { + Chooser chooser = new Chooser(); + chooser.init(); } } diff --git a/projects/robodj/src/java/robodj/chooser/ChooserController.java b/projects/robodj/src/java/robodj/chooser/ChooserController.java index bc9a9171..d92f6630 100644 --- a/projects/robodj/src/java/robodj/chooser/ChooserController.java +++ b/projects/robodj/src/java/robodj/chooser/ChooserController.java @@ -4,11 +4,8 @@ package robodj.chooser; import java.awt.event.ActionEvent; -import javax.swing.JOptionPane; import com.samskivert.swing.Controller; -import com.samskivert.swing.util.TaskMaster; -import com.samskivert.swing.util.TaskObserver; import robodj.Log; @@ -16,12 +13,9 @@ import robodj.Log; * Handles top-level chooser UI commands. */ public class ChooserController extends Controller - implements TaskObserver { public ChooserController () { - // read our playing state - TaskMaster.invokeMethodTask("refreshPlaying", Chooser.scontrol, this); } // documentation inherited @@ -29,48 +23,28 @@ public class ChooserController extends Controller { String cmd = event.getActionCommand(); if (cmd.equals("exit")) { - System.exit(0); + Chooser.exit(); } else if (cmd.equals("skip")) { - TaskMaster.invokeMethodTask("skip", Chooser.scontrol, this); + Chooser.djobj.skip(); } else if (cmd.equals("back")) { - TaskMaster.invokeMethodTask("back", Chooser.scontrol, this); + Chooser.djobj.back(); } else if (cmd.equals("pause")) { - TaskMaster.invokeMethodTask("pause", Chooser.scontrol, this); + Chooser.djobj.pause(); } else if (cmd.equals("play")) { - TaskMaster.invokeMethodTask("play", Chooser.scontrol, this); + Chooser.djobj.play(); } else if (cmd.equals("stop")) { - TaskMaster.invokeMethodTask("stop", Chooser.scontrol, this); + Chooser.djobj.stop(); } else { - System.out.println("Unknown action event: " + cmd); + Log.warning("Unknown action event: " + cmd); return false; } return true; } - - // documentation inherited from interface - public void taskCompleted (String name, Object result) - { - // nothing to do here - } - - // documentation inherited from interface - public void taskFailed (String name, Throwable exception) - { - String msg; - if (Exception.class.equals(exception.getClass())) { - msg = exception.getMessage(); - } else { - msg = exception.toString(); - } - JOptionPane.showMessageDialog(Chooser.frame, msg, "Error", - JOptionPane.ERROR_MESSAGE); - Log.logStackTrace(exception); - } } diff --git a/projects/robodj/src/java/robodj/chooser/ChooserFrame.java b/projects/robodj/src/java/robodj/chooser/ChooserFrame.java index 30d9d750..9e080613 100644 --- a/projects/robodj/src/java/robodj/chooser/ChooserFrame.java +++ b/projects/robodj/src/java/robodj/chooser/ChooserFrame.java @@ -6,6 +6,8 @@ package robodj.chooser; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; import javax.swing.*; @@ -19,17 +21,21 @@ import robodj.repository.*; import robodj.util.ButtonUtil; import robodj.util.FancyPanel; import robodj.util.RDJPrefs; -import robodj.util.ServerControl.PlayingListener; public class ChooserFrame extends JFrame - implements PlayingListener, ControllerProvider + implements ControllerProvider { public ChooserFrame () { super("RoboDJ Chooser " + Version.RELEASE_VERSION); - // quit if we're closed - setDefaultCloseOperation(EXIT_ON_CLOSE); + // quit gracefully if we're closed + setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); + addWindowListener(new WindowAdapter() { + public void windowClosing (WindowEvent e) { + Chooser.exit(); + } + }); // we create a top-level panel to manage everything GroupLayout gl = new VGroupLayout(GroupLayout.STRETCH); @@ -109,8 +115,8 @@ public class ChooserFrame extends JFrame } }); - // add ourselves as a playing listener - Chooser.scontrol.addPlayingListener(this); + // TODO: add ourselves as a playing listener + // Chooser.djobj.addListener(this); // create our controller _controller = new ChooserController(); @@ -152,6 +158,7 @@ public class ChooserFrame extends JFrame return user; } + // TODO: convert to dobj biz public void playingUpdated (int songid, boolean paused) { if (songid == -1 || paused) { diff --git a/projects/robodj/src/java/robodj/chooser/EntryController.java b/projects/robodj/src/java/robodj/chooser/EntryController.java index ad21785e..2eeccfbd 100644 --- a/projects/robodj/src/java/robodj/chooser/EntryController.java +++ b/projects/robodj/src/java/robodj/chooser/EntryController.java @@ -16,8 +16,10 @@ import javax.swing.event.AncestorListener; import com.samskivert.io.PersistenceException; import com.samskivert.swing.util.SwingUtil; import com.samskivert.swing.util.TaskMaster; +import com.samskivert.util.ArrayUtil; import robodj.Log; +import robodj.data.PlaylistEntry; import robodj.repository.Entry; import robodj.repository.Song; @@ -71,11 +73,7 @@ public abstract class EntryController extends ItemController _list.populateEntries(_entries); } else if (name.equals("readAndPlay")) { - for (int i = 0; i < _entry.songs.length; i++) { - Chooser.scontrol.append(_entry.songs[i].entryid, - _entry.songs[i].songid, - _entry.songs[i].location); - } + appendToPlaylist(_entry.songs); } else if (name.equals("readSongs")) { _list.populateSong(_entry); @@ -110,7 +108,7 @@ public abstract class EntryController extends ItemController } else if (cmd.equals(SongItem.PLAY)) { Song song = SongItem.getSong(e.getSource()); - Chooser.scontrol.append(song.entryid, song.songid, song.location); + appendToPlaylist(new Song[] { song }); } else if (cmd.equals("refresh")) { // re-read the category @@ -164,6 +162,20 @@ public abstract class EntryController extends ItemController // nothing doing } + protected void appendToPlaylist (Song[] songs) + { + PlaylistEntry[] plist = Chooser.djobj.playlist; + if (plist == null) { + plist = new PlaylistEntry[0]; + } + for (int i = 0; i < songs.length; i++) { + PlaylistEntry entry = new PlaylistEntry( + songs[i].entryid, songs[i].songid, songs[i].title); + plist = (PlaylistEntry[])ArrayUtil.append(plist, entry); + } + Chooser.djobj.setPlaylist(plist); + } + protected EntryList _list; protected Entry[] _entries; protected Entry _entry; diff --git a/projects/robodj/src/java/robodj/chooser/PlaylistController.java b/projects/robodj/src/java/robodj/chooser/PlaylistController.java index 575d823f..59820b4f 100644 --- a/projects/robodj/src/java/robodj/chooser/PlaylistController.java +++ b/projects/robodj/src/java/robodj/chooser/PlaylistController.java @@ -13,32 +13,42 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import com.samskivert.io.PersistenceException; -import com.samskivert.util.ArrayUtil; import com.samskivert.util.StringUtil; import com.samskivert.swing.util.SwingUtil; import com.samskivert.swing.util.TaskAdapter; import com.samskivert.swing.util.TaskMaster; +import com.threerings.presents.dobj.AttributeChangeListener; +import com.threerings.presents.dobj.AttributeChangedEvent; + import robodj.Log; -import robodj.chooser.PlaylistPanel.PlaylistEntry; +import robodj.data.DJObject; +import robodj.data.PlaylistEntry; import robodj.repository.Entry; import robodj.repository.Song; -import robodj.util.ServerControl.PlayingListener; /** * Handles playlist UI commands. */ public class PlaylistController extends ItemController - implements PlayingListener + implements AttributeChangeListener { public PlaylistController (PlaylistPanel panel) { super(panel); _panel = panel; - // add ourselves as a playing listener - Chooser.scontrol.addPlayingListener(this); + // listen to the DJ object + Chooser.djobj.addListener(this); + } + + public void wasAdded () + { + super.wasAdded(); + + // start up the task that reads the CD info from the database + TaskMaster.invokeMethodTask("readPlaylist", this, this); } // documentation inherited @@ -46,121 +56,47 @@ public class PlaylistController extends ItemController { String cmd = e.getActionCommand(); if (cmd.equals(PlaylistPanel.CLEAR)) { - TaskMaster.invokeMethodTask("clear", Chooser.scontrol, this); - - } else if (cmd.equals(PlaylistPanel.REFRESH)) { - _panel.prepareForRefresh(); - - // start up the task that reads the CD info from the database - TaskMaster.invokeMethodTask("readPlaylist", this, this); + Chooser.djobj.clear(); } else if (cmd.equals(SongItem.SKIP_TO)) { JButton button = (JButton)e.getSource(); SongItem item = (SongItem)button.getParent(); - - // fire up a task to talk to the music daemon - final int songid = item.getSong().songid; - TaskMaster.invokeTask("noop", new TaskAdapter() { - public Object invoke () throws Exception { - Chooser.scontrol.skipto(songid); - return null; - } - }, this); + Chooser.djobj.play(((Integer)item.extra).intValue()); } else if (cmd.equals(SongItem.REMOVE)) { JButton button = (JButton)e.getSource(); SongItem item = (SongItem)button.getParent(); - - // remove the entry UI elements - item.getParent().remove(item); - SwingUtil.refresh(_panel); // relayout - - // remove the entry from the playlist - _panel.plist.remove((PlaylistEntry)item.extra); - - // fire up a task to talk to the music daemon - final int songid = item.getSong().songid; - TaskMaster.invokeTask("noop", new TaskAdapter() { - public Object invoke () throws Exception { - Chooser.scontrol.remove(songid); - return null; - } - }, this); + int sidx = ((Integer)item.extra).intValue(); + Chooser.djobj.remove(sidx, 1); } else if (cmd.equals(PlaylistPanel.REMOVE_ALL)) { JButton src = (JButton)e.getSource(); - PlaylistEntry entry = - (PlaylistEntry)src.getClientProperty("entry"); + PlaylistPanel.PLE entry = + (PlaylistPanel.PLE)src.getClientProperty("entry"); // remove all entries starting with this one until we get to // one that has a different entryid - int count = 0; - Iterator iter = _panel.plist.iterator(); - while (iter.hasNext()) { - PlaylistEntry pe = (PlaylistEntry)iter.next(); + int start = -1, length = 1; + for (int ii = 0; ii < _panel.plist.size(); ii++) { + PlaylistPanel.PLE pe = (PlaylistPanel.PLE)_panel.plist.get(ii); if (entry == pe) { - count++; - // remove the entry UI elements - pe.item.getParent().remove(pe.item); - // remove the entry - iter.remove(); - - } else if (count > 0) { + start = ii; + } else if (start != -1) { if (pe.entry.entryid == entry.entry.entryid) { - count++; - // remove the entry UI elements - pe.item.getParent().remove(pe.item); - // remove the entry - iter.remove(); - + length++; } else { // we hit an entry that doesn't match, bail break; } } } - - // remove the title and stuff - JPanel tpanel = (JPanel)src.getParent(); - tpanel.getParent().remove(tpanel); - - _panel.revalidate(); // relayout - - // fire up a task to talk to the music daemon, telling it to - // remove those entries - final int songid = entry.song.songid; - final int fcount = count; - TaskMaster.invokeTask("noop", new TaskAdapter() { - public Object invoke () throws Exception { - Chooser.scontrol.removeGroup(songid, fcount); - return null; - } - }, this); + if (start != -1) { + Chooser.djobj.remove(start, length); + } } else if (cmd.equals(PlaylistPanel.SHUFFLE)) { - // put all of the songs in the playlist into a list and - // shuffle them - ArrayList slist = new ArrayList(); - for (Iterator iter = _panel.plist.iterator(); iter.hasNext(); ) { - slist.add(((PlaylistEntry)iter.next()).song); - } - final Song[] songs = (Song[])slist.toArray(new Song[slist.size()]); - ArrayUtil.shuffle(songs); - - // now clear and reload the playlist entirely - TaskMaster.invokeTask("noop", new TaskAdapter() { - public Object invoke () throws Exception { - Chooser.scontrol.clear(); - for (int ii = 0; ii < songs.length; ii++) { - Chooser.scontrol.append( - songs[ii].entryid, songs[ii].songid, - songs[ii].location); - } - // finally queue up a UI refresh - postAction(_panel, PlaylistPanel.REFRESH); - return null; - } - }, this); + // shuffle! + Chooser.djobj.shuffle(); } else { return super.handleAction(e); @@ -169,86 +105,70 @@ public class PlaylistController extends ItemController return true; } + // documentation inherited from interface + public void attributeChanged (AttributeChangedEvent event) + { + if (event.getName().equals(DJObject.PLAYLIST)) { + TaskMaster.invokeMethodTask("readPlaylist", this, this); + + } else if (event.getName().equals(DJObject.PLAYING)) { + playingUpdated(); + } + } + public void readPlaylist () throws PersistenceException { // clear out any previous playlist _panel.plist.clear(); - // get the playlist from the music daemon - String[] plist = Chooser.scontrol.getPlaylist(); - - // parse it into playlist entries - for (int i = 0; i < plist.length; i++) { - String[] toks = StringUtil.split(plist[i], "\t"); - int eid, sid; - try { - eid = Integer.parseInt(toks[0]); - sid = Integer.parseInt(toks[1]); - } catch (NumberFormatException nfe) { - Log.warning("Bogus playlist record: '" + plist[i] + "'."); - continue; - } - Entry entry = Chooser.model.getEntry(eid); + int pcount = (Chooser.djobj.playlist == null) ? 0 : + Chooser.djobj.playlist.length; + for (int ii = 0; ii < pcount; ii++) { + PlaylistEntry pentry = Chooser.djobj.playlist[ii]; + Entry entry = Chooser.model.getEntry(pentry.entryId); if (entry != null) { - Song song = entry.getSong(sid); + Song song = entry.getSong(pentry.songId); if (song != null) { - _panel.plist.add(new PlaylistEntry(entry, song)); + _panel.plist.add(new PlaylistPanel.PLE(entry, song)); } else { - Log.warning("No song for entry? [sid=" + sid + - ", entry=" + entry + "]."); + Log.warning("No song for entry? [pentry=" + pentry + "]."); } } else { - Log.warning("Unable to load entry [eid=" + eid + "]."); + Log.warning("Unable to load entry? [pentry=" + pentry + "]."); } } - - // now find out what's currently playing - Chooser.scontrol.refreshPlaying(); } - public void playingUpdated (int songid, boolean paused) + public void playingUpdated () { // unhighlight whoever is playing now - PlaylistEntry pentry = getPlayingEntry(); - if (pentry != null && pentry.item != null) { - pentry.item.setIsPlaying(false); + if (_playing != null) { + _playing.item.setIsPlaying(false); + _playing = null; } - // grab the new playing song id - _playid = songid; + int pidx = Chooser.djobj.playing; + if (pidx >= 0 && _panel.plist.size() > pidx) { + _playing = (PlaylistPanel.PLE)_panel.plist.get(pidx); + } - // highlight the playing song - for (int i = 0; i < _panel.plist.size(); i++) { - PlaylistEntry entry = (PlaylistEntry)_panel.plist.get(i); - if (entry.song.songid == _playid && entry.item != null) { - entry.item.setIsPlaying(true); - } + if (_playing != null) { + _playing.item.setIsPlaying(true); } _panel.repaint(); } - protected PlaylistEntry getPlayingEntry () - { - for (int i = 0; i < _panel.plist.size(); i++) { - PlaylistEntry entry = (PlaylistEntry)_panel.plist.get(i); - if (entry.song.songid == _playid) { - return entry; - } - } - return null; - } - public void taskCompleted (String name, Object result) { if (name.equals("readPlaylist")) { - _panel.populatePlaylist(_panel.plist, _playid); + _panel.populatePlaylist(_panel.plist); + playingUpdated(); } else { super.taskCompleted(name, result); } } protected PlaylistPanel _panel; - - protected int _playid; + protected PlaylistPanel.PLE _playing; } diff --git a/projects/robodj/src/java/robodj/chooser/PlaylistPanel.java b/projects/robodj/src/java/robodj/chooser/PlaylistPanel.java index ec05a113..66cf070d 100644 --- a/projects/robodj/src/java/robodj/chooser/PlaylistPanel.java +++ b/projects/robodj/src/java/robodj/chooser/PlaylistPanel.java @@ -22,7 +22,7 @@ import robodj.util.ButtonUtil; public class PlaylistPanel extends ControlledPanel { - public static class PlaylistEntry + public static class PLE { public Entry entry; @@ -30,7 +30,7 @@ public class PlaylistPanel extends ControlledPanel public SongItem item; - public PlaylistEntry (Entry entry, Song song) + public PLE (Entry entry, Song song) { this.entry = entry; this.song = song; @@ -42,8 +42,6 @@ public class PlaylistPanel extends ControlledPanel } } - public static final String REFRESH = "refresh"; - public static final String CLEAR = "clear"; public static final String REMOVE_ALL = "remove_all"; @@ -80,8 +78,6 @@ public class PlaylistPanel extends ControlledPanel _clearbut = ButtonUtil.createControlButton( CLEAR_TIP, CLEAR, CLEAR_ICON_PATH); cbar.add(_clearbut); - cbar.add(ButtonUtil.createControlButton( - REFRESH_TIP, REFRESH, REFRESH_ICON_PATH)); cbar.add(ButtonUtil.createControlButton( SHUFFLE_TIP, SHUFFLE, SHUFFLE_ICON_PATH)); add(cbar, GroupLayout.FIXED); @@ -97,8 +93,6 @@ public class PlaylistPanel extends ControlledPanel public void addNotify () { super.addNotify(); - // load up the playlist - _controller.postAction(this, REFRESH); } // documentation inherited @@ -118,7 +112,7 @@ public class PlaylistPanel extends ControlledPanel _bpanel.revalidate(); } - protected void populatePlaylist (ArrayList plist, int playid) + protected void populatePlaylist (ArrayList plist) { // clear out any existing children _bpanel.removeAll(); @@ -128,10 +122,12 @@ public class PlaylistPanel extends ControlledPanel gl.setOffAxisPolicy(GroupLayout.EQUALIZE); gl.setOffAxisJustification(GroupLayout.LEFT); + int current = Chooser.djobj.playing; + // add buttons for every entry String title = null; - for (int i = 0; i < plist.size(); i++) { - PlaylistEntry entry = (PlaylistEntry)plist.get(i); + for (int ii = 0; ii < plist.size(); ii++) { + PLE entry = (PLE)plist.get(ii); JButton button; // add record/artist indicators when the record and artist @@ -155,13 +151,13 @@ public class PlaylistPanel extends ControlledPanel // create a song item to display this playlist entry entry.item = new SongItem(entry.song, SongItem.PLAYLIST); - entry.item.setIsPlaying(entry.song.songid == playid); - entry.item.extra = entry; + entry.item.setIsPlaying(current == ii); + entry.item.extra = new Integer(ii); _bpanel.add(entry.item); // let the bpanel know that we want to scroll the active track // label into place once we're all laid out - if (entry.song.songid == playid) { + if (current == ii) { _bpanel.setScrollTarget(entry.item); } } @@ -229,7 +225,6 @@ public class PlaylistPanel extends ControlledPanel // icon paths protected static final String ICON_ROOT = "/robodj/chooser/images/"; - protected static final String REFRESH_ICON_PATH = ICON_ROOT + "refresh.png"; protected static final String CLEAR_ICON_PATH = ICON_ROOT + "clear.png"; protected static final String SHUFFLE_ICON_PATH = ICON_ROOT + "shuffle.png"; protected static final String SKIPTO_ICON_PATH = ICON_ROOT + "skip.png"; @@ -239,7 +234,6 @@ public class PlaylistPanel extends ControlledPanel ICON_ROOT + "remove_song.png"; // button tips - protected static final String REFRESH_TIP = "Refresh the playlist"; protected static final String CLEAR_TIP = "Clear all songs from the playlist"; protected static final String SHUFFLE_TIP = diff --git a/projects/robodj/src/java/robodj/client/DJService.java b/projects/robodj/src/java/robodj/client/DJService.java new file mode 100644 index 00000000..76b4ddb1 --- /dev/null +++ b/projects/robodj/src/java/robodj/client/DJService.java @@ -0,0 +1,16 @@ +// +// $Id$ + +package robodj.client; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; + +/** + * Defines the main RoboDJ services. + */ +public interface DJService extends InvocationService +{ + /** Used to obtain the oid of the DJ object. */ + public void getDJOid (Client client, ResultListener listener); +} diff --git a/projects/robodj/src/java/robodj/data/DJMarshaller.java b/projects/robodj/src/java/robodj/data/DJMarshaller.java new file mode 100644 index 00000000..1dd4e7e1 --- /dev/null +++ b/projects/robodj/src/java/robodj/data/DJMarshaller.java @@ -0,0 +1,35 @@ +// +// $Id$ + +package robodj.data; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService.ResultListener; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.dobj.InvocationResponseEvent; +import robodj.client.DJService; + +/** + * Provides the implementation of the {@link DJService} interface + * that marshalls the arguments and delivers the request to the provider + * on the server. Also provides an implementation of the response listener + * interfaces that marshall the response arguments and deliver them back + * to the requesting client. + */ +public class DJMarshaller extends InvocationMarshaller + implements DJService +{ + /** The method id used to dispatch {@link #getDJOid} requests. */ + public static final int GET_DJOID = 1; + + // documentation inherited from interface + public void getDJOid (Client arg1, ResultListener arg2) + { + ResultMarshaller listener2 = new ResultMarshaller(); + listener2.listener = arg2; + sendRequest(arg1, GET_DJOID, new Object[] { + listener2 + }); + } + +} diff --git a/projects/robodj/src/java/robodj/data/DJObject.dobj b/projects/robodj/src/java/robodj/data/DJObject.dobj new file mode 100644 index 00000000..1b1e0452 --- /dev/null +++ b/projects/robodj/src/java/robodj/data/DJObject.dobj @@ -0,0 +1,128 @@ +// +// $Id$ + +package robodj.data; + +import com.samskivert.util.ArrayUtil; +import com.samskivert.util.ListUtil; + +import com.threerings.presents.dobj.DObject; +import com.threerings.presents.dobj.DSet; + +import robodj.Log; + +/** + * Contains the shared state of RoboDJ. + */ +public class DJObject extends DObject +{ + /** The current playlist. */ + public PlaylistEntry[] playlist; + + /** The index of the currently playing track. */ + public int playing = -1; + + /** + * Returns the currently playing track. + */ + public PlaylistEntry getPlaying () + { + return (playing < 0) ? null : getEntry(playing); + } + + /** + * Returns the playlist entry for the next song in the playlist. + */ + public PlaylistEntry getNext () + { + return getEntry(playing + 1); + } + + /** + * Returns the playlist entry with the current index or null if the + * index is not valid. + */ + public PlaylistEntry getEntry (int index) + { + return havePlaylist() ? playlist[index % playlist.length] : null; + } + + /** + * Plays the specified track in the current playlist. + */ + public void play (int index) + { + if (havePlaylist()) { + setPlaying(index % playlist.length); + } + } + + public void play () + { + if (havePlaylist()) { + // TODO: handle unpausing + if (playing == -1) { + setPlaying(0); + } + } + } + + public void skip () + { + if (havePlaylist()) { + play(playing + 1); + } + } + + public void back () + { + if (havePlaylist()) { + play(playing + playlist.length - 1); + } + } + + public void stop () + { + setPlaying(-1); + } + + public void pause () + { + // TODO + } + + public void remove (int start, int length) + { + if (havePlaylist()) { + setPlaylist((PlaylistEntry[])ArrayUtil.splice( + playlist, start, length)); + if (playing >= (start+length)) { + setPlaying(playing-length); + } else if (playing > start) { + setPlaying(start); + } + } + } + + public void shuffle () + { + if (havePlaylist()) { + PlaylistEntry entry = getPlaying(); + ArrayUtil.shuffle(playlist); + setPlaylist(playlist); + setPlaying(ListUtil.indexOf(playlist, entry)); + } + } + + public void clear () + { + setPlaying(-1); + setPlaylist(null); + } + + /** Helper function. */ + protected boolean havePlaylist () + { + return (playlist != null && playlist.length > 0); + } +} diff --git a/projects/robodj/src/java/robodj/data/DJObject.java b/projects/robodj/src/java/robodj/data/DJObject.java new file mode 100644 index 00000000..6f508fb2 --- /dev/null +++ b/projects/robodj/src/java/robodj/data/DJObject.java @@ -0,0 +1,176 @@ +// +// $Id$ + +package robodj.data; + +import com.samskivert.util.ArrayUtil; +import com.samskivert.util.ListUtil; + +import com.threerings.presents.dobj.DObject; +import com.threerings.presents.dobj.DSet; + +import robodj.Log; + +/** + * Contains the shared state of RoboDJ. + */ +public class DJObject extends DObject +{ + /** The field name of the playlist field. */ + public static final String PLAYLIST = "playlist"; + + /** The field name of the playing field. */ + public static final String PLAYING = "playing"; + + /** The current playlist. */ + public PlaylistEntry[] playlist; + + /** The index of the currently playing track. */ + public int playing = -1; + + /** + * Returns the currently playing track. + */ + public PlaylistEntry getPlaying () + { + return (playing < 0) ? null : getEntry(playing); + } + + /** + * Returns the playlist entry for the next song in the playlist. + */ + public PlaylistEntry getNext () + { + return getEntry(playing + 1); + } + + /** + * Returns the playlist entry with the current index or null if the + * index is not valid. + */ + public PlaylistEntry getEntry (int index) + { + return havePlaylist() ? playlist[index % playlist.length] : null; + } + + /** + * Plays the specified track in the current playlist. + */ + public void play (int index) + { + if (havePlaylist()) { + setPlaying(index % playlist.length); + } + } + + public void play () + { + if (havePlaylist()) { + // TODO: handle unpausing + if (playing == -1) { + setPlaying(0); + } + } + } + + public void skip () + { + if (havePlaylist()) { + play(playing + 1); + } + } + + public void back () + { + if (havePlaylist()) { + play(playing + playlist.length - 1); + } + } + + public void stop () + { + setPlaying(-1); + } + + public void pause () + { + // TODO + } + + public void remove (int start, int length) + { + if (havePlaylist()) { + setPlaylist((PlaylistEntry[])ArrayUtil.splice( + playlist, start, length)); + if (playing >= (start+length)) { + setPlaying(playing-length); + } else if (playing > start) { + setPlaying(start); + } + } + } + + public void shuffle () + { + if (havePlaylist()) { + PlaylistEntry entry = getPlaying(); + ArrayUtil.shuffle(playlist); + setPlaylist(playlist); + setPlaying(ListUtil.indexOf(playlist, entry)); + } + } + + public void clear () + { + setPlaying(-1); + setPlaylist(null); + } + + /** Helper function. */ + protected boolean havePlaylist () + { + return (playlist != null && playlist.length > 0); + } + + /** + * Requests that the playlist field be set to the specified + * value. The local value will be updated immediately and an event + * will be propagated through the system to notify all listeners that + * the attribute did change. Proxied copies of this object (on + * clients) will apply the value change when they received the + * attribute changed notification. + */ + public void setPlaylist (PlaylistEntry[] playlist) + { + requestAttributeChange(PLAYLIST, playlist); + this.playlist = playlist; + } + + /** + * Requests that the indexth element of + * playlist field be set to the specified value. The local + * value will be updated immediately and an event will be propagated + * through the system to notify all listeners that the attribute did + * change. Proxied copies of this object (on clients) will apply the + * value change when they received the attribute changed notification. + */ + public void setPlaylistAt (PlaylistEntry value, int index) + { + requestElementUpdate(PLAYLIST, value, index); + this.playlist[index] = value; + } + + /** + * Requests that the playing field be set to the specified + * value. The local value will be updated immediately and an event + * will be propagated through the system to notify all listeners that + * the attribute did change. Proxied copies of this object (on + * clients) will apply the value change when they received the + * attribute changed notification. + */ + public void setPlaying (int playing) + { + requestAttributeChange(PLAYING, new Integer(playing)); + this.playing = playing; + } +} diff --git a/projects/robodj/src/java/robodj/data/PlaylistEntry.java b/projects/robodj/src/java/robodj/data/PlaylistEntry.java new file mode 100644 index 00000000..35c3a060 --- /dev/null +++ b/projects/robodj/src/java/robodj/data/PlaylistEntry.java @@ -0,0 +1,71 @@ +// +// $Id$ + +package robodj.data; + +import com.threerings.io.SimpleStreamableObject; +import com.threerings.presents.dobj.DSet; + +import robodj.repository.Entry; +import robodj.repository.Song; + +/** + * Defines an entry currently in the playlist. + */ +public class PlaylistEntry extends SimpleStreamableObject + implements DSet.Entry +{ + /** The album with which we are associated. */ + public int entryId; + + /** The id of the song represented by this entry. */ + public int songId; + + /** The title of this entry (duplicated from the Song to simplify + * playlist display. */ + public String title; + + /** Zero argument constructor for streaming. */ + public PlaylistEntry () + { + } + + /** Creates a playlist entry from the specified entry and song. */ + public PlaylistEntry (Entry entry, Song song) + { + this(entry.entryid, song.songid, song.title); + } + + /** Creates a playlist entry from the specified information. */ + public PlaylistEntry (int entryId, int songId, String title) + { + this.entryId = entryId; + this.songId = songId; + this.title = title; + _key = entryId + ":" + songId; + } + + // documentation inherited + public Comparable getKey () + { + return _key; + } + + // documentation inherited + public boolean equals (Object other) + { + if (other instanceof PlaylistEntry) { + return _key.equals(((PlaylistEntry)other)._key); + } else { + return false; + } + } + + // documentation inherited + public int hashCode () + { + return _key.hashCode(); + } + + protected String _key; +} diff --git a/projects/robodj/src/java/robodj/importer/EntryUploader.java b/projects/robodj/src/java/robodj/importer/EntryUploader.java new file mode 100644 index 00000000..64cd2de8 --- /dev/null +++ b/projects/robodj/src/java/robodj/importer/EntryUploader.java @@ -0,0 +1,49 @@ +// +// $Id$ + +package robodj.importer; + +import java.util.ArrayList; +import java.util.Iterator; + +import java.awt.BorderLayout; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import robodj.repository.Entry; +import robodj.repository.Song; + +/** + * Displays an Entry and a list of similar entries and a check box. + */ +public class EntryUploader extends JPanel +{ + public EntryUploader (Entry entry, ArrayList similar) + { + super(new BorderLayout(5, 5)); + add(new JLabel(entry.artist + " - " + entry.title), + BorderLayout.CENTER); + add(_checked = new JCheckBox(), BorderLayout.WEST); + StringBuffer extra = new StringBuffer(""); + for (int ii = 0; ii < entry.songs.length; ii++) { + Song song = entry.songs[ii]; + extra.append(song.position).append(". "); + extra.append(song.title).append("
"); + } + if (similar.size() > 0) { + extra.append("

Similar entries:
"); + for (Iterator iter = similar.iterator(); iter.hasNext(); ) { + Entry siment = (Entry)iter.next(); + extra.append(siment.artist).append(" - ").append(siment.title); + extra.append("
"); + } + } + extra.append(""); + JLabel exlab = new JLabel(extra.toString()); + exlab.setFont(getFont().deriveFont(11)); + add(exlab, BorderLayout.SOUTH); + } + + protected JCheckBox _checked; +} diff --git a/projects/robodj/src/java/robodj/importer/FinishedPanel.java b/projects/robodj/src/java/robodj/importer/FinishedPanel.java index 7be69924..ebd68a70 100644 --- a/projects/robodj/src/java/robodj/importer/FinishedPanel.java +++ b/projects/robodj/src/java/robodj/importer/FinishedPanel.java @@ -16,7 +16,7 @@ public class FinishedPanel extends ImporterPanel implements ActionListener { - public FinishedPanel (Entry entry) + public FinishedPanel () { GroupLayout gl = new HGroupLayout(GroupLayout.STRETCH); gl.setOffAxisPolicy(GroupLayout.STRETCH); @@ -35,7 +35,7 @@ public class FinishedPanel // create our action buttons String[] labels = new String[] - { "Import another CD", "Edit/categorize CDs" }; + { "Import more music", "Edit/categorize CDs" }; String[] actions = new String[] { "import", "edit" }; for (int i = 0; i < labels.length; i++) { @@ -47,9 +47,6 @@ public class FinishedPanel // add our panel to the main group add(ppanel); - - // save this guy for later - _entry = entry; } public void wasAddedToFrame (ImporterFrame frame, boolean popped) @@ -68,7 +65,7 @@ public class FinishedPanel System.exit(0); } else if (cmd.equals("import")) { - _frame.setPanel(new InsertCDPanel()); + _frame.setPanel(new StartPanel()); } else { System.out.println("Unknown action event: " + cmd); @@ -76,5 +73,4 @@ public class FinishedPanel } protected ImporterFrame _frame; - protected Entry _entry; } diff --git a/projects/robodj/src/java/robodj/importer/ImportMusicPanel.java b/projects/robodj/src/java/robodj/importer/ImportMusicPanel.java new file mode 100644 index 00000000..0d31e72f --- /dev/null +++ b/projects/robodj/src/java/robodj/importer/ImportMusicPanel.java @@ -0,0 +1,329 @@ +// +// $Id: ImportMusicPanel.java,v 1.7 2003/05/04 18:16:07 mdb Exp $ + +package robodj.importer; + +import java.io.File; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.*; + +import com.samskivert.net.cddb.CDDB; +import com.samskivert.swing.*; +import com.samskivert.swing.util.*; +import com.samskivert.util.ArrayUtil; +import com.samskivert.util.HashIntMap; +import com.samskivert.util.StringUtil; + +import org.farng.mp3.*; +import org.farng.mp3.id3.*; + +import robodj.convert.*; +import robodj.repository.*; + +public class ImportMusicPanel extends ImporterPanel + implements ActionListener, TaskObserver +{ + public ImportMusicPanel () + { + GroupLayout gl = new HGroupLayout(GroupLayout.STRETCH); + gl.setOffAxisPolicy(GroupLayout.STRETCH); + setLayout(gl); + + // create our label for the left hand side + JLabel cdlabel = new JLabel("Import Music", JLabel.CENTER); + cdlabel.setFont(new Font("Helvetica", Font.BOLD, 24)); + add(cdlabel, GroupLayout.FIXED); + + // on the right hand side we want a status display to let the user + // know how the ID3 reading process is going + GroupLayout vgl = new VGroupLayout(GroupLayout.STRETCH); + vgl.setOffAxisPolicy(GroupLayout.STRETCH); + _spanel = new JPanel(vgl); + + JLabel sl = new JLabel("Status", JLabel.LEFT); + _spanel.add(sl, GroupLayout.FIXED); + + // set up our status display + _status.setText("Ready to import music...\n"); + _spanel.add(new JScrollPane(_status)); + + // add our panel to the main group + add(_spanel); + } + + public void wasAddedToFrame (ImporterFrame frame, boolean popped) + { + // keep this for later + _frame = frame; + + frame.addControlButton("Cancel", "cancel", this); + frame.addControlButton("Back", "back", this); + _next = frame.addControlButton("Next...", "next", this); + _next.setEnabled(popped); + + if (popped) { + // clear out our song arrays + for (Iterator iter = _byid.values().iterator(); iter.hasNext(); ) { + ((Entry)iter.next()).songs = null; + } + + } else { + // create a file chooser dialog + JFileChooser chooser = new JFileChooser(); + chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); + chooser.setMultiSelectionEnabled(true); + chooser.setDialogTitle("Select files and directories to import..."); + + int rv = chooser.showDialog(this, "Import music"); + if (rv == JFileChooser.APPROVE_OPTION) { + // start scanning those directories for MP3 files + final File[] files = chooser.getSelectedFiles(); + TaskMaster.invokeTask("scan", new TaskAdapter() { + public Object invoke () throws Exception { + return scanForFiles(files); + } + }, this); + + } else { + // pop back to the last panel + _frame.popPanel(); + } + } + } + + public void taskCompleted (String name, Object result) + { + if (name.equals("scan")) { + final File[] files = (File[])result; + // now fire up a task to read the ID3 info from these files + TaskMaster.invokeTask("readid3", new TaskAdapter() { + public Object invoke () throws Exception { + readID3(files); + return null; + } + }, this); + + } else if (name.equals("readid3")) { + // replace the info text with the track info table + remove(_spanel); + JPanel bits = new JPanel(new BorderLayout(5, 5)); + bits.add(new JLabel("Edit metadata"), BorderLayout.NORTH); + JTable table = new JTable( + new SongTableModel(_byid, _byname, _songs)); + bits.add(new JScrollPane(table), BorderLayout.CENTER); + bits.add(new JLabel("You will select which albums to upload " + + "on the next page..."), BorderLayout.SOUTH); + add(bits); + SwingUtil.sizeToContents(table); + + // enable the next button + _next.setEnabled(true); + + // relay everything out + validate(); + } + } + + public void taskFailed (String name, Throwable exception) + { + if (name.equals("scan")) { + _status.append("\nUnable to scan for MP3 files:\n\n" + + exception.getMessage()); + + } else if (name.equals("readid3")) { + _status.append("\nFailed while reading ID3 tags:\n\n" + + exception.getMessage()); + } + } + + public void actionPerformed (ActionEvent e) + { + String cmd = e.getActionCommand(); + if (cmd.equals("cancel")) { + System.exit(0); + + } else if (cmd.equals("back")) { + // pop back to the last panel + _frame.popPanel(); + + } else if (cmd.equals("next")) { + entrifyAndContinue(); + + } else { + System.out.println("Unknown action event: " + cmd); + } + } + + /** + * Scans the supplied set of files and directories for all files that + * appear to be MP3s. + * + * @return an array of all matched files. + */ + protected File[] scanForFiles (File[] seeds) + { + ArrayList matches = new ArrayList(); + scanForFiles(seeds, matches); + return (File[])matches.toArray(new File[matches.size()]); + } + + protected void scanForFiles (File[] seeds, ArrayList matches) + { + for (int ii = 0; ii < seeds.length; ii++) { + if (seeds[ii].isDirectory()) { + postAsyncStatus("Scanning " + seeds[ii] + "..."); + scanForFiles(seeds[ii].listFiles(), matches); + } else if (seeds[ii].getName().toLowerCase().endsWith(".mp3")) { + matches.add(seeds[ii]); + } + } + } + + protected void readID3 (File[] files) + { + for (int ii = 0; ii < files.length; ii++) { + try { + MP3File mpf = new MP3File(files[ii]); + String title = getID3Info(mpf, "TIT2", "title"); + String artist = getID3Info(mpf, "TPE1", "artist"); + String album = getID3Info(mpf, "TALB", "album"); + String track = getID3Info(mpf, "TRCK", ""); + + // locate the Entry associated with this album/artist, + // creating a new one if necessary + String key = SongTableModel.getKey(album, artist); + Entry entry = (Entry)_byname.get(key); + if (entry == null) { + entry = new Entry(); + entry.entryid = _byname.size(); // assign a temporary id + entry.artist = nullToUnknown(artist); + entry.title = nullToUnknown(album); + entry.source = "import"; + _byname.put(key, entry); + _byid.put(entry.entryid, entry); + postAsyncStatus("Located album '" + album + "' by '" + + artist + "'."); + } + + Song song = new Song(); + song.entryid = entry.entryid; + song.title = StringUtil.blank(title) ? + stripSuffix(files[ii].getName()) : title; + song.location = files[ii].toString(); + + // try to figure out what the track number is + if (StringUtil.blank(track)) { + Matcher m = _trackNo.matcher(files[ii].getName()); + if (m.matches()) { + track = m.group(1); + } + } + try { + song.position = Integer.parseInt(track); + } catch (Throwable t) { + song.position = 1; + } + + // finally slap that song on the list + _songs.add(song); + postAsyncStatus("Located song '" + title + "' " + + "(" + song.position + ")."); + + } catch (Exception e) { + postAsyncStatus("Failure processing " + files[ii] + ": " + e); + e.printStackTrace(System.err); + } + } + } + + protected String nullToUnknown (String value) + { + return StringUtil.blank(value) ? "Unknown" : value; + } + + protected String stripSuffix (String value) + { + int didx = value.lastIndexOf("."); + return (didx == -1) ? value : value.substring(0, didx); + } + + protected String getID3Info (MP3File file, String v2tag, String v1tag) + { + // first look for the v2 tag + AbstractID3v2 tag2 = file.getID3v2Tag(); + String value = null; + if (tag2 != null) { + AbstractMP3Fragment frame = tag2.getFrame(v2tag); + if (frame != null) { + value = ((AbstractFrameBodyTextInformation) + frame.getBody()).getText(); + } + if (!StringUtil.blank(value)) { + return value; + } + } + + // if that fails, try the v1 tag + ID3v1 tag1 = file.getID3v1Tag(); + if (tag1 != null) { + if (v1tag.equals("album")) { + return tag1.getAlbum(); + } else if (v1tag.equals("artist")) { + return tag1.getArtist(); + } else if (v1tag.equals("title")) { + return tag1.getTitle(); + } + } + + return null; + } + + protected void entrifyAndContinue () + { + // go through our songs and collect them into their Entry objects + ArrayList elist = new ArrayList(); + for (Iterator iter = _songs.iterator(); iter.hasNext(); ) { + Song song = (Song)iter.next(); + Entry entry = (Entry)_byid.get(song.entryid); + if (entry.songs == null) { + entry.songs = new Song[] { song }; + } else { + entry.songs = (Song[])ArrayUtil.append(entry.songs, song); + } + if (!elist.contains(entry)) { + elist.add(entry); + } + } + + // create the upload panel and pass the entries along + Entry[] entries = (Entry[])elist.toArray(new Entry[elist.size()]); + _frame.pushPanel(new UploadMusicPanel(entries)); + } + + protected ImporterFrame _frame; + protected JButton _next; + protected JPanel _spanel; + + protected HashMap _byname = new HashMap(); + protected HashIntMap _byid = new HashIntMap(); + protected ArrayList _songs = new ArrayList(); + + protected static Pattern _trackNo; + static { + try { + _trackNo = Pattern.compile("\\s(\\d+)\\s"); + } catch (Throwable t) { + t.printStackTrace(System.err); + } + } +} diff --git a/projects/robodj/src/java/robodj/importer/Importer.java b/projects/robodj/src/java/robodj/importer/Importer.java index ac040733..3cb1875f 100644 --- a/projects/robodj/src/java/robodj/importer/Importer.java +++ b/projects/robodj/src/java/robodj/importer/Importer.java @@ -67,11 +67,10 @@ public class Importer // create our frame and first panel ImporterFrame frame = new ImporterFrame(); - InsertCDPanel panel = new InsertCDPanel(); - frame.pushPanel(panel); + frame.pushPanel(new StartPanel()); // center the frame in the screen and show it - frame.setSize(640, 480); + frame.setSize(1024, 600); SwingUtil.centerWindow(frame); frame.setVisible(true); } diff --git a/projects/robodj/src/java/robodj/importer/ImporterPanel.java b/projects/robodj/src/java/robodj/importer/ImporterPanel.java index 01437769..6f257e81 100644 --- a/projects/robodj/src/java/robodj/importer/ImporterPanel.java +++ b/projects/robodj/src/java/robodj/importer/ImporterPanel.java @@ -3,18 +3,28 @@ package robodj.importer; +import java.awt.BorderLayout; import java.awt.LayoutManager; +import java.awt.Rectangle; + import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; public abstract class ImporterPanel extends JPanel { public ImporterPanel () { + this(new BorderLayout()); } public ImporterPanel (LayoutManager layout) { super(layout); + + _status = new JTextArea(); + _status.setLineWrap(true); + _status.setEditable(false); } /** @@ -26,4 +36,24 @@ public abstract class ImporterPanel extends JPanel * than added to the frame for the first time. */ public abstract void wasAddedToFrame (ImporterFrame frame, boolean popped); + + /** + * Displays the supplied string in the status field and scrolls the + * status field to the bottom. + */ + protected void postAsyncStatus (final String status) + { + SwingUtilities.invokeLater(new Runnable() { + public void run () + { + _status.append(status + "\n"); + Rectangle bot = _status.getBounds(); + bot.y = bot.height-1; + bot.height = 1; + _status.scrollRectToVisible(bot); + } + }); + } + + protected JTextArea _status; } diff --git a/projects/robodj/src/java/robodj/importer/RipPanel.java b/projects/robodj/src/java/robodj/importer/RipPanel.java index b8a24a34..0a16193f 100644 --- a/projects/robodj/src/java/robodj/importer/RipPanel.java +++ b/projects/robodj/src/java/robodj/importer/RipPanel.java @@ -60,11 +60,8 @@ public class RipPanel // create the status log JLabel sl = new JLabel("Status", JLabel.LEFT); ppanel.add(sl, GroupLayout.FIXED); - _statusLog = new JTextArea(); - _statusLog.setLineWrap(true); - _statusLog.setEditable(false); // make something for it to scroll around in - ppanel.add(new JScrollPane(_statusLog)); + ppanel.add(new JScrollPane(_status)); // add our panel to the main group add(ppanel); @@ -74,16 +71,6 @@ public class RipPanel _entry = entry; } - protected void postAsyncStatus (final String status) - { - SwingUtilities.invokeLater(new Runnable() { - public void run () - { - _statusLog.append(status + "\n"); - } - }); - } - protected void postAsyncProgress (final int overallComplete, final int taskComplete) { @@ -390,7 +377,7 @@ public class RipPanel } else { msg = exception.toString(); } - _statusLog.append("Error: " + msg + "\n"); + _status.append("Error: " + msg + "\n"); Log.logStackTrace(exception); } @@ -401,7 +388,7 @@ public class RipPanel System.exit(0); } else if (cmd.equals("next")) { - _frame.pushPanel(new FinishedPanel(_entry)); + _frame.pushPanel(new FinishedPanel()); } else { System.out.println("Unknown action event: " + cmd); @@ -424,7 +411,6 @@ public class RipPanel protected JProgressBar _oprogress; protected JProgressBar _progress; - protected JTextArea _statusLog; protected Ripper.TrackInfo[] _info; protected Entry _entry; diff --git a/projects/robodj/src/java/robodj/importer/SongTableModel.java b/projects/robodj/src/java/robodj/importer/SongTableModel.java new file mode 100644 index 00000000..056682c2 --- /dev/null +++ b/projects/robodj/src/java/robodj/importer/SongTableModel.java @@ -0,0 +1,191 @@ +// +// $Id$ + +package robodj.importer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; + +import javax.swing.table.AbstractTableModel; + +import com.samskivert.util.HashIntMap; +import com.samskivert.util.QuickSort; + +import robodj.repository.Entry; +import robodj.repository.Song; + +/** + * Allows the display and editing of a big list of songs. + */ +public class SongTableModel extends AbstractTableModel +{ + /** + * Creates a song table model with the supplied set of entries and + * songs. + */ + public SongTableModel (HashIntMap byid, HashMap byname, ArrayList songs) + { + _byid = byid; + _byname = byname; + _songs = songs; + QuickSort.sort(_songs, SONG_COMP); + } + + // documentation inherited + public Class getColumnClass (int column) + { + return CLASSES[column]; + } + + // documentation inherited + public String getColumnName (int column) + { + return COLUMNS[column]; + } + + // documentation inherited + public int getRowCount() + { + return _songs.size(); + } + + // documentation inherited + public int getColumnCount() + { + return COLUMNS.length; + } + + // documentation inherited + public Object getValueAt (int row, int column) { + Song song = (Song)_songs.get(row); + Entry entry = (Entry)_byid.get(song.entryid); + + switch (column) { + case 0: return entry.artist; + case 1: return entry.title; + case 2: return song.title; + case 3: return new Integer(song.position); + default: return ""; + } + } + + // documentation inherited + public boolean isCellEditable (int row, int column) { + return true; + } + + // documentation inherited + public void setValueAt (Object value, int row, int column) { + Song song = (Song)_songs.get(row); + + switch (column) { + case 0: remapSong(song, null, (String)value); break; + case 1: remapSong(song, (String)value, null); break; + case 2: song.title = (String)value; return; // avoid fire..Changed + case 3: song.position = ((Integer)value).intValue(); break; + default: + System.err.println("Refusing to update bogus column [row=" + row + + ", column=" + column + ", val=" + value + "]."); + break; + } + + QuickSort.sort(_songs, SONG_COMP); + fireTableDataChanged(); + } + + /** Updates the album title or artist for the supplied song. */ + protected void remapSong (Song song, String album, String artist) + { + Entry entry = (Entry)_byid.get(song.entryid); + + // fill in any unknown data + if (album == null) { + album = entry.title; + } + if (artist == null) { + artist = entry.artist; + } + String key = getKey(album, artist); + System.err.println("Considering " + key); + + // if neither album or artist are "Unknown", and the new name does + // not already exist as an entry, then just update this entry + if (!entry.title.equals("Unknown") && !entry.artist.equals("Unknown") && + !_byname.containsKey(key)) { + _byname.remove(getKey(entry)); // remove the old key + entry.title = (album == null) ? entry.title : album; + entry.artist = (artist == null) ? entry.artist : artist; + _byname.put(key, entry); // map with the new key + return; + } + + // look up (or create) a new entry + Entry nentry = (Entry)_byname.get(key); + if (nentry == null) { + nentry = clone(entry); + nentry.title = album; + nentry.artist = artist; + nentry.entryid = _byname.size(); + _byname.put(getKey(nentry), nentry); + _byid.put(nentry.entryid, nentry); + } + song.entryid = nentry.entryid; + System.err.println("Remapped " + song); + } + + /** Duplicates the supplied entry. */ + protected Entry clone (Entry source) + { + Entry nentry = new Entry(); + nentry.title = source.title; + nentry.artist = source.artist; + nentry.source = source.source; + return nentry; + } + + /** Creates a key from the entry title and artist. */ + protected static String getKey (Entry entry) + { + return getKey(entry.title, entry.artist); + } + + /** Creates a key from the entry title and artist. */ + protected static String getKey (String title, String artist) + { + return title + ":" + artist; + } + + protected HashMap _byname = new HashMap(); + protected HashIntMap _byid = new HashIntMap(); + protected ArrayList _songs; + + protected Comparator SONG_COMP = new Comparator() { + public int compare (Object a, Object b) { + Song sa = (Song)a, sb = (Song)b; + Entry ea = (Entry)_byid.get(sa.entryid); + Entry eb = (Entry)_byid.get(sb.entryid); + int comp; + if ((comp = ea.artist.compareTo(eb.artist)) != 0) { + return comp; + } + if ((comp = ea.title.compareTo(eb.title)) != 0) { + return comp; + } + if (sa.position != sb.position) { + return sa.position - sb.position; + } + return sa.title.compareTo(sb.title); + } + }; + + protected static final String[] COLUMNS = { + "Artist", "Album", "Title", "#" + }; + + protected static final Class[] CLASSES = { + String.class, String.class, String.class, Integer.class + }; +} diff --git a/projects/robodj/src/java/robodj/importer/InsertCDPanel.java b/projects/robodj/src/java/robodj/importer/StartPanel.java similarity index 51% rename from projects/robodj/src/java/robodj/importer/InsertCDPanel.java rename to projects/robodj/src/java/robodj/importer/StartPanel.java index a2cedb5d..37286609 100644 --- a/projects/robodj/src/java/robodj/importer/InsertCDPanel.java +++ b/projects/robodj/src/java/robodj/importer/StartPanel.java @@ -1,19 +1,22 @@ // -// $Id: InsertCDPanel.java,v 1.3 2002/03/03 21:17:03 mdb Exp $ +// $Id: StartPanel.java,v 1.3 2002/03/03 21:17:03 mdb Exp $ package robodj.importer; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; import java.net.URL; + import javax.swing.*; -public class InsertCDPanel - extends ImporterPanel +import com.samskivert.swing.VGroupLayout; + +public class StartPanel extends ImporterPanel implements ActionListener { - public InsertCDPanel () + public StartPanel () { super(new GridLayout(1, 2)); @@ -24,10 +27,21 @@ public class InsertCDPanel JLabel cdlabel = new JLabel(cdicon); add(cdlabel); - // create the "insert cd" text label for the right hand side - JLabel ilabel = new JLabel("Please insert the CD...", - JLabel.CENTER); - add(ilabel); + // allow the user to select whether they want to rip a CD or + // import music directly + VGroupLayout vgl = new VGroupLayout(); + vgl.setOffAxisJustification(VGroupLayout.LEFT); + JPanel bpanel = new JPanel(vgl); + bpanel.add(_rip = new JRadioButton("Rip a CD (insert CD now)")); + _rip.setMnemonic(KeyEvent.VK_R); + _rip.setSelected(true); + bpanel.add(_imp = new JRadioButton("Import music")); + _imp.setMnemonic(KeyEvent.VK_M); + add(bpanel); + + ButtonGroup group = new ButtonGroup(); + group.add(_imp); + group.add(_rip); // give ourselves a wee bit of a border setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); @@ -49,12 +63,17 @@ public class InsertCDPanel System.exit(0); } else if (cmd.equals("next")) { - _frame.pushPanel(new CDDBLookupPanel()); + if (_rip.isSelected()) { + _frame.pushPanel(new CDDBLookupPanel()); + } else { + _frame.pushPanel(new ImportMusicPanel()); + } } else { System.out.println("Unknown action event: " + cmd); } } + protected JRadioButton _imp, _rip; protected ImporterFrame _frame; } diff --git a/projects/robodj/src/java/robodj/importer/UploadMusicPanel.java b/projects/robodj/src/java/robodj/importer/UploadMusicPanel.java new file mode 100644 index 00000000..6c713a47 --- /dev/null +++ b/projects/robodj/src/java/robodj/importer/UploadMusicPanel.java @@ -0,0 +1,191 @@ +// +// $Id: UploadMusicPanel.java,v 1.7 2003/05/04 18:16:07 mdb Exp $ + +package robodj.importer; + +import java.io.File; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.*; + +import com.samskivert.net.cddb.CDDB; +import com.samskivert.swing.*; +import com.samskivert.swing.util.*; +import com.samskivert.util.ArrayUtil; +import com.samskivert.util.HashIntMap; +import com.samskivert.util.StringUtil; + +import org.farng.mp3.*; +import org.farng.mp3.id3.*; + +import robodj.convert.*; +import robodj.repository.*; +import com.samskivert.util.CollectionUtil; + +public class UploadMusicPanel extends ImporterPanel + implements ActionListener, TaskObserver +{ + public UploadMusicPanel (Entry[] entries) + { + GroupLayout gl = new HGroupLayout(GroupLayout.STRETCH); + gl.setOffAxisPolicy(GroupLayout.STRETCH); + setLayout(gl); + + // save these for later + _entries = entries; + + // create our label for the left hand side + JLabel cdlabel = new JLabel("Upload Music", JLabel.CENTER); + cdlabel.setFont(new Font("Helvetica", Font.BOLD, 24)); + add(cdlabel, GroupLayout.FIXED); + + // display status on looking up similar albums + GroupLayout vgl = new VGroupLayout(GroupLayout.STRETCH); + vgl.setOffAxisPolicy(GroupLayout.STRETCH); + _spanel = new JPanel(vgl); + + JLabel sl = new JLabel("Status", JLabel.LEFT); + _spanel.add(sl, GroupLayout.FIXED); + + _status.setText("Checking for similar music...\n"); + _spanel.add(new JScrollPane(_status)); + + // add our panel to the main group + add(_spanel); + } + + public void wasAddedToFrame (ImporterFrame frame, boolean popped) + { + // keep this for later + _frame = frame; + + frame.addControlButton("Cancel", "cancel", this); + _back = frame.addControlButton("Back", "back", this); + _next = frame.addControlButton("Upload", "upload", this); + + // fire up the similarity checker task + TaskMaster.invokeTask("checkrepo", new TaskAdapter() { + public Object invoke () throws Exception { + checkForSimilar(); + return null; + } + }, this); + } + + public void taskCompleted (String name, Object result) + { + if (name.equals("checkrepo")) { + // display the entries and their similarity matches + remove(_spanel); + JPanel panel = GroupLayout.makeVBox( + GroupLayout.NONE, GroupLayout.TOP, GroupLayout.EQUALIZE); + ((GroupLayout)panel.getLayout()).setOffAxisJustification( + GroupLayout.LEFT); + panel.add(new JLabel("Select the albums to upload...")); + panel.add(new JSeparator()); + for (int ii = 0; ii < _entries.length; ii++) { + panel.add(new EntryUploader( + _entries[ii], (ArrayList) + _similar.get(_entries[ii].entryid))); + } + add(new JScrollPane(panel)); + + // enable the next button + _next.setEnabled(true); + + // relay everything out + validate(); + + } else if (name.equals("upload")) { + _next.setEnabled(true); + _next.setActionCommand("next"); + _next.setText("Next..."); + } + } + + public void taskFailed (String name, Throwable exception) + { + if (name.equals("checkrepo")) { + _status.append("\nUnable to check repository for " + + "similar music:\n\n" + exception.getMessage()); + + } else if (name.equals("readid3")) { + _status.append("\nFailed while reading ID3 tags:\n\n" + + exception.getMessage()); + } + } + + public void actionPerformed (ActionEvent e) + { + String cmd = e.getActionCommand(); + if (cmd.equals("cancel")) { + System.exit(0); + + } else if (cmd.equals("back")) { + // pop back to the last panel + _frame.popPanel(); + + } else if (cmd.equals("upload")) { + _next.setEnabled(false); + _back.setEnabled(false); + TaskMaster.invokeTask("upload", new TaskAdapter() { + public Object invoke () throws Exception { + performUpload(); + return null; + } + }, this); + + } else if (cmd.equals("next")) { + _frame.pushPanel(new FinishedPanel()); + + } else { + System.out.println("Unknown action event: " + cmd); + } + } + + protected void checkForSimilar () + { + for (int ii = 0; ii < _entries.length; ii++) { + Entry entry = _entries[ii]; + postAsyncStatus("Checking for music like '" + entry.title + + "' - '" +entry.artist + "'."); + ArrayList similar = new ArrayList(); + try { + String eartist = StringUtil.replace(entry.artist, "'", "\\'"); + Entry[] artist = + Importer.repository.getEntries( + "where soundex(artist) = soundex('" + eartist + "')"); + CollectionUtil.addAll(similar, artist); + String etitle = StringUtil.replace(entry.title, "'", "\\'"); + Entry[] title = + Importer.repository.getEntries( + "where soundex(title) = soundex('" + etitle + "')"); + CollectionUtil.addAll(similar, title); + _similar.put(entry.entryid, similar); + + } catch (Exception e) { + postAsyncStatus("Choked checking repository: " + e); + } + } + } + + protected void performUpload () + { + } + + protected ImporterFrame _frame; + protected JButton _next, _back; + protected JPanel _spanel; + + protected Entry[] _entries; + protected HashIntMap _similar = new HashIntMap(); +} diff --git a/projects/robodj/src/java/robodj/server/DJDispatcher.java b/projects/robodj/src/java/robodj/server/DJDispatcher.java new file mode 100644 index 00000000..2c6635ac --- /dev/null +++ b/projects/robodj/src/java/robodj/server/DJDispatcher.java @@ -0,0 +1,52 @@ +// +// $Id$ + +package robodj.server; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService.ResultListener; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.server.InvocationDispatcher; +import com.threerings.presents.server.InvocationException; +import robodj.client.DJService; +import robodj.data.DJMarshaller; + +/** + * Dispatches requests to the {@link DJProvider}. + */ +public class DJDispatcher extends InvocationDispatcher +{ + /** + * Creates a dispatcher that may be registered to dispatch invocation + * service requests for the specified provider. + */ + public DJDispatcher (DJProvider provider) + { + this.provider = provider; + } + + // documentation inherited + public InvocationMarshaller createMarshaller () + { + return new DJMarshaller(); + } + + // documentation inherited + public void dispatchRequest ( + ClientObject source, int methodId, Object[] args) + throws InvocationException + { + switch (methodId) { + case DJMarshaller.GET_DJOID: + ((DJProvider)provider).getDJOid( + source, + (ResultListener)args[0] + ); + return; + + default: + super.dispatchRequest(source, methodId, args); + } + } +} diff --git a/projects/robodj/src/java/robodj/server/DJProvider.java b/projects/robodj/src/java/robodj/server/DJProvider.java new file mode 100644 index 00000000..7de0dec1 --- /dev/null +++ b/projects/robodj/src/java/robodj/server/DJProvider.java @@ -0,0 +1,24 @@ +// +// $Id$ + +package robodj.server; + +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.server.InvocationException; +import com.threerings.presents.server.InvocationProvider; + +import robodj.client.DJService; + +/** + * Implements the main RoboDJ services. + */ +public class DJProvider implements InvocationProvider +{ + /** Used to obtain the oid of the DJ object. */ + public void getDJOid (ClientObject caller, + DJService.ResultListener listener) + throws InvocationException + { + listener.requestProcessed(new Integer(MusicDaemon.djobj.getOid())); + } +} diff --git a/projects/robodj/src/java/robodj/server/MusicDaemon.java b/projects/robodj/src/java/robodj/server/MusicDaemon.java new file mode 100644 index 00000000..2e194508 --- /dev/null +++ b/projects/robodj/src/java/robodj/server/MusicDaemon.java @@ -0,0 +1,105 @@ +// +// $Id$ + +package robodj.server; + +import com.samskivert.io.PersistenceException; +import com.samskivert.jdbc.StaticConnectionProvider; +import com.samskivert.util.StringUtil; + +import com.threerings.presents.dobj.DObject; +import com.threerings.presents.dobj.ObjectAccessException; +import com.threerings.presents.dobj.Subscriber; +import com.threerings.presents.server.PresentsServer; + +import robodj.Log; +import robodj.data.DJObject; +import robodj.repository.Repository; +import robodj.util.ErrorUtil; +import robodj.util.RDJPrefs; +import robodj.util.RDJPrefsPanel; + +/** + * The main entry point for the music playing daemon. + */ +public class MusicDaemon extends PresentsServer +{ + /** Provides access to the main RoboDJ services. */ + public static DJProvider djprov = new DJProvider(); + + /** The entity that actually plays the music in the playlist. */ + public static MusicPlayer player = new MusicPlayer(); + + /** Our shared server state. */ + public static DJObject djobj; + + /** Our music repository. */ + public static Repository repo; + + // documentation inherited + public void init () + throws Exception + { + super.init(); + + // loop until the user provides us with a configuration that works + // or requests to exit + for (int ii = 0; ii < 100; ii++) { + String repodir = RDJPrefs.getRepositoryDirectory(); + if (StringUtil.blank(repodir) || ii > 0) { + // display the initial configuration wizard if we are not + // yet properly configured + RDJPrefsPanel.display(true); + } + + // create our repository + try { + repo = new Repository( + new StaticConnectionProvider(RDJPrefs.getJDBCConfig())); + + } catch (PersistenceException pe) { + String errmsg = "Unable to communicate with database:"; + if (ErrorUtil.reportError(errmsg, pe)) { + System.exit(-1); + } + continue; + } + + break; + } + + // create and register our providers + invmgr.registerDispatcher(new DJDispatcher(djprov), true); + + // create our DJ object + omgr.createObject(DJObject.class, new Subscriber() { + public void objectAvailable (DObject object) { + djobj = (DJObject)object; + player.init(djobj); + Log.info("Music daemon ready [oid=" + djobj.getOid() + "]."); + } + public void requestFailed (int oid, ObjectAccessException cause) { + Log.warning("Failed to create DJ object: " + cause); + } + }); + } + + public static void main (String[] args) + { + Log.info("RoboDJ music server starting..."); + + MusicDaemon server = new MusicDaemon(); + try { + // initialize the server + server.init(); + // start the server to running (this method call won't return + // until the server is shut down) + server.run(); + + } catch (Exception e) { + Log.warning("Unable to initialize server."); + Log.logStackTrace(e); + System.exit(-1); + } + } +} diff --git a/projects/robodj/src/java/robodj/server/MusicPlayer.java b/projects/robodj/src/java/robodj/server/MusicPlayer.java new file mode 100644 index 00000000..506a46a1 --- /dev/null +++ b/projects/robodj/src/java/robodj/server/MusicPlayer.java @@ -0,0 +1,189 @@ +// +// $Id$ + +package robodj.server; + +import java.io.FileInputStream; + +import javazoom.jl.player.Player; + +import com.threerings.presents.dobj.AttributeChangeListener; +import com.threerings.presents.dobj.AttributeChangedEvent; + +import robodj.Log; +import robodj.data.DJObject; +import robodj.data.PlaylistEntry; +import robodj.repository.Entry; +import robodj.repository.Song; + +/** + * Handles the actual playing of music in the playlist. + */ +public class MusicPlayer extends Thread + implements AttributeChangeListener +{ + public MusicPlayer () + { + // start up the music playing thread + setDaemon(true); + start(); + } + + public void init (DJObject djobj) + { + _djobj = djobj; + _djobj.addListener(this); + } + + // documentation inherited from interface + public void attributeChanged (AttributeChangedEvent event) + { + if (event.getName().equals(DJObject.PLAYING)) { + playEntry(_djobj.getPlaying()); + recomputeNext(); + + } else if (event.getName().equals(DJObject.PLAYLIST)) { + recomputeNext(); + } + } + + public void run () + { + while (true) { + try { + // this will block if there's nothing to play + Player player = getPlaying(); + Log.info("Playing " + _pentry + "..."); + while (player.play(PLAY_FRAMES)) { + // loop! + } + + } catch (Throwable t) { + Log.warning("Music player choked."); + Log.logStackTrace(t); + } + } + } + + /** + * Configures the specified playlist entry as the currently playing + * entry. Even if we are currently playing the same entry, we will + * restart play from the beginning. + */ + protected synchronized void playEntry (PlaylistEntry entry) + { + // if we're already playing what we want to be playing, stop + if (equals(_pentry, entry)) { + return; + } + + Log.info("Forcing play [entry=" + entry + "]."); + + // queue up the next track + _fnentry = entry; + _fnext = getPlayer(_fnentry); + + // clear out any current track + if (_playing != null) { + Log.info("Stopping current track."); + _playing.close(); + } + + // finally wake up the player in case it is sleeping + notify(); + } + + protected synchronized void recomputeNext () + { + // if we're properly wired up, NOOP + PlaylistEntry nentry = _djobj.getNext(); + if (equals(_nentry, nentry)) { + return; + } + + // clear things out in preparation + if (_next != null) { + _next.close(); + _next = null; + } + + // compute and load up info on the next playing song + _nentry = nentry; + Log.info("Queuing up " + _nentry + "..."); + _next = getPlayer(_nentry); + + // wake up the music player if necessary + notify(); + } + + protected Player getPlayer (PlaylistEntry entry) + { + if (entry == null) { + return null; + } + + Song song = null; + try { + Entry album = MusicDaemon.repo.getEntry(entry.entryId); + if (album != null) { + song = album.getSong(entry.songId); + } + if (album == null || song == null) { + Log.warning("Bogus playlist entry! [entry=" + entry + + ", album=" + album + ", song=" + song + "]."); + return null; + } + return new Player(new FileInputStream(song.location)); + + } catch (Exception e) { + Log.warning("Failed to load song [pentry=" + entry + + ", song=" + song + "]."); + Log.logStackTrace(e); + return null; + } + } + + protected synchronized Player getPlaying () + { + while (_next == null && _fnext == null) { + try { + Log.info("Waiting for track..."); + wait(); + } catch (InterruptedException ie) { + Log.warning("Interrupted waiting for track to play."); + } + } + + if (_fnext != null) { + _playing = _fnext; + _pentry = _fnentry; + _fnext = null; + _fnentry = null; + } else { + _playing = _next; + _pentry = _nentry; + _nentry = null; + _next = null; + _djobj.skip(); // move forward in the playlist + } + return _playing; + } + + /** Helper function. */ + protected boolean equals (PlaylistEntry e1, PlaylistEntry e2) + { + return (e1 == e2 || (e1 != null && e1.equals(e2))); + } + + protected DJObject _djobj; + + protected Player _playing; + protected PlaylistEntry _pentry; + + protected Player _next, _fnext; + protected int _nidx; + protected PlaylistEntry _nentry, _fnentry; + + /** The number of frames to play before checking up on things. */ + protected static final int PLAY_FRAMES = 10; +} diff --git a/projects/robodj/src/java/robodj/util/RDJPrefsPanel.java b/projects/robodj/src/java/robodj/util/RDJPrefsPanel.java index 46bde5cc..cb3dab7b 100644 --- a/projects/robodj/src/java/robodj/util/RDJPrefsPanel.java +++ b/projects/robodj/src/java/robodj/util/RDJPrefsPanel.java @@ -168,6 +168,11 @@ public class RDJPrefsPanel extends JPanel protected String _prefKey; } + public static void main (String[] args) + { + display(true); + } + protected JFrame _frame; /** Defines our configurable preferences. */