Index: mythtv/themes/default-wide/music-ui.xml
===================================================================
--- mythtv/themes/default-wide/music-ui.xml	(revision 16678)
+++ mythtv/themes/default-wide/music-ui.xml	(working copy)
@@ -831,6 +831,159 @@
       </container>
    </window>
 
+   <window name="edit_radiometadata">
+
+      <font name="title" face="Trebuchet MS">
+          <color>#ffff00</color>
+          <dropcolor>#000000</dropcolor>
+          <size>30</size>
+          <size:small>18</size:small>
+          <shadow>3,3</shadow>
+          <bold>yes</bold>
+      </font>
+
+      <font name="labels" face="Trebuchet MS">
+          <color>#ffff00</color>
+          <dropcolor>#000000</dropcolor>
+          <size>18</size>
+          <size:small>14</size:small>
+          <shadow>3,3</shadow>
+          <bold>yes</bold>
+      </font>
+
+      <font name="display" face="Trebuchet MS">
+          <color>#ffffff</color>
+          <dropcolor>#000000</dropcolor>
+          <size>18</size>
+          <size:small>14</size:small>
+          <shadow>3,3</shadow>
+          <bold>yes</bold>
+      </font>
+
+      <container name="edit_container">
+
+         <area>0,0,1820,720</area>
+
+             <!--
+                    Labels
+            -->
+
+             <textarea name="title" draworder="1" align="center">
+                 <area>0,30,1280,50</area>
+                 <font>title</font>
+                 <value>Track Information</value>
+             </textarea>
+
+             <textarea name="station_label" draworder="1" align="right">
+                 <area>15,100,170,30</area>
+                 <font>labels</font>
+                 <value>Station:</value>
+             </textarea>
+
+             <textarea name="channel_label" draworder="1" align="right">
+                 <area>15,150,170,30</area>
+                 <font>labels</font>
+                 <value>Channel:</value>
+             </textarea>
+
+             <textarea name="url_label" draworder="1" align="right">
+                 <area>15,200,170,30</area>
+                 <font>labels</font>
+                 <value>URL:</value>
+             </textarea>
+
+             <textarea name="metaformat_label" draworder="1" align="right">
+                 <area>15,250,170,30</area>
+                 <font>labels</font>
+                 <value>Meta Format:</value>
+             </textarea>
+
+             <textarea name="genre_label" draworder="1" align="right">
+                 <area>15,300,170,30</area>
+                 <font>labels</font>
+                 <value>Genre:</value>
+             </textarea>
+
+             <textarea name="rating_label" draworder="1" align="right">
+                 <area>15,350,170,30</area>
+                 <font>labels</font>
+                 <value>Rating:</value>
+             </textarea>
+
+             <!--
+                    edits
+            -->
+
+             <remoteedit name="station_edit" draworder="1" align="left">
+                 <area>200,100,880,35</area>
+                 <font>display</font>
+             </remoteedit> 
+
+             <pushbutton name="searchstation_button" draworder="2">
+                 <position>1100,100</position>
+                 <image function="on" filename="blankbutton_on.png"> </image>
+                 <image function="off" filename="blankbutton_off.png"> </image>
+                 <image function="pushed" filename="blankbutton_pushed.png"> </image>
+             </pushbutton>
+
+             <remoteedit name="channel_edit" draworder="1" align="left">
+                 <area>200,150,880,35</area>
+                 <font>display</font>
+             </remoteedit>
+
+             <remoteedit name="url_edit" draworder="1" align="left">
+                 <area>200,200,880,35</area>
+                 <font>display</font>
+             </remoteedit>
+
+             <remoteedit name="metaformat_edit" draworder="1" align="left">
+                 <area>200,250,880,35</area>
+                 <font>display</font>
+             </remoteedit>
+
+             <remoteedit name="genre_edit" draworder="1" align="left">
+                 <area>200,300,880,35</area>
+                 <font>display</font>
+             </remoteedit>
+
+             <pushbutton name="searchgenre_button" draworder="2">
+                 <position>1100,300</position>
+                 <image function="on" filename="blankbutton_on.png"> </image>
+                 <image function="off" filename="blankbutton_off.png"> </image>
+                 <image function="pushed" filename="blankbutton_pushed.png"> </image>
+             </pushbutton>
+
+             <repeatedimage name="rating_image" draworder="1" fleximage="no">
+                 <filename>mm_rating.png</filename>
+                 <position>200,360</position>
+                 <orientation>LeftToRight</orientation>
+             </repeatedimage>
+
+             <selector name="rating_button" draworder="0">
+                 <area>1100,350,30,30</area>
+                 <font>display</font>
+                 <image function="on" filename="leftright_on.png"> </image>
+                 <image function="off" filename="leftright_off.png"> </image>
+                 <image function="pushed" filename="leftright_pushed.png"> </image>
+             </selector>
+
+
+             <!--
+                    Push buttons
+            -->
+
+             <textbutton name="done_button" draworder="0">
+                 <position>500,540</position>
+                 <font>display</font>
+                 <image function="on" filename="text_button_on.png"> </image>
+                 <image function="off" filename="text_button_off.png"> </image>
+                 <image function="pushed" filename="text_button_pushed.png"> </image>
+             </textbutton>
+
+      </container>
+
+   </window>
+
    <window name="cdripper">
 
        <font name="title" face="Arial">
Index: mythtv/libs/libmyth/output.cpp
===================================================================
--- mythtv/libs/libmyth/output.cpp	(revision 16678)
+++ mythtv/libs/libmyth/output.cpp	(working copy)
@@ -22,11 +22,8 @@
 
 
 void OutputListeners::error(const QString &e) {
-    QObject *object = firstListener();
-    while (object) {
-	QApplication::postEvent(object, new OutputEvent(e));
-	object = nextListener();
-    }
+    OutputEvent ev (e);
+    dispatch (ev);
 }
 
 void OutputListeners::addVisual(MythTV::Visual *v)
Index: mythtv/libs/libmyth/mythobservable.cpp
===================================================================
--- mythtv/libs/libmyth/mythobservable.cpp	(revision 16678)
+++ mythtv/libs/libmyth/mythobservable.cpp	(working copy)
@@ -12,48 +12,55 @@
 
 void MythObservable::addListener(QObject *listener)
 {
+    QMutexLocker locked(&m_mutex);
     if (m_listeners.find(listener) == -1)
         m_listeners.append(listener);
 }
 
 void MythObservable::removeListener(QObject *listener)
 {
+    QMutexLocker locked(&m_mutex);
     if (m_listeners.find(listener) != -1)
         m_listeners.remove(listener);
 }
 
-QObject* MythObservable::firstListener()
-{
-    return m_listeners.first();
-}
-
-QObject* MythObservable::nextListener()
-{
-    return m_listeners.next();
-}
-
 QPtrList<QObject> MythObservable::getListeners()
 {
+    QMutexLocker locked(&m_mutex);
     return m_listeners;
 }
 
 void MythObservable::dispatch(MythEvent &event)
 {
-    QObject *listener = firstListener();
-    while (listener)
+    // Copy the list and iterate on the copy, in case another thread
+    // modifies the list.
+
+    m_mutex.lock ();
+    QPtrList<QObject> listeners(m_listeners);
+    m_mutex.unlock ();
+
+    QPtrListIterator<QObject> it (listeners);
+    while (class QObject *listener = it.current ())
     {
         QApplication::postEvent(listener, event.clone());
-        listener = nextListener();
+        ++it;
     }
 }
 
 void MythObservable::dispatchNow(MythEvent &event)
 {
-    QObject *listener = firstListener();
-    while (listener)
+    // Copy the list and iterate on the copy, in case another thread
+    // modifies the list.
+
+    m_mutex.lock ();
+    QPtrList<QObject> listeners(m_listeners);
+    m_mutex.unlock ();
+
+    QPtrListIterator<QObject> it (listeners);
+    while (class QObject *listener = it.current ())
     {
         QApplication::sendEvent(listener, event.clone());
-        listener = nextListener();
+        ++it;
     }
 }
 
Index: mythtv/libs/libmyth/mythobservable.h
===================================================================
--- mythtv/libs/libmyth/mythobservable.h	(revision 16678)
+++ mythtv/libs/libmyth/mythobservable.h	(working copy)
@@ -2,6 +2,7 @@
 #define MYTHOBSERVABLE_H_
 
 #include <qptrlist.h>
+#include <qmutex.h>
 #include "mythexp.h"
 #include "mythevent.h"
 
@@ -61,47 +62,6 @@
     */
     void removeListener(QObject *listener);
 
-    /** \brief Begin iteration across listeners
-
-        If you simply need to iterate across the listeners, use \p
-        firstListener and \p nextListener to iterate across the
-        listeners. Ie. instead of 
-
-        \code
-        {
-            QPtrList<QObject> listeners = getListeners();
-            QObject *listener = listeners.first();
-            while (listener) {
-                // use listener...
-                listener = listeners.next();
-            }
-        }
-        \endcode
-
-        you can avoid the copy and just do
-
-        \code
-        {
-            QObject *listener = firstListener();
-            while (listener) {
-                // use listener...
-                listener = nextListener();
-            }
-        } 
-        \endcode
-
-        \returns pointer to the first listener, NULL if there are no listeners
-    */
-    QObject* firstListener();
-
-    /** \brief Continue iteration to the next listener
-
-        See firstListener. Returns NULL if there are no more listeners.
-
-        \returns pointer to the next listener, NULL if there are no more listeners
-    */
-    QObject* nextListener();
-
     /** \brief Get a copy of the list of listener
 
         If you need access to more than just iteration via
@@ -135,6 +95,7 @@
 
   private:
     QPtrList<QObject> m_listeners;
+    QMutex m_mutex;
 };
 
 #endif /* MYTHOBSERVABLE_H */
Index: mythplugins/mythmusic/mythmusic/playbackbox.cpp
===================================================================
--- mythplugins/mythmusic/mythmusic/playbackbox.cpp	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/playbackbox.cpp	(working copy)
@@ -20,14 +20,17 @@
 // MythMusic includes
 #include "metadata.h"
 #include "constants.h"
-#include "streaminput.h"
 #include "decoder.h"
+#include "decoderhandler.h"
 #include "playbackbox.h"
 #include "databasebox.h"
+#include "metaio.h"
 #include "mainvisual.h"
 #include "smartplaylist.h"
 #include "search.h"
 
+#define GET_REPO_ID(attrs) attrs->at(4)
+
 #ifndef USING_MINGW
 #include "cddecoder.h"
 #endif // USING_MINGW
@@ -104,8 +107,12 @@
     connect(volume_display_timer, SIGNAL(timeout()),
             this, SLOT(hideVolume()));
 
+    decoder_handler_progress_timer = new QTimer(this);
+    connect (decoder_handler_progress_timer, SIGNAL(timeout()), 
+             this, SLOT(operationProgressTimer()));
+    
+    // Figure out the shuffle mode
     setShuffleMode(gPlayer->getShuffleMode());
-
     resumemode = gPlayer->getResumeMode();
 
     setRepeatMode(gPlayer->getRepeatMode());
@@ -370,7 +377,7 @@
         }
         else if (action == "INFO")
             if (visualizer_status == 2) 
-                bannerToggle(curMeta);
+                bannerToggle(&displayMeta);
             else
                 showEditMetadataDialog();
         else if (action == "ESCAPE" && visualizer_status != 2)
@@ -546,6 +553,31 @@
     }
 }
 
+/*note:temporary fix for radio - Ideally, I'd like the metadata repo
+  add it's own stuff to the menu entry 
+*/
+void PlaybackBoxMusic::addRadioMenuEntries() 
+{
+    GenericTree *node = music_tree_list->getCurrentNode();
+
+    if (AllMusic::id_to_repo (node->getInt ()) != AllMusic::REPO_ID_RADIO) 
+        return;
+
+    playlist_popup->addButton(tr("Add Radio"), this,
+                              SLOT(showAddRadioStationDialog()));
+
+    if (node->isSelectable()) 
+        playlist_popup->addButton(tr("Remove Radio"), this,
+                                  SLOT(showRemoveRadioStationDialog()));
+
+    QLabel *splitter = playlist_popup->addLabel(" ", MythPopupBox::Small);
+    splitter->setLineWidth(2);
+    splitter->setFrameShape(QFrame::HLine);
+    splitter->setFrameShadow(QFrame::Sunken);
+    splitter->setMaximumHeight((int) (5 * hmult));
+    splitter->setMaximumHeight((int) (5 * hmult));
+}
+
 void PlaybackBoxMusic::handlePush(QString buttonname)
 {
     if (m_pushedButton)
@@ -592,6 +624,8 @@
     splitter->setMaximumHeight((int) (5 * hmult));
     splitter->setMaximumHeight((int) (5 * hmult));
 
+    addRadioMenuEntries();
+
     playlist_popup->addButton(tr("Search"), this,
                               SLOT(showSearchDialog()));
     playlist_popup->addButton(tr("From CD"), this,
@@ -600,16 +634,21 @@
                               SLOT(allTracks()));
     if (curMeta)
     {
-        playlist_popup->addButton(tr("Tracks by current Artist"), this,
-                                  SLOT(byArtist()));
-        playlist_popup->addButton(tr("Tracks from current Album"), this,
-                                  SLOT(byAlbum()));
-        playlist_popup->addButton(tr("Tracks from current Genre"), this,
-                                  SLOT(byGenre()));
-        playlist_popup->addButton(tr("Tracks from current Year"), this,
+        /*note: temporary fix for radio. Ideally, I'd like the
+          metadata repo add it's own stuff to the menur entry. 
+        */
+        if (curMeta->Format()!="cast") {
+            playlist_popup->addButton(tr("Tracks by current Artist"), this,
+                                      SLOT(byArtist()));
+            playlist_popup->addButton(tr("Tracks from current Album"), this,
+                                      SLOT(byAlbum()));
+            playlist_popup->addButton(tr("Tracks from current Genre"), this,
+                                      SLOT(byGenre()));
+            playlist_popup->addButton(tr("Tracks from current Year"), this,
                                   SLOT(byYear()));
-        playlist_popup->addButton(tr("Tracks with same Title"), this,
-                                  SLOT(byTitle()));
+            playlist_popup->addButton(tr("Tracks with same Title"), this,
+                                      SLOT(byTitle()));
+        }
     }
     
     playlist_popup->ShowPopup(this, SLOT(closePlaylistPopup()));
@@ -667,6 +706,22 @@
     }
 }
 
+void PlaybackBoxMusic::operationProgressTimer()
+{
+    operation_progress_count++;
+    decoderHandlerInfo(operation_name, 
+                       QString().fill('.', operation_progress_count));
+
+    // You get ten seconds...
+    if (operation_progress_count >= 10) 
+    {
+        gPlayer->stop();
+        MythPopupBox::showOkPopup(gContext->GetMainWindow(), 
+                                  statusString,
+                                  QString("Operation timed out"));
+    }
+}
+
 void PlaybackBoxMusic::showSearchDialog()
 {
    if (!playlist_popup)
@@ -686,6 +741,94 @@
     }
 }
 
+void PlaybackBoxMusic::showAddRadioStationDialog()
+{
+   if (!playlist_popup)
+        return;
+
+    closePlaylistPopup();
+
+    Metadata *meta = new Metadata ("http://", "", "", "", "");
+    meta->setArtist ("");
+    meta->setAlbum ("");
+    meta->setTitle ("");
+    meta->setGenre ("");
+    meta->setFormat("cast");
+
+    MythThemedDialog *dialog = meta->createEditorDialog ();
+    if (dialog->exec()) {
+        GenericTree *playlist_tree = gPlayer->getPlaylistTree();
+        gMusicData->all_music->addRadioTrack(meta);
+        /*note:temporary fix for radio
+
+          This is a really really poor way or inserting something into
+          the tree. It'd be way nicer to have a object managing
+          classes of metadata which could then do this.
+          
+         */
+        for (int ii = 0; ii < playlist_tree->childCount(); ii++)
+        {
+            GenericTree *t_node = playlist_tree->getChildAt(ii);
+
+            if (!t_node)
+                continue;
+
+            IntVector *attrs = t_node->getAttributes();
+            if (attrs && attrs->size() > 4) 
+            {
+                int counter = t_node->childCount();
+                QString title = meta->FormatArtist ();
+                title += " ~ ";
+                title += meta->Title();
+                meta->setAlbum(title);
+                GenericTree *sub = t_node->addNode(title, meta->ID(), true);
+                sub->setAttribute(0, 1);
+                sub->setAttribute(1, counter);
+                sub->setAttribute(2, rand());
+                sub->setAttribute(3, rand());
+                sub->setAttribute(4, 1);
+                t_node->sortByString();
+                music_tree_list->refresh();
+                break;
+            }
+        }
+    }
+
+    dialog->deleteLater ();
+}
+
+void PlaybackBoxMusic::showRemoveRadioStationDialog()
+{
+   if (!playlist_popup)
+        return;
+
+    closePlaylistPopup();
+
+    GenericTree *node = music_tree_list->getCurrentNode();
+    Metadata *meta = gMusicData->all_music->getMetadata(node->getInt());
+    
+    bool answer = MythPopupBox::showOkCancelPopup(gContext->GetMainWindow(), 
+                                                  QString("Remove Station"),
+                                                  QString("Do you want to remove\n"
+                                                          "%1 ~ %2?").
+                                                  arg(meta->Artist()).
+                                                  arg(meta->Title()),
+                                                  true);
+
+    if (!answer)
+        return;
+
+    meta->removeFromDatabase();    
+    
+    // Move away from the current node...
+    if (!music_tree_list->moveUp(false)) 
+        music_tree_list->moveDown(false);
+
+    node->getParent()->removeNode(node);
+    music_tree_list->refresh();
+}
+
+
 void PlaybackBoxMusic::byArtist()
 {
     if (!playlist_popup || !curMeta)
@@ -977,19 +1120,19 @@
 
 void PlaybackBoxMusic::showEditMetadataDialog()
 {
-    if (!curMeta)
-    {
+    GenericTree *node = music_tree_list->getCurrentNode();
+    Metadata *editMeta = 0;
+
+    /* note: temporary fix for radio, get the appropriate editor for repo */
+    editMeta = gMusicData->all_music->getMetadata(node->getInt());
+
+    if(!editMeta)
         return;
-    }
 
     // store the current track metadata in case the track changes
     // while we show the edit dialog
-    GenericTree *node = music_tree_list->getCurrentNode();
-    Metadata *editMeta = gMusicData->all_music->getMetadata( node->getInt() );
-
-    EditMetadataDialog editDialog(editMeta, gContext->GetMainWindow(),
-                      "edit_metadata", "music-", "edit metadata");
-    if (kDialogCodeRejected != editDialog.exec())
+    MythThemedDialog *editDialog = editMeta->createEditorDialog ();
+    if (kDialogCodeRejected != editDialog->exec())
     {
         MythBusyDialog *busy = new MythBusyDialog(
             QObject::tr("Rebuilding music tree"));
@@ -1000,9 +1143,11 @@
         // Get a reference to the current track
         QValueList <int> branches_to_current_node;
 
-        QValueList <int> *a_route;
-        a_route = music_tree_list->getRouteToActive();
-        branches_to_current_node = *a_route;
+        QValueList <int> *a_route (music_tree_list->getRouteToActive());
+        if (a_route)
+        {
+            branches_to_current_node = *a_route;
+        }
 
         // reload music
         gMusicData->all_music->save();
@@ -1044,6 +1189,8 @@
         busy->Close();
         busy->deleteLater();
     }
+
+    editDialog->deleteLater ();
 }
 
 void PlaybackBoxMusic::checkForPlaylists()
@@ -1277,9 +1424,7 @@
     if (gPlayer->isPlaying())
         gPlayer->stop();
 
-    if (curMeta)
-        playfile = curMeta->Filename();
-    else
+    if (!curMeta)
     {
         // Perhaps we can descend to something playable?
         wipeTrackInfo();
@@ -1293,24 +1438,8 @@
     }
 
     gPlayer->setCurrentNode(music_tree_list->getCurrentNode());
-    gPlayer->playFile(playfile);
+    gPlayer->play ();
 
-    currentTime = 0;
-
-    mainvisual->setDecoder(gPlayer->getDecoder());
-    mainvisual->setOutput(gPlayer->getOutput());
-    mainvisual->setMetadata(curMeta);
-
-    if (gPlayer->isPlaying())
-    {
-        if (resumemode == MusicPlayer::RESUME_EXACT && 
-                gContext->GetNumSetting("MusicBookmarkPosition", 0) > 0)
-        {
-            seek(gContext->GetNumSetting("MusicBookmarkPosition", 0));
-            gContext->SaveSetting("MusicBookmarkPosition", 0);
-        }
-    }
-
     bannerEnable(curMeta, show_album_art);
 }
 
@@ -1391,7 +1520,7 @@
     }
 }
 
-void PlaybackBoxMusic::setTrackOnLCD(Metadata *mdata)
+void PlaybackBoxMusic::setTrackOnLCD(const Metadata *mdata)
 {
     LCD *lcd = LCD::Get();
     if (!lcd || !mdata)
@@ -1412,8 +1541,8 @@
 {
     gPlayer->stop();
 
-    mainvisual->setDecoder(0);
-    mainvisual->setOutput(0);
+    mainvisual->setDecoder(NULL);
+    mainvisual->setOutput(NULL);
     mainvisual->deleteMetadata();
 
     QString time_string = getTimeString(maxTime, 0);
@@ -1611,6 +1740,8 @@
         music_tree_list->setVisualOrdering(1);
     music_tree_list->refresh();
 
+    // refreshing the music_tree causes the LCD to get redrawn
+    // with the tree, so force back to the music screen.
     if (gPlayer->isPlaying())
         setTrackOnLCD(curMeta);
 }
@@ -1977,10 +2108,15 @@
 
             break;
         }
+        case DecoderEvent::Decoding:
+        {
+            statusString = tr("Stream decoding.");
+            displayMeta = *curMeta;
+            break;
+        }
         case DecoderEvent::Stopped:
         {
             statusString = tr("Stream stopped.");
-
             break;
         }
         case DecoderEvent::Finished:
@@ -2007,6 +2143,54 @@
                                       .arg(*dxe->errorMessage()));
             break;
         }
+        case DecoderHandlerEvent::Ready:
+        {
+            decoderHandlerReady();
+            break;
+        }
+        case DecoderHandlerEvent::OperationStart:
+        {
+            DecoderHandlerEvent *dxe = (DecoderHandlerEvent*)event;
+            decoderHandlerOperationStart(*dxe->getMessage());
+            break;
+        }
+        case DecoderHandlerEvent::OperationStop:
+        {
+            decoderHandlerOperationStop();
+            break;
+        }
+        case DecoderHandlerEvent::Error:
+        {
+            statusString = tr("Input error.");
+            DecoderHandlerEvent *dxe = (DecoderHandlerEvent*)event;
+            VERBOSE(VB_IMPORTANT, QString ("%1 %2").arg (statusString).arg(*dxe->getMessage()));
+            MythPopupBox::showOkPopup(gContext->GetMainWindow(), 
+                                      statusString,
+                                      QString("MythMusic has encountered the following error:\n%1")
+                                      .arg(*dxe->getMessage()));
+            stopAll();
+            break;
+        }
+        case DecoderHandlerEvent::Info:
+        {
+            DecoderHandlerEvent *dxe = (DecoderHandlerEvent*)event;
+            Metadata tmp (*curMeta);
+            tmp.setArtist("");
+            tmp.setTitle(*dxe->getMessage());            
+            updateTrackInfo(&tmp);
+            break;
+        }
+        case DecoderHandlerEvent::Meta:
+        {
+            DecoderHandlerEvent *dxe = (DecoderHandlerEvent*)event;
+            displayMeta = *dxe->getMetadata();
+            updateTrackInfo(&displayMeta);
+
+            if (visualizer_status > 0 && cycle_visualizer)
+                CycleVisualizer();
+
+            break;
+        }
     }
 
     QWidget::customEvent(event);
@@ -2051,6 +2235,7 @@
     }
 
     setTrackOnLCD(mdata);
+    bannerEnable(mdata);            
 }
 
 void PlaybackBoxMusic::showAlbumArtImage(Metadata *mdata)
@@ -2126,6 +2311,7 @@
             activenode = music_tree_list->getCurrentNode();
         }
 
+        VERBOSE(VB_PLAYBACK, QString("playing node %1").arg(node_int));
         curMeta = gMusicData->all_music->getMetadata(node_int);
 
         updateTrackInfo(curMeta);
@@ -2429,3 +2615,37 @@
 
     return time_string;
 }
+
+
+void PlaybackBoxMusic::decoderHandlerReady(void)
+{    
+    mainvisual->setMetadata(curMeta);
+    bannerEnable(curMeta, show_album_art);
+}
+
+void PlaybackBoxMusic::decoderHandlerInfo(const QString &message, 
+                                          const QString &submessage)
+{
+    Metadata tmp (*curMeta);
+
+    tmp.setArtist(submessage);
+    tmp.setTitle(message);
+
+    updateTrackInfo(&tmp);
+}
+
+void PlaybackBoxMusic::decoderHandlerOperationStart(const QString &op)
+{
+    operation_name = op;
+    operation_progress_count = 0;
+    decoderHandlerInfo(operation_name, 
+                       QString().fill('.', operation_progress_count));
+    decoder_handler_progress_timer->start(1000);
+}
+
+void PlaybackBoxMusic::decoderHandlerOperationStop()
+{
+    Metadata tmp (*curMeta);
+    updateTrackInfo(&tmp);
+    decoder_handler_progress_timer->stop();
+}
Index: mythplugins/mythmusic/mythmusic/mythmusic.pro
===================================================================
--- mythplugins/mythmusic/mythmusic/mythmusic.pro	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/mythmusic.pro	(working copy)
@@ -40,6 +40,7 @@
 HEADERS += editmetadata.h smartplaylist.h search.h genres.h
 HEADERS += treebuilders.h importmusic.h directoryfinder.h
 HEADERS += filescanner.h libvisualplugin.h musicplayer.h miniplayer.h
+HEADERS += shoutcast.h decoderhandler.h editradiometadata.h pls.h
 
 SOURCES += cddecoder.cpp cdrip.cpp decoder.cpp 
 SOURCES += flacdecoder.cpp flacencoder.cpp maddecoder.cpp main.cpp
@@ -56,6 +57,7 @@
 SOURCES += avfdecoder.cpp editmetadata.cpp smartplaylist.cpp search.cpp
 SOURCES += treebuilders.cpp importmusic.cpp directoryfinder.cpp
 SOURCES += filescanner.cpp libvisualplugin.cpp musicplayer.cpp miniplayer.cpp
+SOURCES += shoutcast.cpp decoderhandler.cpp editradiometadata.cpp pls.cpp 
 
 macx {
     SOURCES -= cddecoder.cpp
Index: mythplugins/mythmusic/mythmusic/decoderhandler.cpp
===================================================================
--- mythplugins/mythmusic/mythmusic/decoderhandler.cpp	(revision 0)
+++ mythplugins/mythmusic/mythmusic/decoderhandler.cpp	(revision 0)
@@ -0,0 +1,556 @@
+#include "decoderhandler.h"
+#include "decoder.h"
+#include "metadata.h"
+#include "streaminput.h"
+#include "shoutcast.h"
+
+#include <qapplication.h>
+#include <qurl.h>
+#include <qurloperator.h>
+
+#include <mythtv/httpcomms.h>
+#include <mythtv/mythcontext.h>
+
+#include <unistd.h>
+#include <stdio.h>
+#include <assert.h>
+
+/**********************************************************************/
+
+DecoderHandlerEvent::DecoderHandlerEvent(const Metadata &m)
+    : MythEvent(Meta), m_msg(NULL), m_meta(NULL)
+{ 
+    m_meta = new Metadata(m);
+}
+
+DecoderHandlerEvent::~DecoderHandlerEvent()
+{
+    delete m_msg;
+    delete m_meta;
+}
+
+DecoderHandlerEvent* DecoderHandlerEvent::clone() 
+{ 
+    DecoderHandlerEvent *result = new DecoderHandlerEvent(*this);
+
+    if (m_msg)
+        result->m_msg = new QString(*m_msg);
+
+    if (m_meta)
+        result->m_meta = new Metadata(*m_meta);
+
+    return result;
+}
+
+/**********************************************************************/
+
+DecoderIOFactory::DecoderIOFactory(DecoderHandler *parent) 
+{
+    m_handler = parent;
+}
+
+DecoderIOFactory::~DecoderIOFactory()
+{
+}
+
+void DecoderIOFactory::doConnectDecoder(const QString &format) 
+{ 
+    m_handler->doOperationStop();
+    m_handler->doConnectDecoder(getUrl(), format); 
+}
+
+Decoder *DecoderIOFactory::getDecoder()
+{
+    return m_handler->getDecoder();
+}
+
+void DecoderIOFactory::doFailed(const QString &message) 
+{ 
+    m_handler->doOperationStop();
+    m_handler->doFailed(getUrl(), message); 
+}
+
+void DecoderIOFactory::doInfo(const QString &message) 
+{ 
+    m_handler->doInfo(message);
+}
+
+void DecoderIOFactory::doOperationStart(const QString &name)
+{
+    m_handler->doOperationStart(name);
+}
+
+void DecoderIOFactory::doOperationStop(void)
+{
+    m_handler->doOperationStop();
+}
+
+/**********************************************************************/
+
+DecoderIOFactoryFile::DecoderIOFactoryFile(DecoderHandler *parent) 
+    : DecoderIOFactory(parent), m_input (NULL)
+{
+}
+
+DecoderIOFactoryFile::~DecoderIOFactoryFile () 
+{
+    delete m_input;
+}
+
+QIODevice* DecoderIOFactoryFile::takeInput () 
+{
+    QIODevice *result = m_input;
+    m_input = NULL;
+    return result;
+}
+
+void 
+DecoderIOFactoryFile::start()
+{
+    QString sourcename = getMetadata().Filename();    
+    VERBOSE(VB_PLAYBACK, QString("DecoderIOFactory: Opening Local File %1").arg(sourcename));
+    m_input = new QFile(sourcename);
+    doConnectDecoder(getUrl());
+}
+
+
+/**********************************************************************/
+
+DecoderIOFactoryUrl::DecoderIOFactoryUrl(DecoderHandler *parent) : DecoderIOFactory(parent) 
+{
+    /* Yes, this is pretty lame, it doesn't throttle the download or
+       anything, but just blindly downloads the entire file. 
+       However, it supports whatever QUrlOperator supports and you should
+       see it as an example that you can enhance...
+    */
+    m_url_op = new QUrlOperator;
+    m_input = new QFile("/tmp/mythmusic.tmp");
+    m_input->open(IO_ReadOnly);
+    m_output = new QFile("/tmp/mythmusic.tmp");
+    m_output->open(IO_WriteOnly);
+    
+    connect(m_url_op, SIGNAL(finished(QNetworkOperation*)), 
+            this, SLOT(finished(QNetworkOperation*)));;
+    connect(m_url_op, SIGNAL(start(QNetworkOperation*)), 
+            this, SLOT(start(QNetworkOperation*)));;
+    connect(m_url_op, SIGNAL(data(const QByteArray&, QNetworkOperation*)), 
+            this, SLOT(data(const QByteArray&, QNetworkOperation*)));;
+}
+
+DecoderIOFactoryUrl::~DecoderIOFactoryUrl () 
+{
+    doClose();
+    
+    m_url_op->deleteLater();
+    m_output->remove();
+
+    delete m_output;
+    delete m_input;
+}
+
+QIODevice* DecoderIOFactoryUrl::takeInput () 
+{
+    QIODevice *result = m_input;
+    m_input = NULL;
+    return result;
+}
+
+void DecoderIOFactoryUrl::start()
+{
+    VERBOSE(VB_PLAYBACK, QString("DecoderIOFactory: Url %1").arg(getUrl().toString()));
+
+    m_started = false;
+
+    doOperationStart("Fetching remote file");
+    
+    m_url_op->get(getUrl());
+}
+
+void DecoderIOFactoryUrl::stop()
+{
+    doClose();
+}
+
+void DecoderIOFactoryUrl::finished(QNetworkOperation *op)
+{
+    m_output->close();
+
+    if (op->state() != QNetworkProtocol::StDone) 
+    {
+        doFailed("Cannot retrieve remote file.");
+        return;
+    }
+    
+    if (!m_started)
+        doStart();
+}
+
+void DecoderIOFactoryUrl::start(QNetworkOperation*)
+{
+}
+
+void DecoderIOFactoryUrl::data(const QByteArray & data, QNetworkOperation*)
+{
+    m_output->writeBlock(data);
+    if (!m_started && m_input->size () > DecoderIOFactory::DefaultPrebufferSize) 
+        doStart();
+}
+
+void DecoderIOFactoryUrl::doStart()
+{
+    doConnectDecoder(getUrl());
+    m_started = true;
+}
+
+void DecoderIOFactoryUrl::doClose()
+{
+    if (m_input->isOpen())
+        m_input->close();
+    if (m_output->isOpen())
+        m_output->close();
+}
+
+/**********************************************************************/
+
+/* I've left this here since it used to exist... Don't even know what
+ * mqp is, much less how to test it... 
+ */
+
+DecoderIOFactoryMqp::DecoderIOFactoryMqp(DecoderHandler *parent) : DecoderIOFactory(parent) 
+{
+    m_input = NULL;
+}
+
+DecoderIOFactoryMqp::~DecoderIOFactoryMqp () 
+{
+    delete m_input;
+}
+
+QIODevice* DecoderIOFactoryMqp::takeInput () 
+{
+    QIODevice *result = m_input;
+    m_input = NULL;
+    return result;
+}
+
+void DecoderIOFactoryMqp::start()
+{
+    VERBOSE(VB_PLAYBACK, QString("DecoderIOFactory: Mqp %1").arg(getUrl().toString()));
+    StreamInput streaminput(getUrl());
+    streaminput.setup();
+    m_input = streaminput.socket();
+    doConnectDecoder(getUrl());
+}
+
+void DecoderIOFactoryMqp::stop()
+{
+    m_input->close();
+}
+
+/**********************************************************************/
+
+DecoderHandler::DecoderHandler() 
+    : m_state (STOPPED), 
+      m_io_factory (NULL), 
+      m_decoder (NULL), 
+      m_op (false), 
+      m_redirects (0)
+{
+}
+
+DecoderHandler::~DecoderHandler() 
+{
+    stop();
+}
+
+void DecoderHandler::start(Metadata *mdata) 
+{
+    m_state = LOADING;
+
+    m_playlist.clear();
+    m_meta = mdata;
+    m_playlist_pos = -1;
+    m_redirects = 0;
+    
+    QUrl url(mdata->Filename());
+    bool result = createPlaylist(url);
+    
+    if (m_state == LOADING && result) 
+    {
+        for (int ii = 0; ii < m_playlist.size(); ii++) 
+            VERBOSE(VB_PLAYBACK, QString("Track %1 = %2").
+                    arg(ii).
+                    arg(m_playlist.get(ii)->File()));
+        next();
+    } else {
+        if (m_state != STOPPED) {
+            doFailed(url, "Could not get playlist");
+        }
+    }
+}
+
+void DecoderHandler::error(const QString &e) 
+{
+    QString *str = new QString(e.utf8());
+    DecoderHandlerEvent ev(DecoderHandlerEvent::Error, str);
+    dispatch(ev);
+}
+
+bool DecoderHandler::done() 
+{
+    if (m_state == STOPPED)
+        return true;
+
+    if (m_playlist_pos + 1 >= m_playlist.size()) 
+    {
+        m_state = STOPPED;
+        return true;
+    }
+
+    return false;
+}
+
+bool DecoderHandler::next() 
+{
+    if (done())
+        return false;
+
+    if (m_meta && m_meta->Format() == "cast") {
+        m_playlist_pos = rand () % m_playlist.size ();
+    } else {
+        m_playlist_pos++;
+    }
+
+    PlayListFileEntry *entry = m_playlist.get(m_playlist_pos);
+    QUrl url(entry->File());
+
+    VERBOSE(VB_PLAYBACK, QString("Now playing '%1'").arg(url.toString()));
+
+    deleteIOFactory();
+    createIOFactory(url);
+
+    if (! haveIOFactory()) 
+        return false;
+
+    getIOFactory()->addListener(this);
+    getIOFactory()->setUrl(url);
+    getIOFactory()->setMeta(m_meta);
+    getIOFactory()->start();
+    m_state = ACTIVE;
+
+    return true;
+}
+
+void DecoderHandler::stop()
+{
+    VERBOSE(VB_PLAYBACK, QString("Stopping decoder"));
+
+    if (m_decoder && m_decoder->running()) {
+        m_decoder->lock();
+        m_decoder->stop();
+        m_decoder->unlock();
+    }
+
+    if (m_decoder) {
+        m_decoder->lock();
+        m_decoder->cond()->wakeAll();
+        m_decoder->unlock();
+    }
+
+    if (m_decoder) {
+        m_decoder->wait();
+        delete m_decoder->input ();
+        delete m_decoder;        
+        m_decoder = NULL;
+    }
+
+    deleteIOFactory ();
+    doOperationStop();
+
+    m_state = STOPPED;
+}
+
+void DecoderHandler::customEvent(QCustomEvent *qevent)
+{
+    if (class DecoderHandlerEvent *event = dynamic_cast<DecoderHandlerEvent*>(qevent)) {
+        // Proxy all DecoderHandlerEvents
+        return dispatch(*event);    
+    }
+}
+
+bool DecoderHandler::createPlaylist(const QUrl &url)
+{
+    QString extension = url.fileName().right(4).lower();
+    VERBOSE (VB_NETWORK, QString ("File %1 has extension %2").arg (url.fileName()).arg(extension));
+    if (extension == ".pls" || extension == ".m3u")
+        if (url.isLocalFile()) 
+            return createPlaylistFromFile(url);
+        else
+            return createPlaylistFromRemoteUrl(url);
+    
+    return createPlaylistForSingleFile(url);
+}
+
+bool DecoderHandler::createPlaylistForSingleFile(const QUrl &url)
+{
+    PlayListFileEntry *entry = new PlayListFileEntry;
+
+    if (url.isLocalFile())
+        entry->setFile(url.dirPath()+'/'+url.fileName());
+    else
+        entry->setFile(url.toString());
+
+    m_playlist.add(entry);
+
+    return m_playlist.size() > 0;
+}
+
+bool DecoderHandler::createPlaylistFromFile(const QUrl &url)
+{
+    QFile f(url.dirPath() + "/" + url.fileName());
+    f.open(IO_ReadOnly);
+    QTextStream stream(&f);
+    
+    if (PlayListFile::parse(&m_playlist, &stream) < 0) 
+        return false;
+
+    return m_playlist.size() > 0;
+}
+
+bool DecoderHandler::createPlaylistFromRemoteUrl(const QUrl &url) 
+{
+    HttpComms comms;
+    QUrl _url(url);
+
+    if (url.hasUser() && url.hasPassword()) {
+       VERBOSE(VB_NETWORK, QString("Using usename and password"));
+       HttpComms::Credentials c(url.user(), url.password());
+       comms.setCredentials(c, HttpComms::CRED_WEB);
+    }
+    VERBOSE(VB_NETWORK, QString("Retrieving playlist from '%1'").arg(_url.toString()));
+
+    doOperationStart("Retrieving playlist");
+    comms.request(_url, 10000, false);    
+    while(!comms.isDone()) 
+    {
+        qApp->processEvents(500);
+    }
+    doOperationStop();
+
+    VERBOSE(VB_NETWORK, QString("Retrieving playlist from '%1' = %2 (%3)").
+            arg(_url.toString()).
+            arg(comms.getStatusCode ()).
+            arg(comms.isTimedout()?"timed out":"not timed out"));
+
+    if (comms.isTimedout()) 
+        return false;
+
+    if (comms.getStatusCode() == 302 ||
+        comms.getStatusCode() == 301) 
+    {
+        QString redir = comms.getRedirectedURL();
+        m_redirects++;
+        if (m_redirects > MaxRedirects) {
+            VERBOSE(VB_IMPORTANT, QString ("Too many redirections."));
+            return false;
+        }
+        return createPlaylistFromRemoteUrl(redir);
+    }
+
+    if (comms.getStatusCode() != 200)
+        return false;
+
+    QBuffer buffer(comms.getRawData());
+    buffer.open(IO_ReadOnly);
+    QTextStream stream(&buffer);
+    
+    bool result = PlayListFile::parse(&m_playlist, &stream) > 0;
+    return result;
+}
+
+void DecoderHandler::doConnectDecoder(const QUrl &url, const QString &format) 
+{
+    if (m_decoder && !m_decoder->factory()->supports(format)) {
+        delete m_decoder;
+        m_decoder = NULL;
+    }
+
+    if (! m_decoder) {
+        if ((m_decoder = Decoder::create(format, NULL, NULL, true)) == NULL) {
+            doFailed(url, "No decoder for this format");
+            return;
+        }
+    }
+
+    m_decoder->setInput(getIOFactory()->takeInput());
+    m_decoder->setFilename(url.toString());
+
+    DecoderHandlerEvent ev(DecoderHandlerEvent::Ready);
+    dispatch(ev);
+}
+
+void DecoderHandler::doFailed(const QUrl &url, const QString &message)
+{
+    printf("mythmusic: unsupported fileformat: %s\n", url.toString().ascii ());
+    DecoderHandlerEvent ev(DecoderHandlerEvent::Error, new QString (message));
+    dispatch(ev);
+}
+
+void DecoderHandler::doInfo(const QString &message)
+{
+    DecoderHandlerEvent ev(DecoderHandlerEvent::Info, new QString (message));
+    dispatch(ev);
+}
+
+void DecoderHandler::doOperationStart(const QString &name)
+{
+    m_op = true;
+    DecoderHandlerEvent ev(DecoderHandlerEvent::OperationStart, new QString (name));
+    dispatch(ev);
+}
+
+void DecoderHandler::doOperationStop(void)
+{
+    if (!m_op)
+        return;
+
+    m_op = false;
+    DecoderHandlerEvent ev(DecoderHandlerEvent::OperationStop);
+    dispatch(ev);
+}
+
+void DecoderHandler::createIOFactory(const QUrl &url)
+{
+    PlayListFile::test();
+
+    if (haveIOFactory()) 
+        deleteIOFactory();
+
+    if (url.isLocalFile()) {
+        m_io_factory = new DecoderIOFactoryFile(this);
+    }
+    else
+    {
+        if (url.protocol() == "mqp" && !url.host().isNull()) 
+            m_io_factory = new DecoderIOFactoryMqp(this);
+        else if (m_meta && m_meta->Format() == "cast")
+            m_io_factory = new DecoderIOFactoryShoutCast(this);
+        else
+            m_io_factory = new DecoderIOFactoryUrl(this);
+    }
+}
+
+void DecoderHandler::deleteIOFactory(void)
+{
+    if (! haveIOFactory())
+        return;
+
+    if (m_state == ACTIVE)
+        m_io_factory->stop ();
+
+    m_io_factory->removeListener(this);
+    m_io_factory->disconnect();
+    m_io_factory->deleteLater();
+    //delete m_io_factory;
+    m_io_factory = NULL;
+}
Index: mythplugins/mythmusic/mythmusic/editradiometadata.cpp
===================================================================
--- mythplugins/mythmusic/mythmusic/editradiometadata.cpp	(revision 0)
+++ mythplugins/mythmusic/mythmusic/editradiometadata.cpp	(revision 0)
@@ -0,0 +1,355 @@
+#include <mythtv/mythcontext.h>
+#include <mythtv/mythdbcon.h>
+#include <qdir.h>
+#include "editradiometadata.h"
+#include "metadata.h"
+#include "decoder.h"
+#include "genres.h"
+
+EditRadioMetadataDialog::EditRadioMetadataDialog(Metadata *source_metadata,
+                                                 MythMainWindow *parent,
+                                                 QString window_name,
+                                                 QString theme_filename,
+                                                 const char* name)
+    :MythThemedDialog(parent, window_name, theme_filename, name)
+{
+    // make a copy so we can abandon changes
+    m_metadata = new Metadata(*source_metadata);
+    m_sourceMetadata = source_metadata;
+    wireUpTheme();
+    fillWidgets();
+    assignFirstFocus();
+}
+
+void EditRadioMetadataDialog::fillWidgets()
+{
+    if (station_edit)
+    {
+        station_edit->setText(m_metadata->Artist());
+    }
+
+    if (channel_edit)
+    {
+        channel_edit->setText(m_metadata->Title());
+    }
+
+    if (url_edit)
+    {
+        url_edit->setText(m_metadata->Filename());
+    }
+
+    if (metaformat_edit)
+    {
+        metaformat_edit->setText(m_metadata->CompilationArtist());
+    }
+
+    if (genre_edit)
+    {
+        genre_edit->setText(m_metadata->Genre());
+    }
+
+    if (rating_image)
+    {
+        rating_image->setRepeat(m_metadata->Rating());
+    }
+}
+
+void EditRadioMetadataDialog::incRating(bool up_or_down)
+{
+    if (up_or_down)
+        m_metadata->incRating();
+    else
+        m_metadata->decRating();
+
+    fillWidgets();
+}
+
+void EditRadioMetadataDialog::keyPressEvent(QKeyEvent *e)
+{
+    bool handled = false;
+
+    QStringList actions;
+    gContext->GetMainWindow()->TranslateKeyPress("Video", e, actions);
+
+    for (unsigned int i = 0; i < actions.size() && !handled; i++)
+    {
+        QString action = actions[i];
+        handled = true;
+
+        if (action == "UP")
+            nextPrevWidgetFocus(false);
+        else if (action == "DOWN")
+            nextPrevWidgetFocus(true);
+        else if (action == "LEFT")
+        {
+            if (getCurrentFocusWidget() == rating_button)
+            {
+                rating_button->push();
+                incRating(false);
+            }
+        }
+        else if (action == "RIGHT")
+        {
+            if (getCurrentFocusWidget() == rating_button)
+            {
+                rating_button->push();
+                incRating(true);
+            }
+        }
+        else if (action == "SELECT")
+        {
+            activateCurrent();
+        }
+        else if (action == "0")
+        {
+            if (done_button)
+                done_button->push();
+        }
+        else if (action == "1")
+        {
+        }
+        else
+            handled = false;
+    }
+
+    if (!handled)
+        MythThemedDialog::keyPressEvent(e);
+}
+
+void EditRadioMetadataDialog::wireUpTheme()
+{
+    station_edit = getUIRemoteEditType("station_edit");
+    if (station_edit)
+    {
+        station_edit->createEdit(this);
+        connect(station_edit, SIGNAL(loosingFocus()), this, SLOT(editLostFocus()));
+    }
+    
+    channel_edit = getUIRemoteEditType("channel_edit");
+    if (channel_edit)
+    {
+        channel_edit->createEdit(this);
+        connect(channel_edit, SIGNAL(loosingFocus()), this, SLOT(editLostFocus()));
+    }
+    
+    url_edit = getUIRemoteEditType("url_edit");
+    if (url_edit)
+    {
+        url_edit->createEdit(this);
+        connect(url_edit, SIGNAL(loosingFocus()), this, SLOT(editLostFocus()));
+    }
+    
+    metaformat_edit = getUIRemoteEditType("metaformat_edit");
+    if (metaformat_edit)
+    {
+        metaformat_edit->createEdit(this);
+        connect(metaformat_edit, SIGNAL(loosingFocus()), this, SLOT(editLostFocus()));
+    }
+         
+    genre_edit = getUIRemoteEditType("genre_edit");
+    if (genre_edit)
+    {
+        genre_edit->createEdit(this);
+        connect(genre_edit, SIGNAL(loosingFocus()), this, SLOT(editLostFocus()));
+    }
+        
+    rating_image = getUIRepeatedImageType("rating_image");
+    
+    searchstation_button = getUIPushButtonType("searchstation_button");
+    if (searchstation_button)
+    {
+        connect(searchstation_button, SIGNAL(pushed()), this, SLOT(searchStation()));
+    }
+
+    searchgenre_button = getUIPushButtonType("searchgenre_button");
+    if (searchgenre_button)
+    {
+        connect(searchgenre_button, SIGNAL(pushed()), this, SLOT(searchGenre()));
+    }
+
+    done_button = getUITextButtonType("done_button");
+    if (done_button)
+    {
+        done_button->setText(tr("Done"));
+        connect(done_button, SIGNAL(pushed()), this, SLOT(showSaveMenu()));
+    }
+
+    rating_button = getUISelectorType("rating_button");
+    if (rating_button)
+    {
+        
+    }
+
+    buildFocusList();
+}
+
+void EditRadioMetadataDialog::editLostFocus()
+{
+    UIRemoteEditType *whichEditor = (UIRemoteEditType *) getCurrentFocusWidget();
+    
+    if (whichEditor == station_edit)
+    {
+        m_metadata->setArtist(station_edit->getText());
+    }
+    else if (whichEditor == channel_edit)
+    {
+        m_metadata->setTitle(channel_edit->getText());
+    }
+    else if (whichEditor == url_edit)
+    {
+        m_metadata->setFilename(url_edit->getText());
+    }
+    else if (whichEditor == metaformat_edit)
+    {
+        m_metadata->setCompilationArtist(metaformat_edit->getText());
+    }
+    else if (whichEditor == genre_edit)
+    {
+        m_metadata->setGenre(genre_edit->getText());
+    }
+}
+
+bool EditRadioMetadataDialog::showList(QString caption, QString &value)
+{
+    bool res = false;
+    
+    MythSearchDialog *searchDialog = new MythSearchDialog(gContext->GetMainWindow(), "");
+    searchDialog->setCaption(caption);
+    searchDialog->setSearchText(value);
+    searchDialog->setItems(searchList);
+    if (searchDialog->ExecPopup() == 0)
+    {
+        value = searchDialog->getResult();
+        res = true;
+    }
+    
+    searchDialog->deleteLater ();
+    setActiveWindow();
+    
+    return res;     
+}
+
+void EditRadioMetadataDialog::fillSearchList(QString field, QString table)
+{
+    searchList.clear();
+    
+    QString querystr;
+    querystr = QString("SELECT DISTINCT %1 FROM %2 ORDER BY %3").
+        arg(field).arg(table).arg(field);
+         
+    MSqlQuery query(MSqlQuery::InitCon());
+    query.exec(querystr);
+
+    if (query.isActive() && query.size())
+    {
+        while (query.next())
+        {
+            searchList << QString::fromUtf8(query.value(0).toString());
+        }
+    }         
+}
+    
+void EditRadioMetadataDialog::searchStation()
+{
+    QString s;
+    
+    fillSearchList("station", "music_radios");
+    
+    s = m_metadata->Artist();
+    if (showList(tr("Select a Station"), s))
+    {
+        m_metadata->setArtist(s);
+        fillWidgets();
+    }
+}
+
+void EditRadioMetadataDialog::searchGenre()
+{
+    QString s;
+
+    // load genre list
+    searchList.clear();
+    for (int x = 0; x < genre_table_size; x++)
+        searchList.push_back(QString(genre_table[x]));
+    searchList.sort();
+
+    s = m_metadata->Genre();
+    if (showList(tr("Select a Genre"), s))
+    {
+        m_metadata->setGenre(s);
+        fillWidgets();
+    }
+   
+}
+
+void EditRadioMetadataDialog::closeDialog()
+{
+    cancelPopup();
+    done(0);  
+}
+
+void EditRadioMetadataDialog::showSaveMenu()
+{
+    popup = new MythPopupBox(gContext->GetMainWindow(), "Menu");
+
+    QLabel *label = popup->addLabel(tr("Save Changes?"), MythPopupBox::Large, false);
+    label->setAlignment(Qt::AlignCenter | Qt::WordBreak);
+
+    QButton *topButton = popup->addButton(tr("Save"), this,
+                         SLOT(saveToDatabase()));
+    popup->addButton(tr("Exit/Do Not Save"), this,
+                         SLOT(closeDialog()));
+    popup->addButton(tr("Cancel"), this, SLOT(cancelPopup()));
+
+    popup->ShowPopup(this, SLOT(cancelPopup()));
+
+    topButton->setFocus();
+}
+
+void EditRadioMetadataDialog::cancelPopup()
+{
+  if (!popup)
+      return;
+
+  popup->hide();
+  popup->deleteLater ();
+  popup = NULL;
+  setActiveWindow();
+}
+
+bool EditRadioMetadataDialog::verifyEntries()
+{
+    // TODO: check valid text in url and metaformat...
+    return true;
+}
+
+void EditRadioMetadataDialog::saveNewToDatabase()
+{
+    cancelPopup();
+
+    if (!verifyEntries()) 
+        return;
+
+    m_metadata->dumpToDatabase();        
+    *m_sourceMetadata = m_metadata;
+
+    done(1);
+}
+
+void EditRadioMetadataDialog::saveToDatabase()
+{
+    cancelPopup();
+
+    if (m_metadata->ID() == 0) 
+        return saveNewToDatabase();
+
+    m_metadata->dumpToDatabase();        
+    *m_sourceMetadata = m_metadata;
+
+    done(1);
+}
+
+EditRadioMetadataDialog::~EditRadioMetadataDialog()
+{
+    delete m_metadata;
+}
Index: mythplugins/mythmusic/mythmusic/miniplayer.cpp
===================================================================
--- mythplugins/mythmusic/mythmusic/miniplayer.cpp	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/miniplayer.cpp	(working copy)
@@ -40,7 +40,7 @@
     if (gPlayer->getCurrentMetadata())
     {
         m_maxTime = gPlayer->getCurrentMetadata()->Length() / 1000;
-        updateTrackInfo(gPlayer->getCurrentMetadata());
+        updateTrackInfo(gPlayer->getDisplayMetadata());
 
         if (!gPlayer->isPlaying())
         {
@@ -309,7 +309,7 @@
             if (gPlayer->getCurrentMetadata())
             {
                 m_maxTime = gPlayer->getCurrentMetadata()->Length() / 1000;
-                updateTrackInfo(gPlayer->getCurrentMetadata());
+                updateTrackInfo(gPlayer->getDisplayMetadata());
             }
             break;
         }
@@ -398,6 +398,26 @@
         {
             break;
         }
+        case DecoderEvent::Decoding:
+        {
+            updateTrackInfo(gPlayer->getDisplayMetadata());
+            break;
+        }          
+        case DecoderHandlerEvent::Info:
+        {
+            DecoderHandlerEvent *dxe = (DecoderHandlerEvent*)event;
+            Metadata tmp (*gPlayer->getDisplayMetadata());
+            tmp.setArtist("");
+            tmp.setTitle(*dxe->getMessage());            
+            updateTrackInfo(&tmp);
+            break;
+        }
+        case DecoderHandlerEvent::Meta:
+        {
+            DecoderHandlerEvent *dxe = (DecoderHandlerEvent*)event;
+            updateTrackInfo(dxe->getMetadata());
+            break;
+        }
     }
     QObject::customEvent(event);
 }
@@ -587,7 +607,7 @@
 {
     m_showingInfo = false;
     LCD *lcd = LCD::Get();
-    Metadata * mdata = gPlayer->getCurrentMetadata();
+    Metadata * mdata = gPlayer->getDisplayMetadata();
 
     if (lcd && mdata)
     {
Index: mythplugins/mythmusic/mythmusic/music-ui.xml
===================================================================
--- mythplugins/mythmusic/mythmusic/music-ui.xml	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/music-ui.xml	(working copy)
@@ -849,6 +849,155 @@
       </container>
    </window>
 
+   <window name="edit_radiometadata">
+
+      <font name="title" face="Arial">
+          <color>#ffff00</color>
+          <dropcolor>#000000</dropcolor>
+          <size>24</size>
+          <shadow>3,3</shadow>
+          <bold>yes</bold>
+      </font>
+
+      <font name="labels" face="Arial">
+          <color>#ffff00</color>
+          <dropcolor>#000000</dropcolor>
+          <size>18</size>
+          <shadow>3,3</shadow>
+          <bold>yes</bold>
+      </font>
+
+      <font name="display" face="Arial">
+          <color>#ffffff</color>
+          <dropcolor>#000000</dropcolor>
+          <size>18</size>
+          <shadow>3,3</shadow>
+          <bold>yes</bold>
+      </font>
+
+       <container name="edit_container">
+             <area>0,0,800,600</area>
+
+             <textarea name="title" draworder="1" align="center">
+                 <area>0,15,800,50</area>
+                 <font>title</font>
+                 <value>Track Information</value>
+             </textarea>
+
+             <!--
+                    Labels
+            -->
+
+
+             <textarea name="station_label" draworder="1" align="right">
+                 <area>15,70,170,30</area>
+                 <font>labels</font>
+                 <value>Station:</value>
+             </textarea>
+
+             <textarea name="channel_label" draworder="1" align="right">
+                 <area>15,110,170,30</area>
+                 <font>labels</font>
+                 <value>Channel:</value>
+             </textarea>
+
+             <textarea name="url_label" draworder="1" align="right">
+                 <area>15,150,170,30</area>
+                 <font>labels</font>
+                 <value>URL:</value>
+             </textarea>
+
+             <textarea name="metaformat_label" draworder="1" align="right">
+                 <area>15,190,170,30</area>
+                 <font>labels</font>
+                 <value>Meta Format:</value>
+             </textarea>
+
+             <textarea name="genre_label" draworder="1" align="right">
+                 <area>15,230,170,30</area>
+                 <font>labels</font>
+                 <value>Genre:</value>
+             </textarea>
+
+             <textarea name="rating_label" draworder="1" align="right">
+                 <area>15,350,170,30</area>
+                 <font>labels</font>
+                 <value>Rating:</value>
+             </textarea>
+
+             <!--
+                    edits
+            -->
+
+             <remoteedit name="station_edit" draworder="1" align="left">
+                 <area>195,70,525,35</area>
+                 <font>display</font>
+             </remoteedit> 
+
+             <pushbutton name="searchstation_button" draworder="2">
+                 <position>725,70</position>
+                 <image function="on" filename="blankbutton_on.png"> </image>
+                 <image function="off" filename="blankbutton_off.png"> </image>
+                 <image function="pushed" filename="blankbutton_pushed.png"> </image>
+             </pushbutton>
+
+             <remoteedit name="channel_edit" draworder="1" align="left">
+                 <area>195,110,525,35</area>
+                 <font>display</font>
+             </remoteedit>
+
+             <remoteedit name="url_edit" draworder="1" align="left">
+                 <area>195,150,525,35</area>
+                 <font>display</font>
+             </remoteedit>
+
+             <remoteedit name="metaformat_edit" draworder="1" align="left">
+                 <area>195,190,525,35</area>
+                 <font>display</font>
+             </remoteedit>
+
+             <remoteedit name="genre_edit" draworder="1" align="left">
+                 <area>195,230,525,35</area>
+                 <font>display</font>
+             </remoteedit>
+
+             <pushbutton name="searchgenre_button" draworder="2">
+                 <position>725,230</position>
+                 <image function="on" filename="blankbutton_on.png"> </image>
+                 <image function="off" filename="blankbutton_off.png"> </image>
+                 <image function="pushed" filename="blankbutton_pushed.png"> </image>
+             </pushbutton>
+
+             <repeatedimage name="rating_image" draworder="1" fleximage="no">
+                 <filename>mm_rating.png</filename>
+                 <position>190,360</position>
+                 <orientation>LeftToRight</orientation>
+             </repeatedimage>
+
+             <selector name="rating_button" draworder="0">
+                 <area>420,350,30,30</area>
+                 <font>display</font>
+                 <image function="on" filename="leftright_on.png"> </image>
+                 <image function="off" filename="leftright_off.png"> </image>
+                 <image function="pushed" filename="leftright_pushed.png"> </image>
+             </selector>
+
+
+             <!--
+                    Push buttons
+            -->
+
+             <textbutton name="done_button" draworder="0">
+                 <position>350,540</position>
+                 <font>display</font>
+                 <image function="on" filename="text_button_on.png"> </image>
+                 <image function="off" filename="text_button_off.png"> </image>
+                 <image function="pushed" filename="text_button_pushed.png"> </image>
+             </textbutton>
+
+       </container>
+    </window>
+
    <window name="cdripper">
 
        <font name="title" face="Arial">
Index: mythplugins/mythmusic/mythmusic/aacdecoder.cpp
===================================================================
--- mythplugins/mythmusic/mythmusic/aacdecoder.cpp	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/aacdecoder.cpp	(working copy)
@@ -175,17 +175,14 @@
         }
     }
 
-
     //
     //  See if we can seek
     //
-
     if (!input()->at(0))
     {
-      error("couldn't seek in input");
+        error("couldn't seek in input");
         return false;
     }
-    
 
     //
     //  figure out if it's an mp4 file (aac in a mp4 wrapper a la Apple) or
@@ -357,9 +354,9 @@
 
     // If max_bitrate == avg_bitrate, then file is fixed bitrate
     if (mp4ff_get_avg_bitrate(mp4_input_file, aac_track_number) ==
-	mp4ff_get_max_bitrate(mp4_input_file, aac_track_number))
+        mp4ff_get_max_bitrate(mp4_input_file, aac_track_number))
     {
-	bitrate = mp4ff_get_avg_bitrate(mp4_input_file, aac_track_number) / 1000;
+        bitrate = mp4ff_get_avg_bitrate(mp4_input_file, aac_track_number) / 1000;
     }
     
     //
@@ -506,7 +503,7 @@
 
             if (output())
             {
-		output()->Drain();
+                output()->Drain();
             }
 
             done = TRUE;
@@ -547,7 +544,7 @@
                                                    );
             
                 sample_count = frame_info.samples;
-
+                
                 //
                 //  Munge the samples into the "right" format and send them
                 //  to the output (after checking we're not going to exceed
@@ -576,24 +573,24 @@
                     output_bytes += sample_count * 2;
                     if (output())
                     {
-			// If source is VBR, bitrate == 0
-			if (bitrate)
-			{
-			    output()->SetSourceBitrate(bitrate);
-			} 
+                        // If source is VBR, bitrate == 0
+                        if (bitrate)
+                        {
+                            output()->SetSourceBitrate(bitrate);
+                        } 
                         else 
                         {
-			    output()->SetSourceBitrate(
-				(int) ((float) (frame_info.bytesconsumed * 8) /
-				       (frame_info.samples / 
-				        frame_info.num_front_channels) 
-				* frame_info.samplerate) / 1000);
-			}
-
+                            output()->SetSourceBitrate(
+                                (int) ((float) (frame_info.bytesconsumed * 8) /
+                                       (frame_info.samples / 
+                                        frame_info.num_front_channels) 
+                                       * frame_info.samplerate) / 1000);
+                        }
+                        
                         flush();
                     }
                 }
-            
+                
                 if (buffer)
                 {
                     free(buffer);
Index: mythplugins/mythmusic/mythmusic/decoder.h
===================================================================
--- mythplugins/mythmusic/mythmusic/decoder.h	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/decoder.h	(working copy)
@@ -37,8 +37,7 @@
 
     ~DecoderEvent()
     {
-        if (error_msg)
-            delete error_msg;
+        delete error_msg;
     }
 
     const QString *errorMessage() const { return error_msg; }
Index: mythplugins/mythmusic/mythmusic/visualize.cpp
===================================================================
--- mythplugins/mythmusic/mythmusic/visualize.cpp	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/visualize.cpp	(working copy)
@@ -17,7 +17,6 @@
 #include <qpainter.h>
 #include <qpixmap.h>
 #include <qimage.h>
-#include <qdir.h>
 #include <qurl.h>
 
 // mythtv
@@ -452,7 +451,7 @@
         (void)pluginName;
         return new AlbumArt(parent);
     }
-}AlbumArtFactory;
+} AlbumArtFactory;
 
 Blank::Blank()
     : VisualBase(true)
@@ -511,6 +510,8 @@
 {
     number_of_squares = 16;
     fake_height = number_of_squares * analyzerBarWidth;
+    startHue = 220;
+    targetHue = 360;
 }
 
 Squares::~Squares()
@@ -524,6 +525,11 @@
     size = newsize;
 }
 
+bool Squares::process(VisualNode *node) {
+    rotateHues ();
+    return Spectrum::process (node);
+}
+
 void Squares::drawRect(QPainter *p, QRect *rect, int i, int c, int w, int h) 
 {
     double r, g, b, per;
@@ -555,9 +561,22 @@
     g = clamp(g, 255.0, 0.0);
     b = clamp(b, 255.0, 0.0);
 
-    p->fillRect (x, y, w, h, QColor (int(r), int(g), int(b)));
+    p->fillRect (x, y, w, h, QColor((int)r, (int)g, (int)b));
 }
 
+void Squares::rotateHues () {
+    targetHue ++;
+    if (targetHue >= 360)
+        targetHue = 0;
+
+    startHue ++;
+    if (startHue >= 360)
+        startHue = 0;
+
+    startColor = QColor (startHue, 255, 255, QColor::Hsv);
+    targetColor = QColor (targetHue, 255, 255, QColor::Hsv);
+}
+
 bool Squares::draw(QPainter *p, const QColor &back)
 {
     p->fillRect (0, 0, size.width (), size.height (), back);
Index: mythplugins/mythmusic/mythmusic/mainvisual.h
===================================================================
--- mythplugins/mythmusic/mythmusic/mainvisual.h	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/mainvisual.h	(working copy)
@@ -115,6 +115,7 @@
     void showBanner(Metadata *meta, bool fullScreen, int visMode, int showTime = 8000);
     void hideBanner();
     bool bannerIsShowing(void) {return bannerTimer->isActive(); }
+	void clearInformation();
 
     static QStringList Visualizations();
 
@@ -158,6 +159,7 @@
     QString info;
     QPixmap info_pixmap;
     QRect   displayRect;
+	QColor  color;
 };
 
 class StereoScope : public VisualBase
Index: mythplugins/mythmusic/mythmusic/metadata.cpp
===================================================================
--- mythplugins/mythmusic/mythmusic/metadata.cpp	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/metadata.cpp	(working copy)
@@ -5,6 +5,7 @@
 #include <qregexp.h> 
 #include <qdatetime.h>
 #include <qdir.h>
+#include <qurl.h>
 
 using namespace std;
 
@@ -15,16 +16,39 @@
 
 // mythmusic
 #include "metadata.h"
+#include "editmetadata.h"
+#include "editradiometadata.h"
 #include "metaiotaglib.h"
 #include "treebuilders.h"
 #include "playlist.h"
 
+unsigned int AllMusic::REPO_ID_DB = 0;
+unsigned int AllMusic::REPO_ID_CD = 1;
+unsigned int AllMusic::REPO_ID_RADIO = 2;
 
 // this is the global MusicData object shared thoughout MythMusic
 MusicData  *gMusicData = NULL;
 
 static QString thePrefix = "the ";
 
+QVariant queryField (const QString &v, const QString &table, 
+                     const QString &q, const QVariant &qv)
+{
+    ;
+    
+    MSqlQuery query(MSqlQuery::InitCon());
+    query.prepare(QString("SELECT %1 FROM %2 WHERE %3 = :Q;").arg(v).arg(table).arg(q));
+    query.bindValue(":Q", qv);
+    
+    if (!query.exec()) {
+        MythContext::DBError(QString("Get %1 from %2 for %3 = %4").arg(v).arg(table).arg(q).arg(qv.toString()), query);           
+    } else if (query.isActive() && query.size() > 0)
+        while (query.next()) {
+            return query.value(0);
+        }                
+    return QVariant ();
+}
+
 bool operator==(const Metadata& a, const Metadata& b)
 {
     if (a.Filename() == b.Filename())
@@ -78,7 +102,7 @@
     query.bindValue(":RATING", m_rating);
     query.bindValue(":PLAYCOUNT", m_playcount);
     query.bindValue(":LASTPLAY", m_lastplay);
-    query.bindValue(":ID", m_id);
+    query.bindValue(":ID", AllMusic::id_to_id(ID()));
 
     if (!query.exec())
         MythContext::DBError("music persist", query);
@@ -88,12 +112,7 @@
 {
     if (m_format == "cast") 
     {
-        int artist_cmp = Artist().lower().localeAwareCompare(other->Artist().lower());
-
-        if (artist_cmp == 0) 
-            return Title().lower().localeAwareCompare(other->Title().lower());
-
-        return artist_cmp;
+        return compare_cast(other);
     }
     else
     {
@@ -106,6 +125,36 @@
     }
 }
 
+int Metadata::compare_cast(Metadata *other) 
+{
+    int artist_cmp = Artist().lower().localeAwareCompare(other->Artist().lower());
+    
+    if (artist_cmp == 0) 
+        return Title().lower().localeAwareCompare(other->Title().lower());
+    
+    return artist_cmp;
+}
+
+MythThemedDialog *Metadata::createEditorDialog(void) 
+{
+    /*note:temporary fix for radio*/
+    if (Format() == "cast")
+        return createEditorDialog_cast();
+
+    return new EditMetadataDialog(this, gContext->GetMainWindow(),
+                                  "edit_metadata", "music-", 
+                                  "edit metadata");
+    
+}
+
+MythThemedDialog *Metadata::createEditorDialog_cast(void) 
+{
+    return new EditRadioMetadataDialog(this, gContext->GetMainWindow(),
+                                       "edit_radiometadata", "music-", 
+                                       "edit metadata");
+    
+}
+
 bool Metadata::isInDatabase()
 {
     bool retval = false;
@@ -150,7 +199,7 @@
         m_year = query.value(5).toInt();
         m_tracknum = query.value(6).toInt();
         m_length = query.value(7).toInt();
-        m_id = query.value(8).toUInt();
+        setID(query.value(8).toUInt());
         m_rating = query.value(9).toInt();
         m_playcount = query.value(10).toInt();
         m_lastplay = query.value(11).toString();
@@ -165,6 +214,10 @@
 
 void Metadata::dumpToDatabase()
 {
+    /*note:temporary fix for radio*/
+    if (Format() == "cast") 
+        return dumpToDatabase_cast();
+
     QString sqlfilepath(m_filename);
     if (!sqlfilepath.contains("://"))
     {
@@ -210,7 +263,7 @@
             m_directoryid = query.lastInsertId().toInt();
         }
     }
-
+    
     if (m_artistid < 0)
     {
         // Load the artist id
@@ -339,7 +392,7 @@
 
     // We have all the id's now. We can insert it.
     QString strQuery;
-    if (m_id < 1)
+    if (AllMusic::id_to_id(ID()) < 1)
     {
         strQuery = "INSERT INTO music_songs ( directory_id,"
                    " artist_id, album_id,  name,         genre_id,"
@@ -384,15 +437,15 @@
     query.bindValue(":FORMAT", m_format);
     query.bindValue(":DATE_MOD", QDateTime::currentDateTime());
 
-    if (m_id < 1)
+    if (AllMusic::id_to_id(ID()) < 1)
         query.bindValue(":DATE_ADD",  QDateTime::currentDateTime());
     else
-        query.bindValue(":ID", m_id);
+        query.bindValue(":ID", AllMusic::id_to_id(ID()));
 
     query.exec();
 
-    if (m_id < 1 && query.isActive() && 1 == query.numRowsAffected())
-        m_id = query.lastInsertId().toInt();
+    if (AllMusic::id_to_id(ID()) < 1 && query.isActive() && 1 == query.numRowsAffected())
+        setID (query.lastInsertId().toInt());
 
     if (! m_albumart.empty())
     {
@@ -402,7 +455,7 @@
             query.prepare("SELECT albumart_id FROM music_albumart WHERE "
                           "song_id=:SONGID AND imagetype=:TYPE;");
             query.bindValue(":TYPE", (*it).imageType);
-            query.bindValue(":SONGID", m_id);
+            query.bindValue(":SONGID", AllMusic::id_to_id(ID()));
             query.exec();
 
             if (query.next())
@@ -425,7 +478,7 @@
 
             query.bindValue(":FILENAME", (*it).description);
             query.bindValue(":TYPE", (*it).imageType);
-            query.bindValue(":SONGID", m_id);
+            query.bindValue(":SONGID", AllMusic::id_to_id(ID()));
             query.bindValue(":EMBED", 1);
 
             query.exec();
@@ -446,6 +499,60 @@
     }
 }
 
+void Metadata::dumpToDatabase_cast()
+{
+    MSqlQuery query(MSqlQuery::InitCon());
+    query.prepare("SELECT url FROM music_radios WHERE "
+                  "( ( station = :STATION ) AND "
+                  "( channel = :CHANNEL ) AND "
+                  "( format = :FORMAT ) AND "
+                  "( url = :URL ) ); ");
+    query.bindValue(":STATION", m_artist.utf8());
+    query.bindValue(":CHANNEL", m_title.utf8());
+    query.bindValue(":URL", m_filename);
+    query.bindValue(":FORMAT", m_format);
+    
+    if (query.exec() && query.isActive() && query.size() > 0)
+        return;
+    
+    QString strQuery;
+    if (AllMusic::id_to_id(ID()) < 1) {
+        strQuery = "INSERT INTO music_radios "
+                   "(station,    channel,    url,        genre, "
+                   " metaformat, format,     rating            )"
+                   " VALUES "
+                   "(:STATION,     :CHANNEL,  :URL,     :GENRE,   "
+                  " :METAFORMAT,  :FORMAT,   :RATING );";
+    } else {
+        strQuery = "UPDATE music_radios          "
+                   "SET station   = :STATION,    "
+                   "    channel   = :CHANNEL,    "
+                   "    url       = :URL,        "
+                   "    genre     = :GENRE,      "
+                   "    metaformat= :METAFORMAT, "
+                   "    rating    = :RATING,     " 
+                   "    format    = :FORMAT      " 
+                   "WHERE intid = :ID;";
+    }
+    query.prepare(strQuery);
+    query.bindValue(":STATION", m_artist.utf8());
+    query.bindValue(":CHANNEL", m_title.utf8());
+    query.bindValue(":URL", m_filename);
+    query.bindValue(":GENRE", m_genre.utf8());
+    query.bindValue(":METAFORMAT", m_compilation_artist);
+    query.bindValue(":RATING", m_rating);
+    query.bindValue(":FORMAT", m_format);                      
+
+    if (AllMusic::id_to_id(ID()) >= 1) {
+        query.bindValue(":ID", AllMusic::id_to_id(ID()));
+    }
+
+    query.exec();
+
+    if (AllMusic::id_to_id(ID()) < 1 && query.isActive() && 1 == query.numRowsAffected())
+        setID(query.lastInsertId().toInt());
+}
+
 // Default values for formats
 // NB These will eventually be customizable....
 QString Metadata::m_formatnormalfileartist      = "ARTIST";
@@ -526,7 +633,6 @@
         m_title = m_filename;
     if (m_genre == "")
         m_genre = QObject::tr("Unknown Genre");
-
 }
 
 inline void Metadata::setCompilationFormatting(bool cd)
@@ -585,7 +691,58 @@
     return m_formattedtitle;
 }
 
+QString Metadata::FormatInfo()
+{       
+    QString result;
+    if (Format() == "cast") {
+        result = QString("\"%1\"\n"
+                         "%2\n"
+                         "Now playing on %3").
+            arg(FormatTitle()).
+            arg(FormatArtist()).
+            arg(Album());
+    } else {
+        result = QString("\"%1\"\n"
+                         "%2 - %3").
+        arg(FormatTitle()).
+            arg(FormatArtist()).
+            arg(Album());
+        
+        if (Year() > 0) 
+            result += " (" + QString::number (Year()) + ")";
+    }
 
+    return result;
+}
+
+void Metadata::removeFromDatabase()
+{
+    /*note:temporary fix for radio*/
+    if (Format() == "cast") 
+        return removeFromDatabase_cast();
+
+    MSqlQuery query(MSqlQuery::InitCon());
+
+    query.prepare("DELETE FROM musicmetadata "
+                  "WHERE intid = :ID;");
+    query.bindValue(":ID", AllMusic::id_to_id(ID()));
+
+    if (!query.exec())
+         MythContext::DBError("Remove musicmetadata", query);
+}
+
+void Metadata::removeFromDatabase_cast()
+{
+    MSqlQuery query(MSqlQuery::InitCon());
+
+    query.prepare("DELETE FROM music_radios "
+                  "WHERE intid = :ID;");
+    query.bindValue(":ID", AllMusic::id_to_id(ID()));
+
+    if (!query.exec())
+         MythContext::DBError("Remove music_radios", query);
+}
+
 void Metadata::setField(const QString &field, const QString &data)
 {
     if (field == "artist")
@@ -630,6 +787,13 @@
         *data = FormatTitle();
     else if (field == "genre")
         *data = m_genre;
+    else if (field == "x-cast-logourl") 
+    {
+        /*note:temporary fix for radio*/
+        if (Format() == "cast") {
+            *data = queryField ("logourl", "music_radios", "intid", AllMusic::id_to_id(ID())).toString ();
+        } 
+    }
     else
     {
         VERBOSE(VB_IMPORTANT, QString("Something asked me to return data "
@@ -638,6 +802,23 @@
     }
 }
 
+void Metadata::setAlbumArt(const QByteArray &data) {
+    /*note:temporary fix for radio*/
+    if (Format() == "cast") {
+        MSqlQuery query(MSqlQuery::InitCon());
+        query.prepare("UPDATE music_radios          "
+                      "SET logocached = :LOGO       "
+                      "WHERE intid = :ID;");
+        query.bindValue(":LOGO", data);
+        if (AllMusic::id_to_id(ID()) >= 1) {
+            query.bindValue(":ID", AllMusic::id_to_id(ID()));
+        }
+        
+        if (!query.exec())
+            MythContext::DBError("Store albumart", query);
+    }
+}
+
 void Metadata::decRating()
 {
     if (m_rating > 0)
@@ -728,6 +909,13 @@
 
 QImage Metadata::getAlbumArt(void)
 {
+    if (Format() == "cast") {
+        QVariant v = queryField ("logocached", "music_radios", "intid", AllMusic::id_to_id(ID()));
+        if (v.isValid()) {
+            return QImage (v.toByteArray());
+        }
+    }
+
     AlbumArtImages albumArt(this);
 
     QImage image;
@@ -762,6 +950,13 @@
 
     QImage image;
 
+    if (Format() == "cast") {
+        QVariant v = queryField ("logocached", "music_radios", "intid", AllMusic::id_to_id(ID()));
+        if (v.isValid()) {
+            image = QImage (v.toByteArray());
+        }
+    }
+    else
     if (albumArt.isImageAvailable(type))
     {
         AlbumArtImage albumart_image = albumArt.getImage(type);
@@ -775,10 +970,51 @@
     return image;
 }
 
-// static function to get a metadata object given a track id
+static Metadata* getRadioMetadataFromID (int id) {
+    QString the_query = 
+        "SELECT "
+        "url, station, channel, genre, intid, rating, format, metaformat "
+        "FROM music_radios "
+        "WHERE intid = :ID;";
+    
+    MSqlQuery query(MSqlQuery::InitCon());
+    query.prepare (the_query);
+    query.bindValue (":ID", id);
+    query.exec ();
+    
+    if (query.isActive() && query.size() > 0) {
+        query.next ();
+        Metadata *md = new Metadata(QString::fromUtf8(query.value(0).toString()),
+                                    QString::fromUtf8(query.value(1).toString()),
+                                    "", "",
+                                    QString::fromUtf8(query.value(2).toString()),
+                                    QString::fromUtf8(query.value(3).toString()),
+                                    0, 0, 0,
+                                    query.value(4).toInt(), query.value(5).toInt());
+        
+            // abuse...
+        md->setCompilationArtist(QString::fromUtf8(query.value(7).toString()));            
+        md->setFormat(query.value(6).toString());
+        md->setID (id);
+        md->setRepoID (AllMusic::REPO_ID_RADIO);
+        md->setAlbum(QString("%1 ~ %2").arg(md->FormatArtist()).arg(md->FormatTitle()));
+        return md;
+    }
+    return NULL;
+}
+
+ // static function to get a metadata object given a track id
 // it's upto the caller to delete the returned object when finished
-Metadata *Metadata::getMetadataFromID(int id)
+Metadata *Metadata::getMetadataFromID(Metadata::IdType an_id)
 {
+    Metadata::IdType repo = AllMusic::id_to_repo (an_id);
+    Metadata::IdType id = AllMusic::id_to_id (an_id);
+    
+    VERBOSE(VB_PLAYBACK, QString("getMetadataFromID (%1) = %2/%3").arg (an_id).arg (repo).arg (id));
+
+    if (repo == AllMusic::REPO_ID_RADIO) {
+        return getRadioMetadataFromID (AllMusic::id_to_id (id));
+    }
     Metadata *meta = NULL;
 
     QString aquery = "SELECT music_songs.song_id, music_artists.artist_name, music_comp_artists.artist_name AS compilation_artist, "
@@ -837,6 +1073,7 @@
             query.value(12).toString(), //lastplay
             (query.value(13).toInt() > 0), //compilation
             query.value(14).toString()); //format
+        meta->setRepoID (AllMusic::REPO_ID_DB);
     }
     else
     {
@@ -883,13 +1120,14 @@
     startLoading();
 
     m_all_music.setAutoDelete(true);
-
+    m_all_radios.setAutoDelete(true);
     m_last_listed = -1;
 }
 
 AllMusic::~AllMusic()
 {
     m_all_music.clear();
+    m_all_radios.clear();
 
     delete m_root_node;
 
@@ -944,7 +1182,6 @@
 void AllMusic::resync()
 {
     m_done_loading = false;
-
     QString aquery = "SELECT music_songs.song_id, music_artists.artist_name, music_comp_artists.artist_name AS compilation_artist, "
                      "music_albums.album_name, music_songs.name, music_genres.genre, music_songs.year, "
                      "music_songs.track, music_songs.length, CONCAT_WS('/', "
@@ -960,7 +1197,6 @@
                      "ORDER BY music_songs.song_id;";
 
     QString filename, artist, album, title;
-
     MSqlQuery query(MSqlQuery::InitCon());
     query.exec(aquery);
 
@@ -974,28 +1210,16 @@
     {
         while (query.next())
         {
-            filename = QString::fromUtf8(query.value(9).toString());
+            QString filename = QString::fromUtf8(query.value(9).toString());
             if (!filename.contains("://"))
                 filename = m_startdir + filename;
 
-            artist = QString::fromUtf8(query.value(1).toString());
-            if (artist.isEmpty())
-                artist = QObject::tr("Unknown Artist");
-
-            album = QString::fromUtf8(query.value(3).toString());
-            if (album.isEmpty())
-                album = QObject::tr("Unknown Album");
-
-            title = QString::fromUtf8(query.value(4).toString());
-            if (title.isEmpty())
-                title = QObject::tr("Unknown Title");
-
             Metadata *temp = new Metadata(
                 filename,
-                artist,
+                QString::fromUtf8(query.value(1).toString()),
                 QString::fromUtf8(query.value(2).toString()),
-                album,
-                title,
+                QString::fromUtf8(query.value(3).toString()),
+                QString::fromUtf8(query.value(4).toString()),
                 QString::fromUtf8(query.value(5).toString()),
                 query.value(6).toInt(),
                 query.value(7).toInt(),
@@ -1006,10 +1230,11 @@
                 query.value(12).toString(), //lastplay
                 (query.value(13).toInt() > 0), //compilation
                 query.value(14).toString()); //format
-
             //  Don't delete temp, as PtrList now owns it
             m_all_music.append(temp);
 
+            temp->setRepoID (AllMusic::REPO_ID_DB);
+
             // compute max/min playcount,lastplay for all music
             if (query.at() == 0)
             { // first song
@@ -1044,7 +1269,7 @@
     music_map.clear();
     while ( (map_add = an_iterator.current()) != 0 )
     {
-        music_map[map_add->ID()] = map_add; 
+        music_map[AllMusic::id_to_id(map_add->ID())] = map_add; 
         ++an_iterator;
     }
 
@@ -1055,9 +1280,57 @@
     //printTree();
     sortTree();
     //printTree();
+
+    resync_radios();
+
     m_done_loading = true;
 }
 
+/*note:temporary fix for radio*/
+void AllMusic::resync_radios(void)
+{
+    m_radio_map.clear();
+    m_all_radios.clear();
+
+    QString the_query = 
+        "SELECT "
+        "url, station, channel, genre, intid, rating, format, metaformat "
+        "FROM "
+        "music_radios ORDER BY station, channel";
+    
+    MSqlQuery query(MSqlQuery::InitCon());
+    
+    if (query.exec(the_query) && query.isActive() && query.size() > 0) {
+        while (query.next()) {
+            Metadata *md = new Metadata(QString::fromUtf8(query.value(0).toString()),
+                                        QString::fromUtf8(query.value(1).toString()),
+                                        "", "",
+                                        QString::fromUtf8(query.value(2).toString()),
+                                        QString::fromUtf8(query.value(3).toString()),
+                                        0, 0, 0,
+                                        query.value(4).toInt(), query.value(5).toInt());
+
+            // abuse...
+            md->setCompilationArtist(QString::fromUtf8(query.value(7).toString()));            
+            md->setFormat(query.value(6).toString());
+            md->setRepoID (REPO_ID_RADIO);
+            addRadioTrack(md);
+        } 
+        m_all_radios.sort ();
+    }    
+}
+
+void AllMusic::addRadioTrack(Metadata *meta)
+{    
+    QString title = meta->FormatArtist ();
+    title += " ~ ";
+    title += meta->FormatTitle();
+    meta->setAlbum(title);
+
+    m_all_radios.append(meta);
+    m_radio_map[AllMusic::id_to_id(meta->ID())] = meta;
+}
+
 void AllMusic::sortTree()
 {
     m_root_node->sort();
@@ -1101,9 +1374,69 @@
     delete builder;
 }
 
+/*note:temporary fix for radio*/
+#define RADIOS_PR_STATION 1
 void AllMusic::writeTree(GenericTree *tree_to_write_to)
 {
     m_root_node->writeTree(tree_to_write_to, 0);
+
+    /*note:temporary fix for radio*/
+    {
+        int rcounter = 0;
+        GenericTree *radio_root = tree_to_write_to->addNode(QObject::tr("All My Radios"), 
+                                                            REPO_ID_RADIO << METADATA_BITS_REPO_LEFT_SHIFT, 
+                                                            false);
+        radio_root->setAttribute(0, 0);
+        radio_root->setAttribute(1, 0);
+        radio_root->setAttribute(2, 0);
+        radio_root->setAttribute(3, 0);
+        radio_root->setAttribute(4, 0);
+        radio_root->setAttribute(5, 0);
+        
+#if RADIOS_PR_STATION
+        QMap<QString,GenericTree*> radio_stations;
+#endif
+        QPtrListIterator<Metadata> iterator(m_all_radios);
+        Metadata *mdata;
+
+        while ((mdata = iterator.current()) != 0)
+        {
+#if RADIOS_PR_STATION
+            if (radio_stations[mdata->Artist()] == 0) {
+                GenericTree *sub = 
+                    radio_root->addNode(mdata->FormatArtist(), 
+                                        REPO_ID_RADIO << METADATA_BITS_REPO_LEFT_SHIFT, 
+                                        false);
+                radio_stations[mdata->Artist()] = sub;
+                sub->setAttribute(0, 1);
+                sub->setAttribute(1, rcounter);
+                sub->setAttribute(2, rand());
+                sub->setAttribute(3, rand());
+                sub->setAttribute(4, rcounter);
+                sub->setAttribute(5, rcounter);
+                rcounter++;
+            }
+#endif
+            QString title = mdata->Album();
+#if RADIOS_PR_STATION
+            title = mdata->Title();
+#endif
+                
+            GenericTree *insert_at = radio_root;
+#if RADIOS_PR_STATION 
+           insert_at = radio_stations[mdata->Artist()];
+#endif
+           GenericTree *sub = insert_at->addNode(title, mdata->ID(), true);            
+            sub->setAttribute(0, 1);
+            sub->setAttribute(1, rcounter);
+            sub->setAttribute(2, rand());
+            sub->setAttribute(3, rand());
+            sub->setAttribute(4, rcounter);
+            sub->setAttribute(5, rcounter);
+            ++rcounter;
+            ++iterator;
+        }        
+    }
 }
 
 bool AllMusic::putYourselfOnTheListView(TreeCheckItem *where)
@@ -1134,22 +1467,25 @@
     }
 }
 
-QString AllMusic::getLabel(int an_id, bool *error_flag)
+QString AllMusic::getLabel(Metadata::IdType an_id, bool *error_flag)
 {
     QString a_label = "";
-    if(an_id > 0)
+    Metadata::IdType id = id_to_id (an_id);
+    Metadata::IdType repo = id_to_repo (an_id);
+
+    if (repo == REPO_ID_DB)
     {
-   
-        if (!music_map.contains(an_id))
+        if (!music_map.contains(id))
         {
-            a_label = QString(QObject::tr("Missing database entry: %1")).arg(an_id);
+            a_label = QString(QObject::tr("Missing database entry: %1")).arg(id);
             *error_flag = true;
             return a_label;
         }
+
       
-        a_label += music_map[an_id]->FormatArtist();
+        a_label += music_map[id]->FormatArtist();
         a_label += " ~ ";
-        a_label += music_map[an_id]->FormatTitle();
+        a_label += music_map[id]->FormatTitle();
     
 
         if(a_label.length() < 1)
@@ -1163,12 +1499,20 @@
         }
         return a_label;
     }
-    else
+    else if (repo == REPO_ID_RADIO)
     {
+        a_label += m_radio_map[id]->FormatArtist();
+        a_label += " ~ ";
+        a_label += m_radio_map[id]->FormatTitle();
+        *error_flag = false;
+        return a_label;
+    }
+    else if (repo == REPO_ID_CD)
+    {
         ValueMetadata::iterator anit;
         for(anit = m_cd_data.begin(); anit != m_cd_data.end(); ++anit)
         {
-            if( (*anit).Track() == an_id * -1)
+            if( (Metadata::IdType)(*anit).Track() == id)
             {
                 a_label = QString("(CD) %1 ~ %2").arg((*anit).FormatArtist())
                                                  .arg((*anit).FormatTitle());
@@ -1183,21 +1527,31 @@
     return a_label;
 }
 
-Metadata* AllMusic::getMetadata(int an_id)
+Metadata* AllMusic::getMetadata(Metadata::IdType an_id)
 {
-    if(an_id > 0)
+    Metadata::IdType id = id_to_id (an_id);
+    Metadata::IdType repo = id_to_repo (an_id);
+    
+    if (repo == REPO_ID_RADIO) {
+        if (m_radio_map.contains(id))
+        {
+            return m_radio_map[id];    
+        }
+        return NULL;
+    } 
+    else if (repo == REPO_ID_DB)
     {
-        if (music_map.contains(an_id))
+        if (music_map.contains(id))
         {
-            return music_map[an_id];    
+            return music_map[id];    
         }
     }
-    else if(an_id < 0)
+    else if (repo == REPO_ID_CD)
     {
         ValueMetadata::iterator anit;
         for(anit = m_cd_data.begin(); anit != m_cd_data.end(); ++anit)
         {
-            if( (*anit).Track() == an_id * -1)
+            if( (Metadata::IdType)(*anit).Track() == id)
             {
                 return &(*anit);
             }
@@ -1206,9 +1560,12 @@
     return NULL;
 }
 
-bool AllMusic::updateMetadata(int an_id, Metadata *the_track)
+
+
+bool AllMusic::updateMetadata(Metadata::IdType an_id, Metadata *the_track)
 {
-    if(an_id > 0)
+    Metadata::IdType repo = id_to_repo (an_id);    
+    if(repo == REPO_ID_DB || repo == REPO_ID_RADIO)
     {
         Metadata *mdata = getMetadata(an_id);
         if (mdata)
@@ -1373,7 +1730,7 @@
                                   .arg(a_track->Track()).arg(a_track->Title());
         QString level_temp = QObject::tr("title");
         TreeCheckItem *new_item = new TreeCheckItem(current_parent, title_temp,
-                                                    level_temp, a_track->ID());
+                                                    level_temp, AllMusic::id_to_id(a_track->ID()));
         ++anit;
         new_item->setCheck(false); //  Avoiding -Wall     
     }  
@@ -1407,7 +1764,7 @@
     while( (a_track = anit.current() ) != 0)
     {
         QString title_temp = QString(QObject::tr("%1 - %2")).arg(a_track->Track()).arg(a_track->Title());
-        GenericTree *subsub_node = sub_node->addNode(title_temp, a_track->ID(), true);
+        GenericTree *subsub_node = sub_node->addNode(title_temp, AllMusic::id_to_id(a_track->ID()), true);
         subsub_node->setAttribute(0, 1);
         subsub_node->setAttribute(1, track_counter);    // regular order
         subsub_node->setAttribute(2, rand());           // random order
@@ -1538,8 +1895,8 @@
     : m_parent(metadata)
 {
     m_imageList.setAutoDelete(true);
-
-    findImages();
+    if (m_parent)
+        findImages();
 }
 
 void AlbumArtImages::findImages(void)
@@ -1549,7 +1906,7 @@
     if (m_parent == NULL)
         return;
 
-    int trackid = m_parent->ID();
+    int trackid = AllMusic::id_to_id(m_parent->ID());
 
     if (trackid == 0)
         return;
Index: mythplugins/mythmusic/mythmusic/metaio.h
===================================================================
--- mythplugins/mythmusic/mythmusic/metaio.h	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/metaio.h	(working copy)
@@ -16,6 +16,7 @@
     
     virtual bool write(Metadata* mdata, bool exclusive = false) = 0;
     virtual Metadata* read(QString filename) = 0;
+	virtual QByteArray getAlbumArt(const QString &filename);
 
     void readFromFilename(QString filename, QString &artist, QString &album, 
                           QString &title, QString &genre, int &tracknum);
Index: mythplugins/mythmusic/mythmusic/dbcheck.cpp
===================================================================
--- mythplugins/mythmusic/mythmusic/dbcheck.cpp	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/dbcheck.cpp	(working copy)
@@ -10,9 +10,663 @@
 #include "mythtv/mythdbcon.h"
 
 const QString currentDatabaseVersion = "1013";
+//----------------------------------------------------------------------------
+// The radio number is to seperate radios from the main. As long
+// as this patch isn't in the main svn trunk, it's a hassle to 
+// share the number. When and if in the svn trunk, use the next
+// db version number. This is a cut'n'paste of the code 
+// just to tweak to use another field in the settings table.
+const QString radioCurrentDatabaseVersion = "5";
+static void RadioUpdateDBVersionNumber(const QString &newnumber)
+{
+    MSqlQuery query(MSqlQuery::InitCon());
 
+    query.exec("DELETE FROM settings WHERE value='RadioMusicDBSchemaVer';");
+    query.exec(QString("INSERT INTO settings (value, data, hostname) "
+                          "VALUES ('RadioMusicDBSchemaVer', %1, NULL);")
+                         .arg(newnumber));
+}
+static void RadioPerformActualUpdate(const QString updates[], QString version,
+                                QString &dbver)
+{
+    VERBOSE(VB_IMPORTANT, QString("Upgrading to MythMusicRadio schema version ") + 
+            version);
+
+    MSqlQuery query(MSqlQuery::InitCon());
+
+    int counter = 0;
+    QString thequery = updates[counter];
+
+    while (thequery != "")
+    {
+        query.exec(thequery);
+        counter++;
+        thequery = updates[counter];
+    }
+
+    RadioUpdateDBVersionNumber(version);
+    dbver = version;
+}
+static void RadioUpgrade() {
+    QString dbver = gContext->GetSetting("RadioMusicDBSchemaVer");
+    VERBOSE(VB_IMPORTANT, QString("RadioUpgrade dbversion='%1'").arg(dbver));
+    if (dbver == radioCurrentDatabaseVersion) return;
+    if (dbver == "") {
+        const QString updates[] = {
+            "DROP TABLE IF EXISTS music_radios;",
+            "CREATE TABLE music_radios ("
+            "    intid INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,"
+            "    station VARCHAR(128) NOT NULL,"
+            "    channel VARCHAR(128) NOT NULL,"
+            "    url VARCHAR(128) NOT NULL,"
+            "    logourl VARCHAR(128) NOT NULL,"
+            "    logocached BLOB,"
+            "    genre VARCHAR(128) NOT NULL,"
+            "    metaformat VARCHAR(128) NOT NULL,"
+            "    format VARCHAR(10) NOT NULL,"
+            "    rating INT UNSIGNED NOT NULL DEFAULT 5,"
+            "    INDEX (station),"
+            "    INDEX (channel)"
+            ");",
+        
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Space Station Soma\","
+            "    url=\"http://somafm.com/spacestation\","
+            "    logourl=\"http://img.somafm.com/img/sss.jpg\","
+            "    genre=\"Electronica\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Doomed\","
+            "    url=\"http://somafm.com/doomed\","
+            "    logourl=\"http://img.somafm.com/img/doomed.gif\","
+            "    genre=\"Darkwave\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Beatblender\","
+            "    url=\"http://somafm.com/beatblender\","
+            "    logourl=\"http://img.somafm.com/img/blender.gif\","
+            "    genre=\"Ambient\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Cliqhop idm\","
+            "    url=\"http://somafm.com/cliqhop\","
+            "    logourl=\"http://img.somafm.com/img/cliqhop.gif\","
+            "    genre=\"Dance\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Indie Pop Rocks\","
+            "    url=\"http://somafm.com/indiepop\","
+            "    logourl=\"http://img.somafm.com/img/indychick.jpg\","
+            "    genre=\"Indie\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Tag's Trance Trip\","
+            "    url=\"http://somafm.com/tagstrance\","
+            "    logourl=\"http://img.somafm.com/img/tagstrancefract.jpg\","
+            "    genre=\"Trance\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Drone Zone\","
+            "    url=\"http://somafm.com/dronezone\","
+            "    logourl=\"http://img.somafm.com/img/DroneZoneBox.jpg\","
+            "    genre=\"Ambient\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Secret Agent\","
+            "    url=\"http://somafm.com/secretagent\","
+            "    logourl=\"http://img.somafm.com/img/SecretAgentBox.jpg\","
+            "    genre=\"Lounge\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Groove Salad\","
+            "    url=\"http://somafm.com/groovesalad\","
+            "    logourl=\"http://img.somafm.com/img/GrooveSaladBox.jpg\","
+            "    genre=\"Ambient\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Illinois Street Lounge\","
+            "    url=\"http://somafm.com/illstreet\","
+            "    logourl=\"http://img.somafm.com/img/illstreet.jpg\","
+            "    genre=\"Lounge\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"trance\","
+            "    url=\"http://di.fm/mp3/trance.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Trance\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"vocaltrance\","
+            "    url=\"http://di.fm/mp3/vocaltrance.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Trance\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"chillout\","
+            "    url=\"http://di.fm/mp3/chillout.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Electronica\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"house\","
+            "    url=\"http://di.fm/mp3/house.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Techno\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"harddance\","
+            "    url=\"http://di.fm/mp3/harddance.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Techno\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"eurodance\","
+            "    url=\"http://di.fm/mp3/eurodance.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Techno\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"progressive\","
+            "    url=\"http://di.fm/mp3/progressive.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Techno\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"goapsy\","
+            "    url=\"http://di.fm/mp3/goapsy.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Techno\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"hardcore\","
+            "    url=\"http://di.fm/mp3/hardcore.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"djmixes\","
+            "    url=\"http://di.fm/mp3/djmixes.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"lounge\","
+            "    url=\"http://di.fm/mp3/lounge.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Lounge\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"ambient\","
+            "    url=\"http://di.fm/mp3/ambient.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"drumandbass\","
+            "    url=\"http://di.fm/mp3/drumandbass.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Techno\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"classictechno\","
+            "    url=\"http://di.fm/mp3/classictechno.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Techno\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"breaks\","
+            "    url=\"http://di.fm/mp3/breaks.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Techno\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"gabber\","
+            "    url=\"http://di.fm/mp3/gabber.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Techno\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"classical\","
+            "    url=\"http://di.fm/mp3/classical.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Classical\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"newage\","
+            "    url=\"http://di.fm/mp3/newage.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Newage\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"guitar\","
+            "    url=\"http://di.fm/mp3/guitar.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Instrumental\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"smoothjazz\","
+            "    url=\"http://di.fm/mp3/smoothjazz.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"tophits\","
+            "    url=\"http://di.fm/mp3/tophits.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Pop\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"the80s\","
+            "    url=\"http://di.fm/mp3/the80s.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"rootsreggae\","
+            "    url=\"http://di.fm/mp3/rootsreggae.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Reggae\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"hit70s\","
+            "    url=\"http://di.fm/mp3/hit70s.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"country\","
+            "    url=\"http://di.fm/mp3/country.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Country\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"jazz\","
+            "    url=\"http://di.fm/mp3/jazz.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Jazz\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+
+            "INSERT INTO music_radios SET station = \"d.i.\", "
+            "    channel = \"salsa\","
+            "    url=\"http://di.fm/mp3/salsa.pls\","
+            "    logourl=\"http://forums.di.fm/images/di/logo_blue.gif\","
+            "    genre=\"Salsa\","
+            "    metaformat=\"%t - %a\","
+            "    format=\"cast\";",
+
+            ""
+        };
+        RadioPerformActualUpdate(updates, "1", dbver);
+    }
+    if (dbver == "1") {
+        const QString updates[] = {
+            "ALTER TABLE music_radios ADD logocached BLOB;",
+            ""
+        };
+        RadioPerformActualUpdate(updates, "2", dbver);
+    }
+    if (dbver == "2") {
+        const QString updates[] = {
+            "DELETE FROM music_radios WHERE station = \"SomaFM\";",
+        
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Space Station Soma\","
+            "    url=\"http://somafm.com/spacestation.pls\","
+            "    logourl=\"http://img.somafm.com/img/sss.jpg\","
+            "    genre=\"Electronica\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Doomed\","
+            "    url=\"http://somafm.com/doomed.pls\","
+            "    logourl=\"http://img.somafm.com/img/doomed.gif\","
+            "    genre=\"Darkwave\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Beatblender\","
+            "    url=\"http://somafm.com/beatblender.pls\","
+            "    logourl=\"http://img.somafm.com/img/blender.gif\","
+            "    genre=\"Ambient\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Cliqhop idm\","
+            "    url=\"http://somafm.com/cliqhop.pls\","
+            "    logourl=\"http://img.somafm.com/img/cliqhop.gif\","
+            "    genre=\"Dance\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Indie Pop Rocks\","
+            "    url=\"http://somafm.com/indiepop.pls\","
+            "    logourl=\"http://img.somafm.com/img/indychick.jpg\","
+            "    genre=\"Indie\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Tag's Trance Trip\","
+            "    url=\"http://somafm.com/tagstrance.pls\","
+            "    logourl=\"http://img.somafm.com/img/tagstrancefract.jpg\","
+            "    genre=\"Trance\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Drone Zone\","
+            "    url=\"http://somafm.com/dronezone.pls\","
+            "    logourl=\"http://img.somafm.com/img/DroneZoneBox.jpg\","
+            "    genre=\"Ambient\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Secret Agent\","
+            "    url=\"http://somafm.com/secretagent.pls\","
+            "    logourl=\"http://img.somafm.com/img/SecretAgentBox.jpg\","
+            "    genre=\"Lounge\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+        
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Groove Salad\","
+            "    url=\"http://somafm.com/groovesalad.pls\","
+            "    logourl=\"http://img.somafm.com/img/GrooveSaladBox.jpg\","
+            "    genre=\"Ambient\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Illinois Street Lounge\","
+            "    url=\"http://somafm.com/illstreet.pls\","
+            "    logourl=\"http://img.somafm.com/img/illstreet.jpg\","
+            "    genre=\"Lounge\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+
+            "INSERT INTO music_radios SET station = \"SomaFM\", "
+            "    channel = \"Boot Liquor\","
+            "    url=\"http://somafm.com/bootliquor.pls\","
+            "    logourl=\"http://img.somafm.com/img/bootliquor.gif\","
+            "    genre=\"Rock\","
+            "    metaformat=\"%a - %t\","
+            "    format=\"cast\";",
+            ""
+        };
+        
+        RadioPerformActualUpdate(updates, "3", dbver);
+    }
+    if (dbver == "3") {
+        const QString updates[] = {
+            "DELETE FROM music_radios WHERE station = \"d.i.\";",
+            "DELETE FROM music_radios WHERE station = \"di.fm\";",
+
+            "INSERT INTO music_radios SET station = \"di.fm\", "
+            "	channel = \"Electro House\"," 
+            "	url = \"http://di.fm/mp3/electro.pls\"," 
+            "	logourl = \"http://www.di.fm/images/chbuttons/electro.gif\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"di.fm\", "
+            "	channel = \"Tribal House\"," 
+            "	url = \"http://di.fm/mp3/tribalhouse.pls\"," 
+            "	logourl = \"http://www.di.fm/images/chbuttons/tribalhouse.gif\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"di.fm\", "
+            "	channel = \"Funky House\"," 
+            "	url = \"http://di.fm/mp3/funkyhouse.pls\"," 
+            "	logourl = \"http://www.di.fm/images/chbuttons/funkyhouse.gif\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"di.fm\", "
+            "	channel = \"Minimal\"," 
+            "	url = \"http://di.fm/mp3/minimal.pls\"," 
+            "	logourl = \"http://www.di.fm/images/chbuttons/minimal.gif\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"di.fm\", "
+            "	channel = \"Hardstyle\"," 
+            "	url = \"http://di.fm/mp3/hardstyle.pls\"," 
+            "	logourl = \"http://www.di.fm/images/chbuttons/hardstyle.gif\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"di.fm\", "
+            "	channel = \"Trance\"," 
+            "	url = \"http://di.fm/mp3/trance.pls\"," 
+            "	logourl = \"\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"di.fm\", "
+            "	channel = \"Chillout\"," 
+            "	url = \"http://di.fm/mp3/chillout.pls\"," 
+            "	logourl = \"\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"di.fm\", "
+            "	channel = \"House\"," 
+            "	url = \"http://di.fm/mp3/house.pls\"," 
+            "	logourl = \"\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"di.fm\", "
+            "	channel = \"Techno\"," 
+            "	url = \"http://di.fm/mp3/techno.pls\"," 
+            "	logourl = \"\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"di.fm\", "
+            "	channel = \"Progressive\"," 
+            "	url = \"http://di.fm/mp3/progressive.pls\"," 
+            "	logourl = \"\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"di.fm\", "
+            "	channel = \"Lounge\"," 
+            "	url = \"http://di.fm/mp3/lounge.pls\"," 
+            "	logourl = \"\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"di.fm\", "
+            "	channel = \"Ambient\"," 
+            "	url = \"http://di.fm/mp3/ambient.pls\"," 
+            "	logourl = \"\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"di.fm\", "
+            "	channel = \"Breaks\"," 
+            "	url = \"http://di.fm/mp3/breaks.pls\"," 
+            "	logourl = \"\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"di.fm\", "
+            "	channel = \"Gabber\"," 
+            "	url = \"http://di.fm/mp3/gabber.pls\"," 
+            "	logourl = \"\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+        
+            ""
+        };
+        
+        RadioPerformActualUpdate(updates, "4", dbver);
+    }
+    if (dbver == "4") {
+        const QString updates[] = {
+            "DELETE FROM music_radios WHERE station = \"SKY.fm\";",
+
+            "INSERT INTO music_radios SET station = \"SKY.fm\", "
+            "	channel = \"Love Music\"," 
+            "	url = \"http://www.sky.fm/mp3/lovemusic.pls\"," 
+            "	logourl = \"http://www.sky.fm/images/chbuttons/lovemusic.gif\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"SKY.fm\", "
+            "	channel = \"Beatles Tribute\"," 
+            "	url = \"http://www.sky.fm/mp3/beatles.pls\"," 
+            "	logourl = \"http://www.sky.fm/images/chbuttons/beatles.gif\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"SKY.fm\", "
+            "	channel = \"Smooth Jazz\"," 
+            "	url = \"http://www.sky.fm/mp3/smoothjazz.pls\"," 
+            "	logourl = \"http://www.sky.fm/images/chbuttons/smoothjazz.gif\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"SKY.fm\", "
+            "	channel = \"Uptempo/Smooth Jazz\"," 
+            "	url = \"http://www.sky.fm/mp3/uptemposmoothjazz.pls\"," 
+            "	logourl = \"http://www.sky.fm/images/chbuttons/uptemposmoothjazz.gif\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"SKY.fm\", "
+            "	channel = \"Top Hits Music\"," 
+            "	url = \"http://www.sky.fm/mp3/tophits.pls\"," 
+            "	logourl = \"http://www.sky.fm/images/chbuttons/tophits.gif\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"SKY.fm\", "
+            "	channel = \"Best of the 80s\"," 
+            "	url = \"http://www.sky.fm/mp3/the80s.pls\"," 
+            "	logourl = \"http://www.sky.fm/images/chbuttons/the80s.gif\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            "INSERT INTO music_radios SET station = \"SKY.fm\", "
+            "	channel = \"All Hit 70s\"," 
+            "	url = \"http://www.sky.fm/mp3/hit70s.pls\"," 
+            "	logourl = \"http://www.sky.fm/images/chbuttons/hit70s.gif\"," 
+            "	genre = \"\"," 
+            "	metaformat = \"%a - %t\"," 
+            "	format = \"cast\";", 
+
+            
+        
+            ""
+        };
+        
+        RadioPerformActualUpdate(updates, "5", dbver);
+    }
+}
+//----------------------------------------------------------------------------
+
 static bool UpdateDBVersionNumber(const QString &newnumber)
-{   
+{
+    MSqlQuery query(MSqlQuery::InitCon());
 
     if (!gContext->SaveSettingOnHost("MusicDBSchemaVer",newnumber,NULL))
     {   
@@ -67,6 +721,10 @@
 bool UpgradeMusicDatabaseSchema(void)
 {
     QString dbver = gContext->GetSetting("MusicDBSchemaVer");
+
+    // Once in the main trunk, the radioupgrade code goes back in here, but as a patch,
+    // it's way easier to just have a call here.
+    RadioUpgrade();
     
     if (dbver == currentDatabaseVersion)
         return true;
@@ -351,7 +1009,6 @@
             return false;
     }
 
-
     if (dbver == "1005")
     {
         const QString updates[] = {
@@ -617,6 +1274,5 @@
 //"DROP TABLE musicmetadata;",
 //"DROP TABLE musicplaylist;",
 
-
     return true;
 }
Index: mythplugins/mythmusic/mythmusic/metadata.h
===================================================================
--- mythplugins/mythmusic/mythmusic/metadata.h	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/metadata.h	(working copy)
@@ -1,6 +1,8 @@
 #ifndef METADATA_H_
 #define METADATA_H_
 
+#include <stdexcept>
+
 // qt
 #include <qstring.h>
 #include <qstringlist.h>
@@ -40,13 +42,18 @@
     bool      embedded;
 } AlbumArtImage;
 
+#define METADATA_BITS_FOR_REPO 4
+#define METADATA_BITS_REPO_LEFT_SHIFT ((sizeof (unsigned int) * CHAR_BIT) - METADATA_BITS_FOR_REPO)
+#define METADATA_BITS_REPO_RIGHT_SIDE (UINT_MAX >> METADATA_BITS_FOR_REPO)
 
 class Metadata
 {
   public:
+    typedef unsigned int IdType;
+  public:
     Metadata(QString lfilename = "", QString lartist = "", QString lcompilation_artist = "",
              QString lalbum = "", QString ltitle = "", QString lgenre = "",
-             int lyear = 0, int ltracknum = 0, int llength = 0, int lid = 0,
+             int lyear = 0, int ltracknum = 0, int llength = 0, IdType lid = 0,
              int lrating = 0, int lplaycount = 0, QString llastplay = "",
              bool lcompilation = false, QString lformat="")
                 : m_artist(lartist),
@@ -110,8 +117,9 @@
 
     QString FormatArtist();
     QString FormatTitle();
+	QString FormatInfo();
 
-    QString Genre() { return m_genre; }
+    QString Genre() const { return m_genre; }
     void setGenre(const QString &lgenre) { m_genre = lgenre; }
 
     void setDirectoryId(int ldirectoryid) { m_directoryid = ldirectoryid; }
@@ -139,14 +147,27 @@
     void setPlaycount(int lplaycount) { m_playcount = lplaycount; }
 
     unsigned int ID() const { return m_id; }
-    void setID(int lid) { m_id = lid; }
+    void setID(int lid) { 
+        if (lid >= (1 << METADATA_BITS_REPO_LEFT_SHIFT))
+            throw std::logic_error ("ID is too big");
+        m_id = (repoID() << METADATA_BITS_REPO_LEFT_SHIFT) + lid; 
+	}
 
+	unsigned int repoID () const { return m_id >> METADATA_BITS_REPO_LEFT_SHIFT; }
+	void setRepoID (unsigned int rid) { 
+        if (rid >= (1 << METADATA_BITS_FOR_REPO))
+            throw std::logic_error ("ID is too big");
+        m_id = (rid << METADATA_BITS_REPO_LEFT_SHIFT) + ID(); 
+    }
+
     QString Filename() const { return m_filename; }
     void setFilename(const QString &lfilename) { m_filename = lfilename; }
 
     QString Format() const { return m_format; }
     void setFormat(const QString &lformat) { m_format = lformat; }
 
+	void setAlbumArt(const QByteArray &data);
+
     int Rating() const { return m_rating; }
     void decRating();
     void incRating();
@@ -174,18 +195,21 @@
 
     bool isInDatabase(void);
     void dumpToDatabase(void);
+	void removeFromDatabase(void);
     void setField(const QString &field, const QString &data);
     void getField(const QString& field, QString *data);
     void persist();
     bool hasChanged() {return m_changed;}
     int compare (Metadata *other);
+
+    MythThemedDialog *createEditorDialog (void);
+
     static void setArtistAndTrackFormats();
-
     static void SetStartdir(const QString &dir);
     static QString GetStartdir() { return m_startdir; }
 
     static QStringList fillFieldList(QString field);
-    static Metadata *getMetadataFromID(int id);
+    static Metadata *getMetadataFromID(Metadata::IdType an_id);
 
     // this looks for any image available - preferring a front cover if available
     QImage getAlbumArt(void);
@@ -219,7 +243,7 @@
     bool m_compilation;
     QValueList<struct AlbumArtImage> m_albumart;
 
-    unsigned int m_id;
+    IdType m_id;
     QString m_filename;
     bool    m_changed;
 
@@ -237,6 +261,13 @@
     static QString m_formatcompilationfiletrack;
     static QString m_formatcompilationcdartist;
     static QString m_formatcompilationcdtrack;
+
+    /*note:temporary fix for radio. These should be split out by subclassing Metadata */
+    int compare_cast (Metadata *other);
+    MythThemedDialog *createEditorDialog_cast (void);
+    void dumpToDatabase_cast();
+    void updateDatabase_cast();
+    void removeFromDatabase_cast();
 };
 
 bool operator==(const Metadata& a, const Metadata& b);
@@ -340,15 +371,17 @@
     AllMusic(QString path_assignment, QString a_startdir);
     ~AllMusic();
 
-    QString     getLabel(int an_id, bool *error_flag);
-    Metadata*   getMetadata(int an_id);
-    bool        updateMetadata(int an_id, Metadata *the_track);
+    QString     getLabel(unsigned int an_id, bool *error_flag);
+    Metadata*   getMetadata(unsigned int an_id);
+    bool        updateMetadata(unsigned int an_id, Metadata *the_track);
     int         count() const { return m_numPcs; }
     int         countLoaded() const { return m_numLoaded; } 
     void        save();
     bool        startLoading(void);
     void        resync();   //  After a CD rip, for example
+    void        resync_radios(void); /*note:temporary fix for radio*/
     void        clearCDData();
+    void        addRadioTrack(Metadata *the_track); /*note:temporary fix for radio*/
     void        addCDTrack(Metadata *the_track);
     bool        checkCDTrack(Metadata *the_track);
     bool        getCDMetadata(int m_the_track, Metadata *some_metadata);
@@ -371,6 +404,8 @@
   private:
   
     MetadataPtrList     m_all_music;
+    MetadataPtrList     m_all_radios; /*note:temporary fix for radio*/
+    QMap<Metadata::IdType,Metadata*> m_radio_map;  /*note:temporary fix for radio*/
     MusicNode           *m_root_node;
     
     int m_numPcs;
@@ -381,7 +416,7 @@
     //  so they still reference the correct data
     //  If, however, you create or delete metadata
     //  you NEED to clear and rebuild the map
-    typedef QMap<int, Metadata*> MusicMap;
+    typedef QMap<Metadata::IdType, Metadata*> MusicMap;
     MusicMap music_map;
     
     typedef QValueList<Metadata>  ValueMetadata;
@@ -401,6 +436,13 @@
     double                   m_lastplayMin;
     double                   m_lastplayMax;
 
+public:
+    static unsigned int REPO_ID_DB;
+    static unsigned int REPO_ID_RADIO;
+    static unsigned int REPO_ID_CD;
+
+    static unsigned int id_to_id (unsigned int id) { return id & METADATA_BITS_REPO_RIGHT_SIDE; }
+    static unsigned int id_to_repo (unsigned int id) { return id >> METADATA_BITS_REPO_LEFT_SHIFT; }
 };
 
 //----------------------------------------------------------------------------
@@ -449,6 +491,7 @@
 
     Metadata                *m_parent;
     QPtrList<AlbumArtImage>  m_imageList;
+
 };
 
 #endif
Index: mythplugins/mythmusic/mythmusic/pls.cpp
===================================================================
--- mythplugins/mythmusic/mythmusic/pls.cpp	(revision 0)
+++ mythplugins/mythmusic/mythmusic/pls.cpp	(revision 0)
@@ -0,0 +1,406 @@
+/*
+  playlistfile (.pls) parser
+  Eskil Heyn Olsen, 2005, distributed under the GPL as part of mythtv.
+*/
+
+#include "pls.h"
+
+#include <qpair.h>
+#include <qvaluelist.h>
+#include <qmap.h>
+
+#include <assert.h>
+
+using namespace std;
+
+class CfgReader 
+{
+  public:
+    CfgReader() 
+    {
+    }
+    ~CfgReader() 
+    {
+    }
+
+    typedef QPair<QString,QString> KeyValue;
+    typedef QValueList<KeyValue> KeyValueList;
+    typedef QMap<QString, KeyValueList> ConfigMap;
+
+    void parse(const char *d, int l) 
+    {
+        const char *ptr = d;
+        int line = 1;
+        bool done = l <= 0;
+
+        QString current_section = "";
+        KeyValueList keyvals;
+
+        while(!done) 
+        {
+            switch(*ptr) 
+            {
+            case '\0':
+                done = true;
+                break;
+            case '#': 
+            {
+                char *end = strchr(ptr, '\n');
+                if (!end) done = true;
+                ptr = end;
+                break;
+            }
+            case '\n':
+                ptr ++;
+                line ++;
+                break;
+            case '[': 
+            {
+                ptr ++;
+                const char *nl = strchr(ptr, '\n');
+                const char *end = strchr(ptr, ']');
+
+                if (!nl) nl = d + l;
+
+                if (!end || nl < end) 
+                {
+                    fprintf(stderr, "Badly formatted section, line %d\n", line);
+                    done = true;
+                }
+
+                if (current_section.length() > 0) 
+                {
+                    cfg[current_section] = keyvals;
+                    keyvals = KeyValueList();
+                }
+
+                current_section = std::string(ptr, end - ptr);
+                if (current_section.length() == 0) 
+                {
+                    fprintf(stderr, "Badly formatted section, line %d\n", line);
+                    done = true;
+                }
+                ptr = end + 1;
+                break;
+            }
+            default:
+                if (current_section.length() > 0) 
+                {
+                    const char *eq = strchr(ptr, '=');
+                    const char *nl = strchr(ptr, '\n');
+
+                    if (!nl) nl = d + l;
+
+                    if (!eq || nl < eq) 
+                    {
+                        fprintf(stderr, "Badly formatted line %d\n", line);
+                        done = true;
+                    } 
+                    else 
+                    {
+                        QString key = string(ptr, eq - ptr);
+                        QString val = string(eq + 1, nl - eq - 1);
+                        keyvals.push_back(KeyValue(key, val));
+                        ptr = nl;
+                    }
+                } 
+                else 
+                {
+                    fprintf(stderr, "Badly formatted line %d\n", line);
+                    done = true;
+                }
+                break;
+            }
+
+            if (ptr - d == l) 
+                done = true;
+        }
+
+        if (current_section.length() > 0) 
+            cfg[current_section] = keyvals;
+    }
+
+    QValueList<QString> getSections(void) 
+    {
+        QValueList<QString> res;
+        for (ConfigMap::iterator it = cfg.begin(); it != cfg.end(); it++) 
+            res.push_back(it.key());
+        return res;
+    }
+
+    QValueList<QString> getKeys(const QString &section) 
+    {
+        KeyValueList keylist = cfg[section];
+        QValueList<QString> res;
+        for (KeyValueList::iterator it = keylist.begin(); it != keylist.end(); it++)
+            res.push_back((*it).first);
+        return res; 
+    }
+
+    QString getStrVal(const QString &section, const QString &key, const QString &def="") 
+    {
+        KeyValueList keylist = cfg[section];
+        QString res = def;
+        for (KeyValueList::iterator it = keylist.begin(); it != keylist.end(); it++) 
+        {
+            if ((*it).first == key) 
+            {
+                res =(*it).second;
+                break;
+            }
+        }
+        return res;
+    }
+
+    int getIntVal(const QString &section, const QString &key, int def=0) 
+    {
+        QString def_str;
+        def_str.setNum (def);
+        return getStrVal(section, key, def_str).toInt();
+    }
+    
+    // very simple unit test, only tests parsing a wellformed file...
+    static void test(void)
+    {
+        const char *sample1 =
+            "# Sample config file\n"
+            "\n"
+            "[foo]\n"
+            "key1=value1\n"
+            "key2=value2\n"
+            "key3=value3\n"
+            "\n"
+            "key4=value4\n"
+            "[bar]\n"
+            "key5=value5\n"
+            "key6=value5\n"
+            "key7=7";
+        
+        CfgReader cfg;
+        cfg.parse(sample1, strlen(sample1));
+        QValueList<QString> sections = cfg.getSections();
+        QValueList<QString> keys;
+        QValueList<QString>::iterator key_it;
+        QValueList<QString>::iterator section_it;
+        section_it = sections.begin(); 
+
+        assert((*section_it) == "bar");
+        keys = cfg.getKeys((*section_it));
+        key_it = keys.begin();
+        assert((*key_it) == "key5");
+        assert(cfg.getStrVal(*section_it, *key_it) == "value5");
+        key_it++;
+        assert((*key_it) == "key6");
+        assert(cfg.getStrVal(*section_it, *key_it) == "value5");
+        key_it++;
+        assert((*key_it) == "key7");
+        assert(cfg.getIntVal(*section_it, *key_it) == 7);
+        key_it++;
+        assert(key_it == keys.end());
+        section_it++;
+
+        assert((*section_it) == "foo");
+        keys = cfg.getKeys((*section_it));
+        key_it = keys.begin();
+        assert((*key_it) == "key1");
+        assert(cfg.getStrVal(*section_it, *key_it) == "value1");
+        key_it++;
+        assert((*key_it) == "key2");
+        assert(cfg.getStrVal(*section_it, *key_it) == "value2");
+        key_it++;
+        assert((*key_it) == "key3");
+        assert(cfg.getStrVal(*section_it, *key_it) == "value3");
+        key_it++;
+        assert((*key_it) == "key4");
+        assert(cfg.getStrVal(*section_it, *key_it) == "value4");
+        key_it++;
+        assert(key_it == keys.end());
+        section_it++;
+
+        assert(section_it == sections.end());
+    }
+
+  private:
+    ConfigMap cfg;
+};
+
+/****************************************************************************/
+
+PlayListFile::PlayListFile() 
+{
+    m_entries.setAutoDelete(true);
+}
+
+PlayListFile::~PlayListFile() 
+{
+}
+
+int PlayListFile::parse(PlayListFile *pls, QTextStream *stream) 
+{
+    int parsed = 0;
+    QString d = stream->read();
+    CfgReader cfg;
+    cfg.parse(d.ascii(), d.length());    
+    
+    int num_entries = cfg.getIntVal("playlist", "numberofentries", -1);
+
+    // Some pls files have "numberofentries", some has "NumberOfEntries".
+    if (num_entries == -1) 
+        num_entries = cfg.getIntVal("playlist", "NumberOfEntries", -1);
+
+    for (int n = 1; n <= num_entries; n++) 
+    {
+        PlayListFileEntry *e = new PlayListFileEntry();
+        QString t_key = QString("Title%1").arg(n);
+        QString f_key = QString("File%1").arg(n);
+        QString l_key = QString("Length%1").arg(n);
+
+        e->setFile(cfg.getStrVal("playlist", f_key));
+        e->setTitle(cfg.getStrVal("playlist", t_key));
+        e->setLength(cfg.getIntVal("playlist", l_key));
+
+        pls->add(e);
+        parsed++;
+    }
+
+    return parsed;
+}
+
+void PlayListFile::test(void) 
+{
+    CfgReader::test();
+
+    {
+        // test reading an empty string
+        PlayListFile parser;
+        QString a1 = "";
+        QTextIStream aa (&a1);
+        assert (PlayListFile::parse (&parser, &aa) == 0);
+    }
+    {
+        // test reading an empty playlist
+        PlayListFile parser;
+        QString b1 = "[playlist]\n";
+        b1 += "numberofentries=0\n";
+        b1 += "version=2\n";
+        QTextIStream bb (&b1);
+        assert (parser.PlayListFile::parse(&parser, &bb) == 0);
+    }
+    {
+        // test reading a playlist w/1 item
+        PlayListFile parser;
+        QString c1 = "[playlist]\n";
+        c1 += "numberofentries=1\n";
+        c1 += "File1=file_1\n";
+        c1 += "Title1=title 1\n";
+        c1 += "Length1=1\n";
+        c1 += "version=2\n";
+        QTextIStream cc (&c1);
+        assert (PlayListFile::parse(&parser, &cc) == 1);
+        assert (parser.get (0)->File () == "file_1");
+        assert (parser.get (0)->Title () == "title 1");
+        assert (parser.get (0)->Length () == 1);
+        assert (parser.get (1) == 0);
+    }
+
+    {
+        // test reading a playlist w/1 item but more after first item
+        PlayListFile parser;
+        QString d1 = "[playlist]\n";
+        d1 += "numberofentries=1\n";
+        d1 += "File1=file_1\n";
+        d1 += "Title1=title 1\n";
+        d1 += "Length1=1\n";
+        d1 += "File2=file_2\n";
+        d1 += "Title2=title 2\n";
+        d1 += "Length2=2\n";
+        d1 += "File3=file_3\n";
+        d1 += "Title3=title 3\n";
+        d1 += "Length3=3\n";
+        d1 += "version=2\n";
+        QTextIStream dd (&d1);
+        assert (PlayListFile::parse(&parser, &dd) == 1);
+        assert (parser.get (0)->File () == "file_1");
+        assert (parser.get (0)->Title () == "title 1");
+        assert (parser.get (0)->Length () == 1);
+        assert (parser.get (1) == 0);
+    }
+
+    {
+        // test reading a playlist w/3 items
+        PlayListFile parser;
+        QString e1 = "[playlist]\n";
+        e1 += "numberofentries=3\n";
+        e1 += "File1=file_1\n";
+        e1 += "Title1=title 1\n";
+        e1 += "Length1=1\n";
+        e1 += "File2=file_2\n";
+        e1 += "Title2=title 2\n";
+        e1 += "Length2=2\n";
+        e1 += "File3=file_3\n";
+        e1 += "Title3=title 3\n";
+        e1 += "Length3=3\n";
+        e1 += "version=2\n";
+        QTextIStream ee (&e1);
+        assert (PlayListFile::parse(&parser, &ee) == 3);
+        assert (parser.get (0)->File () == "file_1");
+        assert (parser.get (0)->Title () == "title 1");
+        assert (parser.get (0)->Length () == 1);
+        assert (parser.get (1)->File () == "file_2");
+        assert (parser.get (1)->Title () == "title 2");
+        assert (parser.get (1)->Length () == 2);
+        assert (parser.get (2)->File () == "file_3");
+        assert (parser.get (2)->Title () == "title 3");
+        assert (parser.get (2)->Length () == 3);
+        assert (parser.get (3) == 0);
+    }
+
+    {
+        // test reading a playlist w/2 items but more after second
+        PlayListFile parser;
+        QString f1 = "[playlist]\n";
+        f1 += "numberofentries=2\n";
+        f1 += "File1=file_1\n";
+        f1 += "Title1=title 1\n";
+        f1 += "Length1=1\n";
+        f1 += "File2=file_2\n";
+        f1 += "Title2=title 2\n";
+        f1 += "Length2=2\n";
+        f1 += "File3=file_3\n";
+        f1 += "Title3=title 3\n";
+        f1 += "Length3=3\n";
+        f1 += "version=2\n";
+        QTextIStream ff (&f1);
+        assert (PlayListFile::parse(&parser, &ff) == 2);
+        assert (parser.get (0)->File () == "file_1");
+        assert (parser.get (0)->Title () == "title 1");
+        assert (parser.get (0)->Length () == 1);
+        assert (parser.get (1)->File () == "file_2");
+        assert (parser.get (1)->Title () == "title 2");
+        assert (parser.get (1)->Length () == 2);
+        assert (parser.get (2) == 0);
+    }
+    {
+        PlayListFile parser;
+        QString f1 = "[playlist]\n";
+        f1 += "NumberOfEntries=2\n";
+        f1 += "File1=http://64.236.34.97:80/stream/1035\n";
+        f1 += "Title1=D I G I T A L L Y - I M P O R T E D - Chillout - ambient psy chillout, check out our tripy flavors!\n";
+        f1 += "Length1=-1\n";
+        f1 += "File2=http://64.236.34.97:5190/stream/1035\n";
+        f1 += "Title2=D I G I T A L L Y - I M P O R T E D - Chillout - ambient psy chillout, check out our tripy flavors!\n";
+        f1 += "Length2=-1\n";
+        f1 += "Version=2\n";
+        QTextIStream ff (&f1);
+        assert (PlayListFile::parse(&parser, &ff) == 2);
+        assert (parser.size () == 2);
+        assert (parser.get (0)->File () == "http://64.236.34.97:80/stream/1035");
+        assert (parser.get (0)->Title () == "D I G I T A L L Y - I M P O R T E D - Chillout - ambient psy chillout, check out our tripy flavors!");
+        assert (parser.get (0)->Length () == -1);
+        assert (parser.get (1)->File () == "http://64.236.34.97:5190/stream/1035");
+        assert (parser.get (1)->Title () == "D I G I T A L L Y - I M P O R T E D - Chillout - ambient psy chillout, check out our tripy flavors!");
+        assert (parser.get (1)->Length () == -1);
+        assert (parser.get (2) == 0);
+    }
+}
+
Index: mythplugins/mythmusic/mythmusic/musicplayer.h
===================================================================
--- mythplugins/mythmusic/mythmusic/musicplayer.h	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/musicplayer.h	(working copy)
@@ -7,8 +7,8 @@
 #include <mythtv/audiooutput.h>
 
 #include "metadata.h"
+#include "decoderhandler.h"
 
-class Decoder;
 class AudioOutput;
 class MainVisual;
 
@@ -21,7 +21,7 @@
      MusicPlayer(QObject *parent, const QString &dev);
     ~MusicPlayer(void);
 
-    void playFile(const QString &filename);
+    //void playFile(const QString &filename);
     void playFile(const Metadata &meta);
 
     void setListener(QObject *listener);
@@ -56,7 +56,8 @@
     void canShowPlayer(bool canShow) { m_canShowPlayer = canShow; }
     bool getCanShowPlayer(void) { return m_canShowPlayer; }
 
-    Decoder     *getDecoder(void) { return m_decoder; }
+    Decoder     *getDecoder(void) { return m_decoderHandler ? m_decoderHandler->getDecoder() : NULL; }
+    DecoderHandler *getDecoderHandler(void) { return m_decoderHandler; }
     AudioOutput *getOutput(void) { return m_output; }
 
     GenericTree *constructPlaylist(void);
@@ -71,6 +72,7 @@
     void         seek(int pos);
 
     Metadata    *getCurrentMetadata(void);
+    Metadata    *getDisplayMetadata(void) { return &m_displayMetadata; }
     void         refreshMetadata(void);
 
     void showMiniPlayer(void);
@@ -115,17 +117,24 @@
     void openOutputDevice(void);
     QString getFilenameFromID(int id);
     void updateLastplay(void);
+    void setupDecoderHandler(void);
 
+    void decoderHandlerReady(void);
+    void decoderHandlerInfo(const QString&, const QString&);
+    void decoderHandlerOperationStart(const QString &);
+    void decoderHandlerOperationStop();
+
     GenericTree *m_playlistTree;
 
     GenericTree *m_currentNode;
     Metadata    *m_currentMetadata;
-    QString      m_currentFile;
+	Metadata     m_displayMetadata;
+    //QString      m_currentFile;
     int          m_currentTime;
 
     QIODevice   *m_input;
     AudioOutput *m_output;
-    Decoder     *m_decoder;
+    DecoderHandler *m_decoderHandler;
 
     QObject     *m_listener;
     MainVisual  *m_visual;
Index: mythplugins/mythmusic/mythmusic/inlines.h
===================================================================
--- mythplugins/mythmusic/mythmusic/inlines.h	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/inlines.h	(working copy)
@@ -11,6 +11,64 @@
 
 // *fast* convenience functions
 
+static inline void smoothen_mono(short *in_data, short *out_data, int len, int new_len)
+{
+    int step = new_len/len;
+    int dii = 0;
+    for (int ii = 0; ii < len; ii++)            
+    {
+        out_data[dii] = in_data[ii];
+        
+        short smooth = 0;
+
+        if (ii < len-1) 
+            smooth = in_data[ii+1] - in_data[ii];
+
+        if (smooth)
+            smooth /= step;
+        
+        for (int cc = 1; cc < step; cc++) {
+            out_data[dii+cc] = out_data[dii+cc-1] + smooth;
+        }
+        
+        dii += step;
+    }
+}
+
+static inline void smoothen_stereo(short *l_in_data, short *l_out_data, 
+                                   short *r_in_data, short *r_out_data,
+                                   int len, int new_len)
+{
+    int step = new_len/len;
+    int dii = 0;
+    for (int ii = 0; ii < len; ii++)            
+    {
+        l_out_data[dii] = l_in_data[ii];
+        r_out_data[dii] = r_in_data[ii];
+        
+        short l_smooth = 0;
+        short r_smooth = 0;
+
+        if (ii < len-1) {
+            l_smooth = l_in_data[ii+1] - l_in_data[ii];
+            r_smooth = r_in_data[ii+1] - r_in_data[ii];
+        }
+
+        if (l_smooth)
+            l_smooth /= step;
+        
+        if (r_smooth)
+            r_smooth /= step;
+        
+        for (int cc = 1; cc < step; cc++) {
+            l_out_data[dii+cc] = l_out_data[dii+cc-1] + l_smooth;
+            r_out_data[dii+cc] = r_out_data[dii+cc-1] + r_smooth;
+        }
+        
+        dii += step;
+    }
+}
+
 static inline void stereo16_from_stereopcm8(register short *l,
 					    register short *r,
 					    register uchar *c,
Index: mythplugins/mythmusic/mythmusic/shoutcast.h
===================================================================
--- mythplugins/mythmusic/mythmusic/shoutcast.h	(revision 0)
+++ mythplugins/mythmusic/mythmusic/shoutcast.h	(revision 0)
@@ -0,0 +1,153 @@
+/*
+  Shoutcast decoder for MythTV.
+  Eskil Heyn Olsen, 2005, distributed under the GPL as part of mythtv.
+*/
+
+#ifndef SHOUTCAST_H_
+#define SHOUTCAST_H_
+
+#include "config.h"
+#include "decoder.h"
+#include "decoderhandler.h"
+
+#include <qobject.h>
+#include <qsocketdevice.h>
+#include <qbuffer.h>
+#include <qurl.h>
+#include <qdns.h>
+#include <qmutex.h>
+
+#include <mythtv/httpcomms.h>
+
+#include <sys/time.h>
+#include <time.h>
+
+
+class ShoutCastRequest;
+class ShoutCastResponse;
+class ShoutCastLogoGrabber;
+class ShoutCastBuffer;
+
+typedef QMap<QString,QString> ShoutCastMetaMap;
+
+class ShoutCastIODevice : public QObject, public QIODevice {
+    Q_OBJECT
+public:
+	enum State {         
+        NOT_CONNECTED,
+        RESOLVING,
+        CONNECTING,
+        CANT_RESOLVE,
+        CANT_CONNECT,
+		CONNECTED,
+        WRITING_HEADER,
+        READING_HEADER,
+		PLAYING,
+        STREAMING,
+        STREAMING_META,
+        STOPPED
+	};
+    static const char* stateString (const State &s);
+
+    ShoutCastIODevice ();
+    ~ShoutCastIODevice ();
+
+	void connectToUrl (const QUrl &url);
+    bool open(int);
+    void close();
+    void flush();
+
+    Q_ULONG size() const;
+    Offset at () const { return 0; }
+    bool at (Offset) { return false; }
+	Q_ULONG bytesAvailable () const;
+
+    Q_LONG readBlock(char *data, Q_ULONG sz);
+    Q_LONG writeBlock(const char *data, Q_ULONG sz);
+    
+    int getch();
+    int putch(int c);
+    int ungetch(int);
+
+    bool getResponse(ShoutCastResponse &response);
+
+signals:
+    void meta(const QString &metadata);
+    void changedState(ShoutCastIODevice::State newstate);
+
+private slots:
+	void socketHostFound ();
+	void socketConnected ();
+	void socketConnectionClosed ();
+	void socketReadyRead ();
+	void socketBytesWritten (int);
+	void socketError (int);
+
+private:
+    void switchToState(const State &s);
+    int parseHeader(const char *data, Q_ULONG len);
+    int parseStream(char *data, Q_ULONG maxlen);
+    bool parseMeta(void);
+	void doKbPerSecond(int bytes_read);
+	bool waitForData (Q_ULONG millisecs, Q_ULONG bytes_needed = 1);
+
+	// Our tools
+    ShoutCastBuffer *m_buffer;
+	ShoutCastResponse *m_response;
+	int m_redirects;
+    QSocket *m_socket;
+
+	// Our scratchpad
+	QByteArray m_scratchpad;
+	Q_ULONG m_scratchpad_pos;
+
+	// Our state info
+	QUrl m_url;
+	Q_ULONG m_bytes_till_next_meta;
+	State m_state;
+	QString m_last_metadata;
+	struct timeval m_sample_tv;
+	uint m_bytes_downloaded;
+	bool m_response_gotten;
+
+	// 
+    QWaitCondition m_cond;
+};
+
+class DecoderIOFactoryShoutCast : public DecoderIOFactory 
+{
+    Q_OBJECT
+  public:
+	DecoderIOFactoryShoutCast(DecoderHandler *parent);
+    ~DecoderIOFactoryShoutCast();
+
+    void start();
+    void stop();
+    QIODevice *takeInput(void);	
+
+  protected slots:
+	void periodicallyCheckResponse(void);
+	void periodicallyCheckBuffered(void);
+    void shoutcastMeta(const QString &metadata);
+    void shoutcastChangedState(ShoutCastIODevice::State newstate);
+	void checkLogoGrabber ();
+
+  private:
+	void socketConnected(void);
+	void socketClosed(void);
+	void socketError(int);
+
+	int checkResponseOK();
+	
+	void makeIODevice();
+	void closeIODevice();
+    QTimer *m_timer;
+
+	ShoutCastIODevice *m_input;
+	uint m_prebuffer;
+	
+	HttpComms m_logo_grabber;
+    QTimer *m_logo_grabber_timer;
+};
+
+#endif /* SHOUTCAST_H_ */
Index: mythplugins/mythmusic/mythmusic/musicplayer.cpp
===================================================================
--- mythplugins/mythmusic/mythmusic/musicplayer.cpp	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/musicplayer.cpp	(working copy)
@@ -19,6 +19,7 @@
 // mythmusic
 #include "musicplayer.h"
 #include "decoder.h"
+#include "decoderhandler.h"
 #include "cddecoder.h"
 #include "constants.h"
 #include "mainvisual.h"
@@ -27,6 +28,7 @@
 
 // how long to wait before updating the lastplay and playcount fields
 #define LASTPLAY_DELAY 15
+#define GET_REPO_ID(attrs) attrs->at(4)
 
 MusicPlayer  *gPlayer = NULL;
 
@@ -36,8 +38,7 @@
     :QObject(parent)
 {
     m_CDdevice = dev;
-    m_decoder = NULL;
-    m_input = NULL;
+    m_decoderHandler = NULL;
     m_output = NULL;
 
     m_playlistTree = NULL;
@@ -108,6 +109,16 @@
         m_currentMetadata = NULL;
     }
 
+    if (m_decoderHandler) 
+    {
+        m_decoderHandler->removeListener(this);
+        if (m_listener) {
+            m_decoderHandler->removeListener (m_listener);
+        }
+        m_decoderHandler->deleteLater();
+        m_decoderHandler = NULL;
+    }
+
     if (m_shuffleMode == SHUFFLE_INTELLIGENT)
         gContext->SaveSetting("PlayMode", "intelligent");
     else if (m_shuffleMode == SHUFFLE_RANDOM)
@@ -134,17 +145,23 @@
     if (m_listener && m_output)
         m_output->removeListener(m_listener);
 
-    if (m_listener && m_decoder)
-        m_decoder->removeListener(m_listener);
+    if (m_listener && getDecoder())
+        getDecoder()->removeListener(m_listener);
 
+    if (m_listener && m_decoderHandler)
+        m_decoderHandler->removeListener(m_listener);
+
     m_listener = listener;
 
     if (m_listener && m_output)
         m_output->addListener(m_listener);
 
-    if (m_listener && m_decoder)
-        m_decoder->addListener(m_listener);
+    if (m_listener && getDecoder())
+        getDecoder()->addListener(m_listener);
 
+    if (m_listener && getDecoderHandler())
+        getDecoderHandler()->addListener(m_listener);
+
     (listener == NULL) ? m_isAutoplay = true :  m_isAutoplay = false;
 }
 
@@ -167,16 +184,21 @@
 
 void MusicPlayer::playFile(const Metadata &meta)
 {
-    playFile(meta.Filename());
+    delete m_currentMetadata;
     m_currentMetadata = new Metadata(meta);
+    play();
     m_currentNode = NULL;
 }
 
+/*
 void MusicPlayer::playFile(const QString &filename)
 {
+    delete m_currentMetadata;
+    m_currentMetadata = new Metadata(meta);
     m_currentFile = filename;
     play();
 }
+*/
 
 void MusicPlayer::stop(bool stopAll)
 {
@@ -191,20 +213,14 @@
         m_output->Reset();
     }
 
-    if (m_input)
-        delete m_input;
-    m_input = NULL;
-
     m_isPlaying = false;
 
-    if (stopAll && m_decoder)
+    if (stopAll && getDecoder())
     {
-        m_decoder->removeListener(this);
+        getDecoder()->removeListener(this);
         if (m_listener)
-            m_decoder->removeListener(m_listener);
+            getDecoder()->removeListener(m_listener);
 
-        delete m_decoder;
-        m_decoder = NULL;
         m_listener = NULL;
     }
 
@@ -233,109 +249,36 @@
         m_output->Pause(!m_isPlaying);
     }
     // wake up threads
-    if (m_decoder) 
+    if (getDecoder()) 
     {
-        m_decoder->lock();
-        m_decoder->cond()->wakeAll();
-        m_decoder->unlock();
+        getDecoder()->lock();
+        getDecoder()->cond()->wakeAll();
+        getDecoder()->unlock();
     }
 }
 
-void MusicPlayer::play(void)
+void MusicPlayer::play()
 {
     stopDecoder();
 
+    Metadata *meta = getCurrentMetadata();
+    if (!meta)
+        return;
+
     if (!m_output)
         openOutputDevice();
 
-    if (m_input)
-        delete m_input;
+    if (!getDecoderHandler()) 
+        setupDecoderHandler();
 
-    m_input = new QFile(m_currentFile);
-
-    if (m_decoder && !m_decoder->factory()->supports(m_currentFile))
-    {
-        m_decoder->removeListener(this);
-
-        if (m_listener)
-            m_decoder->removeListener(m_listener);
-
-        delete m_decoder;
-        m_decoder = NULL;
-    }
-
-    if (!m_decoder)
-    {
-        m_decoder = Decoder::create(m_currentFile, m_input, m_output, true);
-        if (!m_decoder)
-        {
-            VERBOSE(VB_IMPORTANT, "MusicPlayer: Failed to create decoder for playback");
-            return;
-        }
-
-        if (m_currentFile.contains("cda") == 1)
-            dynamic_cast<CdDecoder*>(m_decoder)->setDevice(m_CDdevice);
-
-        m_decoder->setBlockSize(2 * 1024);
-
-        m_decoder->addListener(this);
-
-        if (m_listener)
-            m_decoder->addListener(m_listener);
-    }
-    else
-    {
-        m_decoder->setInput(m_input);
-        m_decoder->setFilename(m_currentFile);
-        m_decoder->setOutput(m_output);
-    }
-
-    if (m_decoder->initialize())
-    {
-        if (m_output)
-            m_output->Reset();
-
-        m_decoder->start();
-
-        m_isPlaying = true;
-
-        if (m_currentNode)
-        {
-            if (m_currentNode->getInt() > 0)
-            {
-                m_currentMetadata = Metadata::getMetadataFromID(m_currentNode->getInt());
-                m_updatedLastplay = false;
-            }
-            else
-            {
-                // CD track
-                CdDecoder *cddecoder = dynamic_cast<CdDecoder*>(m_decoder);
-                if (m_decoder)
-                    m_currentMetadata = cddecoder->getMetadata(-m_currentNode->getInt());
-            }
-        }
-    }
+    getDecoderHandler()->start(meta);
 }
 
 void MusicPlayer::stopDecoder(void)
 {
-    if (m_decoder && m_decoder->running())
-    {
-        m_decoder->lock();
-        m_decoder->stop();
-        m_decoder->unlock();
-    }
+    if (getDecoderHandler())
+        getDecoderHandler()->stop();
 
-    if (m_decoder) 
-    {
-        m_decoder->lock();
-        m_decoder->cond()->wakeAll();
-        m_decoder->unlock();
-    }
-
-    if (m_decoder)
-        m_decoder->wait();
-
     if (m_currentMetadata)
     {
         if (m_currentMetadata->hasChanged())
@@ -457,6 +400,36 @@
 
 void MusicPlayer::customEvent(QCustomEvent *event)
 {
+    switch ((int)event->type())
+    {
+        case DecoderHandlerEvent::Ready:
+        {
+            decoderHandlerReady();
+            break;
+        }
+        case DecoderEvent::Decoding:
+        {
+            m_displayMetadata = *getCurrentMetadata();
+            break;
+        }
+
+        case DecoderHandlerEvent::Info:
+        {
+            DecoderHandlerEvent *dxe = (DecoderHandlerEvent*)event;
+            m_displayMetadata = *getCurrentMetadata();
+            m_displayMetadata.setArtist("");
+            m_displayMetadata.setTitle(*dxe->getMessage());            
+            break;
+        }
+
+        case DecoderHandlerEvent::Meta:
+        {
+            DecoderHandlerEvent *dxe = (DecoderHandlerEvent*)event;
+            m_displayMetadata = *dxe->getMetadata();
+            break;
+        }
+    }
+
     if (m_isAutoplay)
     {
         switch ((int)event->type())
@@ -498,7 +471,7 @@
                                         .arg(*dxe->errorMessage()));
                 break;
             }
-
+            
             case MythEvent::MythEventMessage:
             {
                 MythEvent *me = (MythEvent *) event;
@@ -585,7 +558,7 @@
     else
     {
         // cd track
-        CdDecoder *cddecoder = dynamic_cast<CdDecoder*>(m_decoder);
+        CdDecoder *cddecoder = dynamic_cast<CdDecoder*>(getDecoder());
         if (cddecoder)
         {
             Metadata *meta = cddecoder->getMetadata(-id);
@@ -678,9 +651,7 @@
         m_currentNode = m_currentNode->getChildAt(0, -1);
         if (m_currentNode)
         {
-            m_currentFile = getFilenameFromID(m_currentNode->getInt());
-            if (m_currentFile != "")
-                play();
+            play();
         }
     }
 }
@@ -692,11 +663,11 @@
         m_output->Reset();
         m_output->SetTimecode(pos*1000);
 
-        if (m_decoder && m_decoder->running())
+        if (getDecoder() && getDecoder()->running())
         {
-            m_decoder->lock();
-            m_decoder->seek(pos);
-            m_decoder->unlock();
+            getDecoder()->lock();
+            getDecoder()->seek(pos);
+            getDecoder()->unlock();
         }
     }
 }
@@ -829,3 +800,61 @@
     m_playSpeed -= 0.05;
     setSpeed(m_playSpeed);
 }
+
+void MusicPlayer::setupDecoderHandler() 
+{
+    m_decoderHandler = new DecoderHandler();
+    getDecoderHandler()->addListener(this);
+    if (m_listener)
+        getDecoderHandler()->addListener(m_listener);
+}
+
+void MusicPlayer::decoderHandlerReady(void)
+{    
+    VERBOSE(VB_PLAYBACK, QString ("decoder handler is ready, decoding %1").
+            arg(getDecoder()->getFilename()));
+
+    if (getDecoder()->getFilename().contains("cda") == 1)
+        dynamic_cast<CdDecoder*>(getDecoder())->setDevice(m_CDdevice);
+    
+    getDecoder()->setOutput(m_output);
+    getDecoder()->setBlockSize(2 * 1024);
+    getDecoder()->addListener(this);
+
+    if (m_listener)
+        getDecoder()->addListener(m_listener);
+
+    m_currentTime = 0;
+
+    if (m_visual)
+    {
+        m_visual->setDecoder(getDecoder());
+        m_visual->setOutput(m_output);
+    }
+    
+    if (getDecoder()->initialize()) 
+    {
+        if (m_output)
+             m_output->Reset();
+         
+        getDecoder()->start();
+        
+        if (m_resumeMode == RESUME_EXACT &&
+            gContext->GetNumSetting("MusicBookmarkPosition", 0) > 0)
+        {
+            seek(gContext->GetNumSetting("MusicBookmarkPosition", 0));
+            gContext->SaveSetting("MusicBookmarkPosition", 0);
+        }
+
+        m_isPlaying = true;
+
+        // hmm, it'd be more fair to only inc playcount and set lastplay 
+        // when it's played at least x seconds of the track.
+        m_currentMetadata->setLastPlay();
+        m_currentMetadata->incPlayCount();    
+    } else {
+        VERBOSE(VB_PLAYBACK, QString ("Cannot initialise decoder for %1").
+                arg (getDecoder()->getFilename ()));
+    }
+}
+
Index: mythplugins/mythmusic/mythmusic/pls.h
===================================================================
--- mythplugins/mythmusic/mythmusic/pls.h	(revision 0)
+++ mythplugins/mythmusic/mythmusic/pls.h	(revision 0)
@@ -0,0 +1,89 @@
+/*
+  playlistfile (.pls) parser
+  Eskil Heyn Olsen, 2005, distributed under the GPL as part of mythtv.
+*/
+
+#ifndef PLS_H_
+#define PLS_H_
+
+#include <qstring.h>
+#include <qbuffer.h>
+#include <qptrlist.h>
+#include <qtextstream.h>
+
+/** \brief Class for representing entries in a pls file 
+ */
+class PlayListFileEntry 
+{
+  public:
+    PlayListFileEntry() {}
+    ~PlayListFileEntry() {}
+
+    QString File(void) { return file; }
+    QString Title(void) { return title; }
+    int Length(void) { return length; }
+
+    void setFile(const QString &f) { file = f; }
+    void setTitle(const QString &t) { title = t; }
+    void setLength(int l) { length = l; }
+
+  private:  
+    QString file;
+    QString title;
+    int length;
+};
+
+/** \brief Class for containing the info of a pls file
+ */
+class PlayListFile 
+{
+public:
+    PlayListFile ();
+    ~PlayListFile ();
+
+    /** \brief Get the number of entries in the pls file 
+
+        This returns the number of actual parsed entries, not the
+        <tt>numberofentries</tt> field.
+
+        \returns the number of entries
+    */
+    int size (void) const { return m_entries.count(); }
+
+    /** \brief Get a pls file entry
+        \param i which entry to get, between 0 and the value returned by calling \p PlayListParser::size
+        \returns a pointer to a \p PlayListEntry
+    */
+    PlayListFileEntry* get (int i) { return m_entries.at(i); }
+
+    /** \brief Version of the parsed pls file 
+
+        Returns the version number specified in the <tt>version</tt>
+        field of the pls file.
+
+        \returns the version number
+     */
+    int version (void) const { return m_version; }
+
+    /** Add a entry to the playlist 
+        \param e a \p PlayListFileEntry object
+     */
+    void add (PlayListFileEntry *e) { m_entries.append(e); }
+
+    /** Clear out all the entries */
+    void clear (void) { m_entries.clear(); }
+
+    /** Perform internal unittest */
+    static void test (void);
+
+    /** \brief Parse a pls file.
+        \param stream the playlist file in a \p QTextStream
+        \returns the number of entries parsed 
+    */
+    static int parse (PlayListFile *pls, QTextStream *stream);
+  private:
+    QPtrList<PlayListFileEntry> m_entries;
+    int m_version;
+};
+
+#endif /* PLS_H_ */
Index: mythplugins/mythmusic/mythmusic/playbackbox.h
===================================================================
--- mythplugins/mythmusic/mythmusic/playbackbox.h	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/playbackbox.h	(working copy)
@@ -15,12 +15,12 @@
 #include "mainvisual.h"
 #include "metadata.h"
 #include "playlist.h"
-#include "editmetadata.h"
 #include "databasebox.h"
 #include "musicplayer.h"
 
 class Output;
 class Decoder;
+class DecoderHandler;
 
 class PlaybackBoxMusic : public MythThemedDialog
 {
@@ -85,6 +85,8 @@
 
     void occasionallyCheckCD();
 
+    void operationProgressTimer();
+
     // popup menu
     void showMenu();
     void closePlaylistPopup();
@@ -97,6 +99,8 @@
     void fromCD();
     void showSmartPlaylistDialog();
     void showSearchDialog();
+	void showAddRadioStationDialog();
+	void showRemoveRadioStationDialog();
     bool getInsertPLOptions(InsertPLOption &insertOption,
                             PlayPLOption &playOption, bool &bRemoveDups);
 
@@ -112,7 +116,7 @@
     void doUpdatePlaylist(QString whereClause);
     void CycleVisualizer(void);
     void updatePlaylistFromCD(void);
-    void setTrackOnLCD(Metadata *mdata);
+    void setTrackOnLCD(const Metadata *mdata);
     void updateTrackInfo(Metadata *mdata);
     void postUpdate();
     void playFirstTrack();
@@ -121,10 +125,20 @@
     void bannerToggle(Metadata *mdata);
     void savePosition(uint position);
     void restorePosition(const QString &position);
+    void addRadioMenuEntries();
+
+    void decoderHandlerReady(void);
+    void decoderHandlerInfo(const QString&, const QString&);
+    void decoderHandlerOperationStart(const QString &);
+    void decoderHandlerOperationStop();
+    
+    QString operation_name;
+    int operation_progress_count;
+    QTimer  *decoder_handler_progress_timer;
+
     void pushButton(UIPushButtonType *button);
     QString getTimeString(int exTime, int maxTime);
 
-    QString playfile;
     QString statusString;
     QString curSmartPlaylistCategory;
     QString curSmartPlaylistName;
@@ -136,6 +150,7 @@
     bool scrollingDown;
 
     Metadata *curMeta;
+    Metadata displayMeta;
 
     unsigned int resumemode;
 
Index: mythplugins/mythmusic/mythmusic/mainvisual.cpp
===================================================================
--- mythplugins/mythmusic/mythmusic/mainvisual.cpp	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/mainvisual.cpp	(working copy)
@@ -34,7 +34,6 @@
 // fast inlines
 #include "inlines.h"
 
-
 using namespace std;
 
 VisFactory* VisFactory::g_pVisFactories = 0;
@@ -203,6 +202,33 @@
     else
         len = 0;
 
+    if (len < 512)
+    {
+        short *new_l = 0;
+        short *new_r = 0;
+
+        if (c == 1)
+        {
+            new_l = new short[512];
+            smoothen_mono(l, new_l, len, 512);
+            delete[] l;
+            l = new_l;
+        }
+
+        if (c == 2)
+        {
+            new_l = new short[512];
+            new_r = new short[512];
+            smoothen_stereo(l, new_l, r, new_r, len, 512);
+            delete[] r;
+            delete[] l;
+            r = new_r;
+            l = new_l;
+        }
+
+        len = 512;
+    }
+
     nodes.append(new VisualNode(l, r, len, w));
 }
 
@@ -341,7 +367,7 @@
 }
 
 InfoWidget::InfoWidget(QWidget *parent)
-    : QWidget( parent)
+    : QWidget( parent), color (0x03, 0x27, 0x44)
 {
     hide();
 }
@@ -415,11 +441,11 @@
         y += displayRect.y();
         // only show the text box if the visualiser is actually fullscreen
         if (visMode == 2)
-            p.fillRect(displayRect, QColor ("darkblue"));
+            p.fillRect(displayRect, color);
     }
     else
     {
-        p.fillRect(0, 0, info_pixmap.width(), info_pixmap.height(), QColor ("darkblue"));
+        p.fillRect(0, 0, info_pixmap.width(), info_pixmap.height(), color);
 
         if (! albumArt.isNull())
         {
@@ -479,7 +505,7 @@
     int x = indent;
     int y = indent;
 
-    p.fillRect(0, 0, info_pixmap.width(), info_pixmap.height(), QColor ("darkblue"));
+    p.fillRect(0, 0, info_pixmap.width(), info_pixmap.height(), color);
 
     QString info_copy = info;
     for (int offset = 0; offset < textHeight; offset += fm.height()) 
@@ -532,10 +558,10 @@
     long s, indexTo;
     double *magnitudesp = magnitudes.data();
     double valL, valR, tmpL, tmpR;
-    double index, step = 512.0 / size.width();
 
     if (node) {
-	index = 0;
+    double step = (node->length < 512 ? node->length : 512.0) / size.width();
+	double index = 0;
 	for ( i = 0; i < size.width(); i++) {
 	    indexTo = (int)(index + step);
             if (indexTo == (int)(index))
@@ -731,11 +757,10 @@
     double *magnitudesp = magnitudes.data();
     double val, tmp;
 
-    double index, step = 512.0 / size.width();
-
     if (node) 
     {
-        index = 0;
+        double step = (node->length < 512 ? node->length : 512.0) / size.width();
+        double index = 0;
         for ( i = 0; i < size.width(); i++) 
         {
             indexTo = (int)(index + step);
@@ -967,6 +992,8 @@
     alpha = x;
     for (i = 1; i < (int) domain; i++) {
         scaled = (int) floor(0.5 + (alpha * log((double(i) + alpha) / alpha)));
+        if (scaled < 1)
+            scaled = 1;
         if (indices[scaled - 1] < i)
             indices[scaled - 1] = i;
     }
Index: mythplugins/mythmusic/mythmusic/decoderhandler.h
===================================================================
--- mythplugins/mythmusic/mythmusic/decoderhandler.h	(revision 0)
+++ mythplugins/mythmusic/mythmusic/decoderhandler.h	(revision 0)
@@ -0,0 +1,208 @@
+#ifndef DECODERHANDLER_H_
+#define DECODERHANDLER_H_
+
+#include <qobject.h>
+#include <qiodevice.h>
+#include <qfile.h>
+#include <qhttp.h>
+#include <qurl.h>
+#include "metadata.h"
+
+#include <mythtv/mythobservable.h>
+
+#include "pls.h"
+
+class QUrl;
+class QUrlOperator;
+
+class Decoder;
+class Metadata;
+class DecoderIOFactory;
+class DecoderHandler;
+
+/** \brief Events sent by the \p DecoderHandler and it's helper classes.
+ */
+class DecoderHandlerEvent : public MythEvent
+{
+  public:
+    enum Type { Ready = (QEvent::User + 300), Meta, Info, OperationStart, OperationStop, Error};
+
+    DecoderHandlerEvent(Type t)
+        : MythEvent(t), m_msg(0), m_meta(0)
+    { ; }
+
+    DecoderHandlerEvent(Type t, QString *e)
+        : MythEvent(t), m_msg(e), m_meta(0)
+    { ; }
+
+    DecoderHandlerEvent(const Metadata &m);
+    ~DecoderHandlerEvent();
+
+    QString *getMessage() const { return m_msg; }
+    Metadata *getMetadata() const { return m_meta; }
+
+    virtual DecoderHandlerEvent *clone();
+
+private:
+    QString *m_msg;
+    Metadata *m_meta;
+};
+
+/** \brief Class for starting stream decoding.
+
+    This class handles starting the \p Decoder for the \p
+    PlaybackBox via DecoderIOFactorys. 
+
+    It operates on a playlist, either created with a single file, by
+    loading a .pls or downloading it, and for each entry creates an
+    appropriate DecoderIOFactory. The creator is simply a intermediate
+    class that translates the next URL in the playlist to
+    QIODevice. Ie. the DecoderIOFactoryFile just returns a QFile,
+    whereas the DecoderIOFactoryShoutcast returns a QSocket subclass,
+    where reads do the necessary translation of the shoutcast stream.
+ */
+class DecoderHandler : public QObject, public MythObservable
+{
+    Q_OBJECT
+    friend class DecoderIOFactory;
+  public:
+    typedef enum { 
+        ACTIVE,
+        LOADING,
+        STOPPED
+    } State;
+
+    DecoderHandler();
+    virtual ~DecoderHandler();
+
+    Decoder *getDecoder(void) { return m_decoder; }
+    void start(Metadata *mdata);
+
+    void stop(void);
+    void customEvent(QCustomEvent*);
+    bool done();
+    bool next(void);
+    void error(const QString &msg);
+
+  protected:
+    void doOperationStart(const QString &name);
+    void doOperationStop(void);
+    void doConnectDecoder(const QUrl &url, const QString &format);
+    void doFailed(const QUrl &url, const QString &message);
+    void doInfo(const QString &message);
+
+  private:  
+	int               m_state;
+    int               m_playlist_pos;
+    PlayListFile      m_playlist;
+    DecoderIOFactory *m_io_factory;
+    Decoder          *m_decoder;
+    Metadata         *m_meta;
+	bool              m_op;
+	uint              m_redirects;
+
+    static const uint MaxRedirects = 3;
+
+    bool createPlaylist(const QUrl &url);
+    bool createPlaylistForSingleFile(const QUrl &url);
+    bool createPlaylistFromFile(const QUrl &url);
+    bool createPlaylistFromRemoteUrl(const QUrl &url);
+
+	bool haveIOFactory(void) { return m_io_factory != 0; }
+    DecoderIOFactory *getIOFactory(void) { return m_io_factory; }
+	void createIOFactory(const QUrl &url);
+	void deleteIOFactory(void);
+};
+
+/** \brief The glue between the DecoderHandler and the Decoder
+	
+    The DecoderIOFactory is responsible for opening the QIODevice that
+    is given to the Decoder....
+ */
+class DecoderIOFactory : public QObject, public MythObservable
+{
+  public:
+    DecoderIOFactory(DecoderHandler *parent);
+    virtual ~DecoderIOFactory();
+
+    virtual void start() = 0;
+    virtual void stop() = 0;
+    virtual QIODevice *takeInput(void) = 0;
+
+    void setUrl (const QUrl &url) { m_url = url; }
+    void setMeta (Metadata *meta) { m_meta = *meta; }
+
+    static const uint DefaultPrebufferSize = 128 * 1024;
+    static const uint MaxRedirects = 3;
+
+  protected:
+    void doConnectDecoder(const QString &format);
+    Decoder *getDecoder(void);
+    void doFailed(const QString &message);
+    void doInfo(const QString &message);
+    void doOperationStart(const QString &name);
+    void doOperationStop(void);
+	Metadata& getMetadata() { return m_meta; }
+	QUrl& getUrl() { return m_url; }
+
+  private:
+    DecoderHandler *m_handler;
+    Metadata m_meta;
+    QUrl m_url;
+};
+
+class DecoderIOFactoryFile : public DecoderIOFactory 
+{
+    Q_OBJECT
+  public:
+    DecoderIOFactoryFile(DecoderHandler *parent);
+    ~DecoderIOFactoryFile();
+    void start();
+    void stop() {}
+    QIODevice *takeInput(void);
+
+  private:
+    QIODevice *m_input;
+};
+
+class DecoderIOFactoryUrl : public DecoderIOFactory 
+{
+    Q_OBJECT
+  public:
+    DecoderIOFactoryUrl(DecoderHandler *parent);
+    ~DecoderIOFactoryUrl();
+
+    void start();
+    void stop();
+    QIODevice *takeInput(void);
+
+  protected slots:
+    void finished(QNetworkOperation *op);
+    void start(QNetworkOperation *op);
+    void data(const QByteArray & data, QNetworkOperation * op);
+
+  private:
+    void doStart(void);
+    void doClose(void);
+
+    bool m_started;
+    QUrlOperator *m_url_op;
+    QFile *m_output;
+    QFile *m_input;
+};
+
+class DecoderIOFactoryMqp : public DecoderIOFactory 
+{
+    Q_OBJECT
+  public:
+    DecoderIOFactoryMqp(DecoderHandler *parent);
+    ~DecoderIOFactoryMqp();
+    void start();
+    void stop();
+    QIODevice *takeInput(void);
+
+  private:
+    QIODevice *m_input;
+};
+
+#endif /* DECODERHANDLER_H_ */
Index: mythplugins/mythmusic/mythmusic/editradiometadata.h
===================================================================
--- mythplugins/mythmusic/mythmusic/editradiometadata.h	(revision 0)
+++ mythplugins/mythmusic/mythmusic/editradiometadata.h	(revision 0)
@@ -0,0 +1,74 @@
+#ifndef EDITRADIOMETADATA_H_
+#define EDITRADIOMETADATA_H_
+
+#include <iostream>
+using namespace std;
+
+#include <mythtv/mythdialogs.h>
+
+#include "editmetadata.h"
+
+class UIPhoneEntry;
+
+class EditRadioMetadataDialog : public MythThemedDialog
+{
+
+  Q_OBJECT
+
+  public:
+
+    EditRadioMetadataDialog(Metadata *source_metadata,
+							MythMainWindow *parent,
+							QString window_name,
+							QString theme_filename,
+							const char* name = 0);
+    ~EditRadioMetadataDialog();
+
+    void keyPressEvent(QKeyEvent *e);
+    void wireUpTheme(void);
+    void fillWidgets(void);
+
+  public slots:
+
+    void closeDialog(void);
+    void searchStation(void);
+    void searchGenre(void);
+    void incRating(bool up_or_down);
+    void showSaveMenu(void);
+	bool verifyEntries();
+    void saveNewToDatabase();
+    void saveToDatabase();
+    void cancelPopup();
+    void editLostFocus();
+
+  private:
+
+    void fillSearchList(QString field, QString table);
+    bool showList(QString caption, QString &value);
+    
+    Metadata *m_metadata;
+	Metadata *m_sourceMetadata ;
+    MythPopupBox *popup;
+
+    //
+    //  GUI stuff
+    //
+
+    UIRemoteEditType    *station_edit;
+    UIRemoteEditType    *channel_edit;
+    UIRemoteEditType    *url_edit;
+    UIRemoteEditType    *metaformat_edit;
+    UIRemoteEditType    *genre_edit;
+
+    UIRepeatedImageType *rating_image;
+
+    UIPushButtonType    *searchstation_button;
+    UIPushButtonType    *searchgenre_button;
+    UIPushButtonType    *rating_button;
+    
+    UITextButtonType    *done_button;
+    
+    QStringList         searchList;
+};
+
+#endif
Index: mythplugins/mythmusic/mythmusic/shoutcast.cpp
===================================================================
--- mythplugins/mythmusic/mythmusic/shoutcast.cpp	(revision 0)
+++ mythplugins/mythmusic/mythmusic/shoutcast.cpp	(revision 0)
@@ -0,0 +1,1160 @@
+/*
+  Shoutcast decoder for MythTV.
+  Eskil Heyn Olsen, 2005, distributed under the GPL as part of mythtv.
+ */
+
+#include <qapplication.h>
+#include <qregexp.h>
+#include <qsocket.h>
+#include <mythtv/mythcontext.h>
+#include "shoutcast.h"
+#include "metadata.h"
+#include <assert.h>
+#include <algorithm>
+#include "mythtv/qmdcodec.h"
+
+/****************************************************************************/
+
+#define WAIT_FOR_MS 1500
+#define MAX_ALLOWED_META_SIZE 1024 * 100
+#define MAX_REDIRECTS 3
+#define PREBUFFER_SECS 5
+
+/****************************************************************************/
+
+struct timeval& operator-= (struct timeval &lhs, const struct timeval &rhs) {
+    lhs.tv_sec -= rhs.tv_sec;
+    if (lhs.tv_usec >= rhs.tv_usec) {
+        lhs.tv_usec -= rhs.tv_usec;
+    } else {
+        lhs.tv_sec --;
+        lhs.tv_usec = 1000000 - (rhs.tv_usec - lhs.tv_usec);
+    }
+    return lhs;
+}
+
+bool operator!=(const struct timeval &lhs, const struct timeval &rhs) {
+    if (lhs.tv_sec == rhs.tv_sec)
+        return lhs.tv_usec != rhs.tv_usec;
+    return true;    
+}
+
+bool operator==(const struct timeval &lhs, const struct timeval &rhs) {
+    if (lhs.tv_sec == rhs.tv_sec)
+        return lhs.tv_usec == rhs.tv_usec;
+    return false;
+}
+
+bool operator<(const struct timeval &lhs, const struct timeval &rhs) {
+    if (lhs.tv_sec == rhs.tv_sec)
+        return lhs.tv_usec < rhs.tv_usec;
+    return lhs.tv_sec < rhs.tv_sec;
+}
+
+bool operator<=(const struct timeval &lhs, const struct timeval &rhs) {
+    return lhs == rhs || lhs < rhs;
+}
+
+bool operator>(const struct timeval &lhs, const struct timeval &rhs) {
+    if (lhs.tv_sec == rhs.tv_sec)
+        return lhs.tv_usec > rhs.tv_usec;
+    return lhs.tv_sec > rhs.tv_sec;
+}
+
+bool operator>=(const struct timeval &lhs, const struct timeval &rhs) {
+    return lhs == rhs || lhs > rhs;
+}
+
+void test_timeval_operators () {
+    struct timeval a, b;
+    a.tv_sec = 1174197435;
+    a.tv_usec = 279159;
+    b.tv_sec = 1174197435;
+    b.tv_usec = 208796;
+    
+    assert (a == a);
+    assert (b == b);
+    
+    assert (a != b);
+    
+    assert (a > b);
+    assert (a >= b);
+    assert (a >= a);
+    
+    assert (b < a);
+    assert (b <= a);
+    assert (b <= b);
+    
+    a -= b;
+    assert (a.tv_sec == 0);
+    assert (a.tv_usec == 70363);
+    
+    a.tv_sec = 1174197735;
+    a.tv_usec = 40493;
+    b.tv_sec = 1174197735;
+    b.tv_usec = 29043;
+    a -= b;
+    assert (a.tv_sec == 0);
+    assert (a.tv_usec == 11450);
+}
+
+/****************************************************************************/
+
+const char* ShoutCastIODevice::stateString (const State &s) {
+#define TO_STRING(a) case a: return #a
+    switch (s) {
+        TO_STRING (NOT_CONNECTED);
+        TO_STRING (RESOLVING);
+        TO_STRING (CONNECTING);
+        TO_STRING (CANT_RESOLVE);
+        TO_STRING (CANT_CONNECT);
+		TO_STRING (CONNECTED);
+        TO_STRING (WRITING_HEADER);
+        TO_STRING (READING_HEADER);
+		TO_STRING (PLAYING);
+        TO_STRING (STREAMING);
+        TO_STRING (STREAMING_META);
+        TO_STRING (STOPPED);
+    default:
+        return "unknown state";        
+    }
+#undef TO_STRING
+}
+
+int moveFromByteArray(QByteArray &array, char *data, Q_ULONG maxlen) {
+    Q_ULONG consumed = array.size ();
+
+    if (consumed > maxlen)
+        consumed = maxlen;    
+    if (data)
+        memcpy (data, array.data(), consumed);   
+    if (consumed != array.size ())
+        memmove (array.data(), array.data() + consumed, array.size() - consumed);
+    array.resize(array.size() - consumed);
+
+    return consumed;
+}
+
+/****************************************************************************/
+
+/** \brief Class to download shoutcast station logos
+
+    Runs a HttpComms request in a thread until success/failure or
+    stop_transfer is called.
+ */
+class ShoutCastLogoGrabber : public QThread {
+public:
+    ShoutCastLogoGrabber(const Metadata *m) : m_meta(*m), m_stop(false) { }
+
+    void stop_transfer() { 
+        m_stop = true; 
+    }
+
+    void run() {
+        HttpComms *comms = new HttpComms;
+
+        QString urlstr;
+        m_meta.getField("x-cast-logourl", &urlstr);
+
+        QUrl url(urlstr);
+        comms->request(url, 10000, false);
+
+        while (!m_stop && !comms->isDone())
+            usleep(500);
+
+        if (m_stop)
+            return;
+
+        if (comms->isTimedout() || comms->getStatusCode() != 200) {
+            comms->deleteLater();
+            comms = NULL;
+            return;
+        }
+
+        m_meta.setAlbumArt(comms->getRawData());
+        comms->deleteLater();
+        comms = NULL;
+    }
+
+private:
+    Metadata m_meta;
+    bool m_stop;
+};
+
+/****************************************************************************/
+
+/** \brief A buffer class
+    The intent with this it can be replaced with may RingBuffer ?
+ */
+class ShoutCastBuffer {
+public:
+    ShoutCastBuffer () : m_pos (0), m_size (0) { }
+    ~ShoutCastBuffer () { }
+
+    Q_ULONG Read (char *data, Q_ULONG max) {
+        QMutexLocker holder (&m_socket_mutex);
+        const QByteArray &next_buffer = m_buffers.front();
+        const char *next_data = next_buffer.data () + m_pos;
+        uint next_size = next_buffer.size () - m_pos;
+
+        if (max > m_size)
+            max = m_size;
+
+        if (max > next_size)
+            max = next_size;
+        
+        memcpy (data, next_data, max);
+
+        m_pos += max;
+        m_size -= max;
+        
+        if (max == next_size) {
+            m_pos = 0;
+            m_buffers.pop_front();
+        }
+
+        return max;
+    }
+
+    /** \brief Add data to the buffer
+        \param data the bytes to add, will be owned by the ShoutCastBuffer
+        \param sz the size of data
+    */
+    void Write (char *data, uint sz) {
+        if (sz == 0)
+            return;
+
+        QMutexLocker holder (&m_socket_mutex);
+        QByteArray array (sz);
+        array.assign (data, sz);
+        m_buffers.push_back (array);
+        m_size += sz;
+    }
+
+    /** \brief Add data to the buffer
+        \param arr the byte array to add
+    */
+    void Write (QByteArray &array) {
+        if (array.size () == 0)
+            return;
+        QMutexLocker holder (&m_socket_mutex);
+        m_buffers.push_back (array);
+        m_size += array.size ();
+    }
+
+    Q_ULONG ReadBufAvail () const { 
+        return m_size; 
+    }
+
+    static void selfTest () {
+        VERBOSE(VB_IMPORTANT, "Selftesting ShoutCastBuffer");
+        ShoutCastBuffer iobuf;
+        char *data_1 = strdup ("aaaaa");
+        char *data_2 = strdup ("bbbbb");
+        QByteArray arr (5);
+        arr.duplicate ("ccccc", 5);
+
+        iobuf.Write (data_1, strlen (data_1));
+        assert (iobuf.ReadBufAvail() == 5);
+        iobuf.Write (data_2, strlen (data_2));
+        assert (iobuf.ReadBufAvail() == 10);
+        iobuf.Write (arr);
+        assert (iobuf.ReadBufAvail() == 15);
+
+        char *data = (char*)alloca(iobuf.ReadBufAvail());
+        assert (iobuf.Read (data, 3) == 3);
+        assert (iobuf.ReadBufAvail() == 12);
+        assert (memcmp (data ,"aaa", 3) == 0);
+
+        assert (iobuf.Read (data, 3) == 2);
+        assert (iobuf.ReadBufAvail() == 10);
+        assert (memcmp (data ,"aa", 2) == 0);
+
+        assert (iobuf.Read (data, 10) == 5);
+        assert (iobuf.ReadBufAvail() == 5);
+        assert (memcmp (data ,"bbbbb", 5) == 0);
+
+        assert (iobuf.Read (data, 5) == 5);
+        assert (iobuf.ReadBufAvail() == 0);
+        assert (memcmp (data ,"ccccc", 5) == 0);
+    }
+
+private:
+    QValueList<QByteArray> m_buffers;
+    uint m_pos;
+    Q_ULONG m_size;
+    QMutex m_socket_mutex;
+};
+
+
+/****************************************************************************/
+
+class ShoutCastRequest 
+{
+public:
+    ShoutCastRequest() { }
+    ShoutCastRequest(const QUrl &url) { setUrl(url); }
+    ~ShoutCastRequest() { }
+	const char *data(void) { return m_data.data(); }
+	uint size(void) { return m_data.size(); }
+	
+private:
+    void setUrl(const QUrl &url) {
+        QString hdr;
+        hdr = QString("GET %1 HTTP/1.1\r\n"
+                      "Host: %2\r\n"
+                      "User-Agent: mythmusic/svn\r\n"
+                      "Keep-Alive:\r\n"
+                      "Connection: TE, Keep-Alive\r\n").arg(url.path()).arg(url.host());
+        if (url.hasUser() && url.hasPassword()) {
+            QString authstring = url.user() + ":" + url.password();
+            QString auth = QCodecs::base64Encode(QCString(authstring));
+            
+            hdr += "Authorization: Basic " + auth;
+        }
+	hdr += QString("TE: trailers\r\n"
+                      "icy-metadata:1\r\n"
+                      "\r\n");
+        
+        m_data.duplicate(hdr.ascii(), hdr.length());
+    }
+
+    QByteArray m_data;
+};
+
+
+/****************************************************************************/
+
+
+class ShoutCastResponse 
+{
+  public:
+    ShoutCastResponse() { }
+    ~ShoutCastResponse() { }
+    
+    int getMetaint(void) { return getInt("icy-metaint"); }
+    int getBitrate(void) { return getInt("icy-br"); }
+    QString getGenre(void) { return getString("icy-genre"); }
+    QString getName(void) { return getString("icy-name"); }
+    int getStatus(void) { return getInt("status"); }
+    bool isICY(void) { return QString(m_data["protocol"]).left(3) == "ICY"; }
+    QString getContent(void) { return getString("content-type"); }
+    QString getLocation(void) { return getString("location"); }
+
+    QString getString(const QString &key) { return m_data[key]; }
+    int getInt(const QString &key) { return m_data[key].toInt(); }
+
+    int fillResponse(const char *data, int len);
+  private:
+    QMap<QString,QString> m_data;
+};
+
+/** \brief Consume bytes and parse shoutcast header
+    \returns number of bytes consumed
+*/
+int ShoutCastResponse::fillResponse(const char *s, int l) 
+{
+    QCString d(s, l);
+    int result = 0;
+    // check each line
+    for (;;) 
+    {
+        int pos = d.find("\r");
+
+        if (pos <= 0) 
+            break;
+
+        // Extract the line
+        QCString snip(d.data(), pos + 1);
+        d.remove(0, pos + 2);
+        result += pos + 2;
+        
+        if (snip.left(4) == "ICY ") 
+        {
+            int space = snip.find(' ');
+            m_data["protocol"] = "ICY";
+            QString tmp = snip.mid(space).simplifyWhiteSpace();
+            int second_space = tmp.find(' ');
+            if (second_space > 0) {
+                m_data["status"] = tmp.left(second_space);
+            } else {
+                m_data["status"] = tmp;
+            }
+        } 
+        else if (snip.left(7) == "HTTP/1.") 
+        {
+            int space = snip.find(' ');
+            m_data["protocol"] = snip.left(space);
+            QString tmp = snip.mid(space).simplifyWhiteSpace();
+            int second_space = tmp.find(' ');
+            if (second_space > 0) {
+                m_data["status"] = tmp.left(second_space);
+            } else {
+                m_data["status"] = tmp;
+            }
+        } 
+        else if (snip.left(9).lower() == "location:") 
+        {
+            m_data["location"] = snip.mid(9).stripWhiteSpace();
+        } 
+        else if (snip.left(13).lower() == "content-type:") 
+        {
+            m_data["content-type"] = snip.mid(13).stripWhiteSpace();
+        } 
+        else if (snip.left(4) == "icy-") 
+        {
+            int pos = snip.find(':');
+            QString key = snip.left(pos);
+            m_data[key.ascii()] = snip.mid(pos+1).stripWhiteSpace();
+        }
+    }
+
+    return result;
+}
+
+/****************************************************************************/
+
+class ShoutCastMetaParser
+{
+  public:
+	ShoutCastMetaParser() { }
+	~ShoutCastMetaParser() { }
+
+	void setMetaFormat(const QString &metaformat);
+	ShoutCastMetaMap parseMeta(QString meta);
+
+  private:
+	QString m_meta_format;
+	int m_meta_artist_pos;
+	int m_meta_title_pos;
+	int m_meta_album_pos;
+};
+
+void ShoutCastMetaParser::setMetaFormat(const QString &metaformat) 
+{
+/*
+  We support these metatags :
+  %a - artist
+  %t - track
+  %b - album
+  %r - random bytes
+ */
+    m_meta_format = metaformat;
+
+    m_meta_artist_pos = 0;
+    m_meta_title_pos = 0;
+    m_meta_album_pos = 0;
+
+    int assign_index = 1;
+    int pos = 0;
+
+    pos = m_meta_format.find("%", pos);
+    while (pos >= 0) {
+        pos++;
+        QChar ch = m_meta_format.at(pos);
+
+        if (ch == '%') {
+            pos++;
+        }
+        else if (ch == 'r' || ch == 'a' || ch == 'b' || ch == 't') 
+        {
+            if (ch == 'a')
+                m_meta_artist_pos = assign_index;
+            
+            if (ch == 'b')
+                m_meta_album_pos = assign_index;
+            
+            if (ch == 't')
+                m_meta_title_pos = assign_index;
+
+            assign_index++;
+        } else
+            fprintf(stderr, "CastDecoder: malformed metaformat '%s'\n", m_meta_format.ascii());
+
+        pos = m_meta_format.find("%", pos);
+    }
+    
+    m_meta_format.replace("%a", "(.*)");
+    m_meta_format.replace("%t", "(.*)");
+    m_meta_format.replace("%b", "(.*)");
+    m_meta_format.replace("%r", "(.*)");
+    m_meta_format.replace("%%", "%");
+}
+
+ShoutCastMetaMap ShoutCastMetaParser::parseMeta(QString meta)
+{
+    QCString metastring(meta);
+    ShoutCastMetaMap result;
+    int title_begin_pos = metastring.find("StreamTitle='");
+    int title_end_pos;
+
+    if (title_begin_pos >= 0) 
+    {
+        title_begin_pos += 13;
+        title_end_pos = metastring.find("';", title_begin_pos);
+        QCString title = metastring.mid(title_begin_pos, 
+                                        title_end_pos - title_begin_pos);
+        QRegExp rx;
+        rx.setPattern(m_meta_format);
+        if (rx.search(title) != -1)
+        {
+            VERBOSE(VB_PLAYBACK, QString("ShoutCast: Meta     : '%1'").
+                    arg(meta));
+            VERBOSE(VB_PLAYBACK, QString("ShoutCast: Parsed as: '%1' by '%2'").
+                    arg(rx.cap(m_meta_title_pos)).
+                    arg(rx.cap(m_meta_artist_pos)));
+            
+            if (m_meta_title_pos > 0) 
+                result["title"] = rx.cap(m_meta_title_pos);
+            
+            if (m_meta_artist_pos > 0) 
+                result["artist"] = rx.cap(m_meta_artist_pos);
+            
+            if (m_meta_album_pos > 0) 
+                result["album"] = rx.cap(m_meta_album_pos);
+        }
+    }
+
+    return result;
+}
+
+/****************************************************************************/
+
+ShoutCastIODevice::ShoutCastIODevice ()
+    :  m_redirects (0), 
+       m_scratchpad_pos (0),
+       m_state (NOT_CONNECTED)
+{ 
+    // Run some quick unittests. Put this here since there's no
+    // 'formal' unittest harness blabla...
+    ShoutCastBuffer::selfTest();
+    test_timeval_operators ();
+    // done testing...
+
+    m_buffer = new ShoutCastBuffer;
+    m_socket = new QSocket;
+    m_response = new ShoutCastResponse;
+
+    connect (m_socket, SIGNAL (hostFound ()), SLOT (socketHostFound ()));
+    connect (m_socket, SIGNAL (connected ()), SLOT (socketConnected ()));
+    connect (m_socket, SIGNAL (connectionClosed ()), SLOT (socketConnectionClosed ()));
+    connect (m_socket, SIGNAL (readyRead ()), SLOT (socketReadyRead ()));
+    connect (m_socket, SIGNAL (error (int)), SLOT (socketError (int)));
+
+    switchToState (NOT_CONNECTED);
+
+    setFlags (IO_Direct | IO_Async | IO_ReadOnly);
+
+    m_sample_tv.tv_sec = 0;
+    m_sample_tv.tv_usec = 0;
+}
+
+ShoutCastIODevice::~ShoutCastIODevice () { 
+    delete m_buffer;
+    delete m_response;
+    m_socket->close ();
+    m_socket->disconnect (this);
+    m_socket->deleteLater ();
+    m_socket = NULL;
+}
+
+void ShoutCastIODevice::connectToUrl (const QUrl &url)  { 
+    m_url = url;
+    switchToState (RESOLVING);
+    setMode(IO_ReadOnly);
+    setState(IO_Open);
+    return m_socket->connectToHost (m_url.host (), m_url.port ());
+}
+
+bool ShoutCastIODevice::open(int) { 
+    VERBOSE(VB_NETWORK, "ShoutCastIODevice: open");
+    return true;
+}
+
+void ShoutCastIODevice::close() { 
+    return m_socket->close();
+}
+
+void ShoutCastIODevice::flush() { 
+    return m_socket->flush();
+}
+
+Q_ULONG ShoutCastIODevice::size() const { 
+    return m_buffer->ReadBufAvail(); 
+}
+
+bool ShoutCastIODevice::waitForData (Q_ULONG millisecs, Q_ULONG bytes_needed) {
+    struct timeval tv_begin;
+    gettimeofday (&tv_begin, 0);
+    
+    do {
+        if (m_state != STOPPED && m_buffer->ReadBufAvail() < bytes_needed) {
+            m_cond.wait (millisecs);            
+        }
+
+        if (m_buffer->ReadBufAvail() >= bytes_needed)
+            return true;
+
+        struct timeval tv_delta;
+        gettimeofday (&tv_delta, 0); 
+        tv_delta -= tv_begin;        
+        Q_ULONG delta = (tv_delta.tv_sec * 1000) + (tv_delta.tv_usec / 1000);
+        if (delta > millisecs) 
+            millisecs = 0;
+        else
+            millisecs -= delta;
+
+        VERBOSE(VB_PLAYBACK, QString ("slept for %1 ms, adjust to %2, avail is %3/%4").
+                arg((tv_delta.tv_sec * 1000) + (tv_delta.tv_usec / 1000)).
+                arg(millisecs).arg(m_buffer->ReadBufAvail()).arg(bytes_needed));
+    } while (millisecs > 0);
+
+    if (m_buffer->ReadBufAvail() < bytes_needed)
+        return false;
+
+    return true;
+}
+
+Q_LONG ShoutCastIODevice::readBlock(char *data, Q_ULONG maxlen) { 
+    int result = 0;
+
+    if (waitForData (WAIT_FOR_MS, 1024) == false) {
+        VERBOSE(VB_PLAYBACK, "Waited for data in stream, got none");
+        switchToState(STOPPED);
+    }
+
+    if (m_state == STREAMING_META && parseMeta()) 
+        switchToState(STREAMING);
+    
+    if (m_state == STREAMING) 
+    {
+        if (waitForData (WAIT_FOR_MS) == false) {
+            VERBOSE(VB_PLAYBACK, "Waited for in stream again, got none");
+            switchToState(STOPPED);
+        } else {
+            result = parseStream(data, maxlen);
+            if (m_bytes_till_next_meta == 0) 
+                switchToState(STREAMING_META);
+        }
+    } 
+    
+    if (m_state != STOPPED) 
+        VERBOSE(VB_NETWORK, QString("ShoutCastIO2: %1 kb in buffer, btnm=%2/%3 state=%4, rb=%5/%6").
+                arg(m_buffer->ReadBufAvail () / 1024, 4).
+                arg(m_bytes_till_next_meta, 4).
+                arg(m_response->getMetaint()).
+                arg(stateString (m_state)).
+                arg(result).
+                arg(maxlen));
+    else 
+        VERBOSE(VB_NETWORK, QString("ShoutCastIO2: stopped"));
+    
+    return result;
+}
+
+Q_LONG ShoutCastIODevice::writeBlock(const char *data, Q_ULONG sz) { 
+    return m_socket->writeBlock (data, sz);
+}
+
+Q_ULONG ShoutCastIODevice::bytesAvailable () const {
+    return m_buffer->ReadBufAvail ();
+}
+
+int ShoutCastIODevice::getch() { 
+    assert (0);
+    return -1;
+}
+int ShoutCastIODevice::putch(int) { 
+    assert (0);
+    return -1;
+}
+int ShoutCastIODevice::ungetch(int) { 
+    assert (0);
+    return -1;
+}
+
+void ShoutCastIODevice::socketHostFound () {
+    VERBOSE(VB_NETWORK, "ShoutCastIO2: Host Found");
+    switchToState (CONNECTING);
+}
+
+void ShoutCastIODevice::socketConnected () {
+    VERBOSE(VB_NETWORK, "ShoutCastIO2: Connected");
+    switchToState (CONNECTED);
+
+    ShoutCastRequest request (m_url);
+    Q_ULONG written = m_socket->writeBlock (request.data (), request.size ());
+    VERBOSE(VB_NETWORK, QString ("ShoutCastIO2: Sending Request, %1 of %1 bytes").arg(written).arg(request.size()));
+
+    if (written != request.size ()) {
+        m_scratchpad.duplicate (request.data () + written, request.size () - written);
+        m_scratchpad_pos = 0;
+        connect (m_socket, SIGNAL (bytesWritten (int)), SLOT (socketBytesWritten (int)));
+        switchToState (WRITING_HEADER);
+    } else {
+        switchToState (READING_HEADER);
+    }
+}
+
+void ShoutCastIODevice::socketConnectionClosed () {
+    VERBOSE(VB_NETWORK, "ShoutCastIO2: Connection Closed");
+    switchToState (STOPPED);
+}
+
+void ShoutCastIODevice::socketReadyRead () {
+    Q_ULONG sz = m_socket->bytesAvailable();
+
+    //VERBOSE(VB_IMPORTANT, QString ("ShoutCastIO2: %1 bytes readable").arg(sz));    
+
+    // data is given to m_buffer, not freed here, and m_buffer uses
+    // QByteArray, so it has to be malloc'ed.
+    char *data = (char*)malloc (sz);
+    Q_ULONG actual_sz = m_socket->readBlock (data, sz);
+    if (actual_sz < sz)
+        data = (char*)realloc (data, actual_sz);
+
+    if (m_state == READING_HEADER) {
+        if (parseHeader (data, actual_sz) == 1) {
+            if (m_response->isICY () && m_response->getStatus () == 200) {
+                switchToState (PLAYING);
+                
+                m_response_gotten = true;
+                
+                // debug, collect kb/s info
+                gettimeofday (&m_sample_tv, NULL);
+                m_bytes_downloaded = 0;
+                m_bytes_till_next_meta = m_response->getMetaint();
+                
+                // whatever's left in the scratch pad, toss into m_buffer
+                free (data);
+                m_buffer->Write (m_scratchpad);
+                m_cond.wakeOne ();
+                
+                switchToState (STREAMING);
+            } else if (m_response->getStatus () == 302 ||
+                       m_response->getStatus () == 301) 
+            {
+                if (++m_redirects > MAX_REDIRECTS) {
+                    VERBOSE (VB_NETWORK, QString ("Too many redirects"));
+                    switchToState (STOPPED);
+                } else {
+                    VERBOSE (VB_NETWORK, QString ("Redirect to %1").arg(m_response->getLocation()));
+                    connectToUrl(m_url);
+                }
+            } else {
+                VERBOSE (VB_NETWORK, QString ("Unknown response status %1").arg (m_response->getStatus ()));
+                switchToState (STOPPED);
+            }
+
+        }
+    } else {
+        // ...Write takes ownership of data...
+        m_buffer->Write (data, actual_sz);
+        m_cond.wakeOne ();
+        doKbPerSecond(actual_sz);
+    }
+}
+
+void ShoutCastIODevice::socketBytesWritten (int) {
+    Q_ULONG written = m_socket->writeBlock (m_scratchpad.data () + m_scratchpad_pos,
+                                            m_scratchpad.size () - m_scratchpad_pos);
+    VERBOSE(VB_NETWORK, QString ("ShoutCastIO: %1 bytes written").arg(written));
+
+    m_scratchpad_pos += written;
+    if (m_scratchpad_pos == m_scratchpad.size ()) {
+        m_scratchpad.truncate (0);
+        disconnect (m_socket, SIGNAL (bytesWritten (int)), this, 0);
+        switchToState (READING_HEADER);
+    }
+}
+
+void ShoutCastIODevice::socketError (int error) {
+    VERBOSE(VB_NETWORK, QString ("ShoutCastIO: Socket Error %1").arg(error));
+
+    switch (error) {
+    case QSocket::ErrConnectionRefused:
+        VERBOSE(VB_NETWORK, "ShoutCastIO2: Error Connection Refused");
+        switchToState(CANT_CONNECT);
+        break;
+    case QSocket::ErrHostNotFound:
+        VERBOSE(VB_NETWORK, "ShoutCastIO2: Error Host Not Found"); 
+        switchToState(CANT_RESOLVE);
+        break;
+    case QSocket::ErrSocketRead:
+        VERBOSE(VB_NETWORK, "ShoutCastIO2: Error Socket Read");
+        switchToState (STOPPED);
+        break;
+    }
+}
+
+void ShoutCastIODevice::switchToState(const State &state) 
+{
+    switch (state) 
+    {
+    case PLAYING:
+        VERBOSE(VB_PLAYBACK, QString ("Playing %1 (%2) at %3 kbps").
+                arg(m_response->getName()).
+                arg(m_response->getGenre()).
+                arg(m_response->getBitrate()));
+        break;
+    case STREAMING:
+        if (m_state == STREAMING_META)
+            m_bytes_till_next_meta = m_response->getMetaint();
+        break;
+    case STOPPED:
+        m_socket->close ();
+        m_cond.wakeAll ();
+        break;
+    default:
+        break;
+    }
+
+    m_state = state;
+    emit changedState(m_state);
+}
+
+/** Add the given data/len to the scratchpad. Data contains the
+    ShoutCast response header, and once we've called this enough times
+    to have read a complete header, return 1, otherwise, return
+    0. 
+*/
+int ShoutCastIODevice::parseHeader(const char *data, Q_ULONG len) {
+    // Pad the read data to the end the m_scratchpad...
+    int old_buf_size = m_scratchpad.size();
+    m_scratchpad.resize(old_buf_size + len);
+    memcpy(m_scratchpad.data() + old_buf_size, data, len);
+    
+    int consumed = m_response->fillResponse(m_scratchpad.data(), m_scratchpad.size());        
+    VERBOSE(VB_NETWORK, QString ("ShoutCastIO2: Receiving header, %1 bytes").arg(consumed));
+    {
+        QString tmp;
+        tmp.setAscii (m_scratchpad.data (), consumed);        
+        VERBOSE(VB_NETWORK, QString ("ShoutCastIO2: Receiving header %1").arg(tmp));
+    }
+    moveFromByteArray (m_scratchpad, 0, consumed);
+    
+    if (m_scratchpad.size() >= 2 && 
+        m_scratchpad[0] == '\r' && 
+        m_scratchpad[1] == '\n')
+    {
+        moveFromByteArray (m_scratchpad, 0, 2);        
+        return 1;
+    }
+    
+    return 0;
+}
+
+bool ShoutCastIODevice::getResponse(ShoutCastResponse &response) {
+    if (! m_response_gotten)
+        return false;
+
+    response = *m_response;
+    return true;
+}
+
+int ShoutCastIODevice::parseStream(char *data, Q_ULONG maxlen) {
+    if (maxlen > m_bytes_till_next_meta)
+        maxlen = m_bytes_till_next_meta;
+
+    /*
+    // throttle... by leaving bytes, we can delay actually having to
+    // make readBlock block the caller waiting for more bytes, thereby
+    // making the decoder be more responsive to ie. stop requests.
+    if (maxlen > m_buffer->ReadBufAvail ()) {
+        VERBOSE(VB_NETWORK, QString ("throttling, %1 becomes %2").arg(maxlen).arg(m_buffer->ReadBufAvail()/2));
+        maxlen = m_buffer->ReadBufAvail () / 3;
+        if (maxlen < 128) {
+            maxlen = 128;
+            waitForData (2000, maxlen);
+        }
+    }
+    */
+
+    int result = m_buffer->Read (data, maxlen);
+    m_bytes_till_next_meta -= result;
+
+    doKbPerSecond(result);
+    return result;
+}
+
+bool ShoutCastIODevice::parseMeta() {
+    unsigned char ch;
+    m_buffer->Read (reinterpret_cast<char*>(&ch), 1);
+
+    Q_ULONG meta_size = 16 * ch;
+    if (meta_size == 0)
+        return true;
+
+    // FIXME: in case the stream is f*cked, we don't want to allocate too much 
+    if (meta_size > MAX_ALLOWED_META_SIZE) {
+        VERBOSE(VB_PLAYBACK, QString ("Error in stream, got a meta size of %1").arg(meta_size));
+        switchToState (STOPPED);
+        return false;
+    }
+
+    VERBOSE(VB_NETWORK, QString("ShoutCast: Reading %1 bytes of meta").arg(meta_size));
+
+    if (waitForData (WAIT_FOR_MS, meta_size) == false) {
+        VERBOSE(VB_PLAYBACK, "Stream seems to have stopped");
+        switchToState(STOPPED);
+        return false;
+    }
+
+    // Read in a loop until we have all of meta_size (but no more)
+    QByteArray metadata (meta_size);
+    Q_ULONG bytes_read = 0;
+    do {
+        Q_ULONG len = m_buffer->Read (metadata.data () + bytes_read, meta_size - bytes_read);
+        if (len <= 0) {
+            VERBOSE(VB_PLAYBACK, QString ("Error in metadata, expected %1 bytes of meta, got %1").
+                    arg(meta_size).arg(len));
+            switchToState(STOPPED);
+            return false;
+        }
+        bytes_read += len;
+    } while (bytes_read < meta_size);
+    m_bytes_downloaded += bytes_read;
+
+    // Avoid sending signals if the data hasn't changed
+    QString metadata_string (metadata);
+    if (m_last_metadata == metadata_string) 
+        return true;
+
+    m_last_metadata = metadata_string;
+    emit meta(metadata_string);
+
+    return true;
+}
+
+void ShoutCastIODevice::doKbPerSecond(int bytes_read)
+{
+    // debug, collect kb/s info
+    m_bytes_downloaded += bytes_read;
+    struct timeval tv = {0, 0};
+    gettimeofday(&tv, NULL);
+    int msecs =((tv.tv_sec * 1000000 + tv.tv_usec) - 
+                (m_sample_tv.tv_sec * 1000000 + m_sample_tv.tv_usec)) / 1000;
+
+    if (msecs > 5000) 
+    {
+        VERBOSE(VB_NETWORK, QString("ShoutCast: download speed, %1 kb in %2 s =  %3 kb/s").
+                arg(m_bytes_downloaded/1024.0, 1, 'f', 1).
+                arg(msecs/1000.0, 1, 'f', 1).
+                arg((m_bytes_downloaded/1024)/(msecs/1000.0), 3, 'f', 1));
+        m_sample_tv = tv;
+        m_bytes_downloaded = 0;
+    }
+}
+
+/****************************************************************************/
+
+DecoderIOFactoryShoutCast::DecoderIOFactoryShoutCast(DecoderHandler *parent) 
+    : DecoderIOFactory(parent), m_timer(NULL), m_input(NULL)
+{
+    m_timer = new QTimer(this);
+}
+
+DecoderIOFactoryShoutCast::~DecoderIOFactoryShoutCast() 
+{
+    closeIODevice();
+    if (!m_logo_grabber.isDone ()) {
+        m_logo_grabber.stop ();
+    }
+}
+
+QIODevice* DecoderIOFactoryShoutCast::takeInput () 
+{
+    QIODevice *result = m_input;
+    m_input = 0;
+    return result;
+}
+
+void DecoderIOFactoryShoutCast::makeIODevice() 
+{
+    closeIODevice();
+
+    m_input = new ShoutCastIODevice();
+
+    connect(m_input, SIGNAL(meta(const QString&)),
+            this,    SLOT(shoutcastMeta(const QString&)));
+    connect(m_input, SIGNAL(changedState(ShoutCastIODevice::State)),
+            this,    SLOT(shoutcastChangedState(ShoutCastIODevice::State)));
+}
+
+void DecoderIOFactoryShoutCast::closeIODevice() 
+{
+    if (m_input) {
+        m_input->disconnect();
+        if (m_input->isOpen()) {
+            m_input->close();
+        }
+        m_input->deleteLater();
+        m_input = NULL;
+        VERBOSE(VB_PLAYBACK, "DecoderIOFactoryShoutCast m_input is now 0");
+    }
+}
+
+
+void DecoderIOFactoryShoutCast::start()
+{
+    VERBOSE(VB_PLAYBACK, QString("DecoderIOFactoryShoutCast %1").arg(getUrl().toString()));
+    doOperationStart("Connecting");
+
+    makeIODevice();
+    m_input->connectToUrl(getUrl());
+
+    QString urlstr;
+    getMetadata().getField ("x-cast-logourl", &urlstr);
+    QUrl url (urlstr);
+    m_logo_grabber.request (url, 10000, false);
+    m_logo_grabber_timer = new QTimer (this);
+    connect (m_logo_grabber_timer, SIGNAL(timeout()), this, SLOT(checkLogoGrabber()));
+    m_logo_grabber_timer->start (1000);
+}
+
+void DecoderIOFactoryShoutCast::checkLogoGrabber () {
+    VERBOSE(VB_PLAYBACK, QString("Checking logograbber %1 %2 %3").
+            arg(m_logo_grabber.isDone ()?"done":"busy").
+            arg(m_logo_grabber.isTimedout ()?"timedout":"not-timedout").
+            arg(m_logo_grabber.getStatusCode()));
+    if (!m_logo_grabber.isDone ())
+        return;
+
+    m_logo_grabber_timer->stop ();
+    m_logo_grabber_timer->disconnect ();
+
+    if (m_logo_grabber.isTimedout() || m_logo_grabber.getStatusCode() != 200)
+        return;
+
+    getMetadata().setAlbumArt(m_logo_grabber.getRawData());
+}
+
+void DecoderIOFactoryShoutCast::stop()
+{
+    if (m_timer) 
+        m_timer->disconnect ();
+
+    doOperationStop();
+
+    Metadata mdata(getMetadata());    
+    mdata.setTitle("Stopped");
+    mdata.setArtist("");
+    mdata.setLength(-1);
+    DecoderHandlerEvent ev(mdata);
+    dispatch(ev);
+}
+
+void DecoderIOFactoryShoutCast::periodicallyCheckResponse(void)
+{
+    int res = checkResponseOK();        
+    if (res == 0) 
+    {
+        ShoutCastResponse response;
+        m_input->getResponse (response);
+        m_prebuffer = PREBUFFER_SECS * response.getBitrate () * 125; // 125 = 1000/8 (kilo, bits...)
+        VERBOSE(VB_NETWORK, QString ("kbps is %1, prebuffering %2 secs = %3 kb").
+                arg(response.getBitrate()).arg(PREBUFFER_SECS).arg(m_prebuffer/1024));
+        m_timer->stop();
+        m_timer->disconnect();
+        connect(m_timer, SIGNAL(timeout()), this, SLOT(periodicallyCheckBuffered()));
+        m_timer->start(500);
+    }
+    else if (res < 0) 
+    {
+        m_timer->stop();
+        doFailed("Cannot parse this stream");
+    }
+}
+
+void DecoderIOFactoryShoutCast::periodicallyCheckBuffered(void)
+{
+    VERBOSE(VB_NETWORK, QString("DecoderIOFactoryShoutCast: prebuffered %1/%2KB").
+            arg(m_input->bytesAvailable()/1024).arg(m_prebuffer/1024));
+    
+    if (m_input->bytesAvailable() < m_prebuffer || m_input->bytesAvailable() == 0)
+        return;
+    
+    ShoutCastResponse response;
+    m_input->getResponse (response);
+    VERBOSE(VB_PLAYBACK, QString ("contents '%1'").arg (response.getContent()));
+    if (response.getContent () == "audio/mpeg") {
+        doConnectDecoder("create-mp3-decoder.mp3");
+    } else if (response.getContent () == "audio/aacp") {
+        doConnectDecoder("create-aac-decoder.m4a");
+    } else {
+        doFailed (QObject::tr ("Unsupported content type for ShoutCast stream: %1").
+                  arg (response.getContent ()));
+    }
+
+    m_timer->disconnect();
+    m_timer->stop();
+}
+
+void DecoderIOFactoryShoutCast::shoutcastMeta(const QString &metadata)
+{
+    ShoutCastMetaParser parser;
+    parser.setMetaFormat(getMetadata().CompilationArtist());
+
+    ShoutCastMetaMap meta_map = parser.parseMeta(metadata);
+
+    Metadata mdata(getMetadata());
+    mdata.setTitle(meta_map["title"]);
+    mdata.setArtist(meta_map["artist"]);
+    mdata.setAlbum(getMetadata().Album()); // meta_map["album"]
+    mdata.setLength(-1);
+
+    DecoderHandlerEvent ev(mdata);
+    dispatch(ev);
+}
+
+void DecoderIOFactoryShoutCast::shoutcastChangedState(ShoutCastIODevice::State state)
+{    
+    //VERBOSE(VB_PLAYBACK, QString ("ShoutCast changed state to %1").arg(ShoutCastIODevice::stateString (state)));
+    if (state == ShoutCastIODevice::RESOLVING)
+        doOperationStart("Finding radio");
+    if (state == ShoutCastIODevice::CANT_RESOLVE)
+        doFailed (QObject::tr ("Cannot find radio.\nCheck the URL is correct."));
+
+    if (state == ShoutCastIODevice::CONNECTING)
+        doOperationStart("Connecting to radio");
+    if (state == ShoutCastIODevice::CANT_CONNECT)
+        doFailed (QObject::tr ("Cannot connect to radio.\nCheck the URL is correct."));
+    if (state == ShoutCastIODevice::CONNECTED) {
+        doOperationStart("Connected to radio");
+        m_timer->stop();
+        m_timer->disconnect();
+        connect(m_timer, SIGNAL(timeout()),
+                this, SLOT(periodicallyCheckResponse()));
+        m_timer->start(300);
+    }
+    if (state == ShoutCastIODevice::PLAYING) {
+        doOperationStart("Buffering");
+    }
+
+    if (state == ShoutCastIODevice::STOPPED)
+        stop();
+}
+
+int DecoderIOFactoryShoutCast::checkResponseOK()
+{
+    ShoutCastResponse response;
+
+    if (!m_input->getResponse(response))
+        return 0;
+
+    if (! response.isICY() && 
+        response.getStatus() == 302 && 
+        ! response.getLocation().isEmpty())
+    {
+        // restart with new location...
+        setUrl(response.getLocation());
+        start();
+        return 1;
+    }
+
+    if (! response.isICY() || response.getStatus() != 200) 
+        return -1;
+
+    return 0;
+}
+    
Index: mythplugins/mythmusic/mythmusic/metaio.cpp
===================================================================
--- mythplugins/mythmusic/mythmusic/metaio.cpp	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/metaio.cpp	(working copy)
@@ -2,6 +2,8 @@
 
 #include "metaio.h"
 #include "metadata.h"
+#include <qdir.h>
+#include <qurl.h>
 #include <mythtv/mythcontext.h>
 
 //==========================================================================
@@ -105,3 +107,37 @@
 
     return retdata;
 }
+
+//==========================================================================
+/*!
+ * \brief Retrieve the albumart.
+ *
+ * The default implementation picks a random image file from the
+ * directory in which the file is.
+ *
+ * \param filename The filename to try and determin metadata for.
+ * \returns A QByteArray that can be passed to QImage or such.
+ */
+QByteArray MetaIO::getAlbumArt(const QString &filename)
+{
+    QString curdir = QUrl(filename).dirPath();
+    QString namefilter = gContext->GetSetting("AlbumArtFilter",
+                                              "*.png;*.jpg;*.jpeg;*.gif;*.bmp");
+    // Get directory contents based on filter
+    QDir folder(curdir, namefilter, QDir::Name | QDir::IgnoreCase, 
+                QDir::Files | QDir::Hidden);
+    
+    if (!folder.count())
+        return QByteArray();
+    
+    QString result = folder[rand() % folder.count()];
+    result.prepend("/");
+    result.prepend(curdir);
+    
+    QFile file(result);
+    file.open(IO_ReadOnly);
+    if (!file.isOpen())
+        return QByteArray();
+
+    return file.readAll();
+}
Index: mythplugins/mythmusic/mythmusic/goom/mythgoom.cpp
===================================================================
--- mythplugins/mythmusic/mythmusic/goom/mythgoom.cpp	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/goom/mythgoom.cpp	(working copy)
@@ -80,19 +80,28 @@
         return true;
 
     int numSamps = 512;
+    int step = 1;
+
     if (node->length < 512)
-        numSamps = node->length;
+        step = 512 / node->length;
 
     signed short int data[2][512];
    
     int i = 0;
-    for (i = 0; i < numSamps; i++)
+    for (i = 0; i < numSamps; i += step)
     {
         data[0][i] = node->left[i];
         if (node->right)
             data[1][i] = node->right[i];
         else
             data[1][i] = data[0][i];
+
+        if (step > 1) 
+            for (int j = 1; j < step; j++)
+            {
+                data[0][i+j] = data[0][i];
+                data[1][i+j] = data[1][i];
+            }
     }
 
     for (; i < 512; i++)
Index: mythplugins/mythmusic/mythmusic/visualize.h
===================================================================
--- mythplugins/mythmusic/mythmusic/visualize.h	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/visualize.h	(working copy)
@@ -123,15 +123,20 @@
     virtual ~Squares();
 
     void resize (const QSize &newsize);
+    bool process(VisualNode *node = 0);
     bool draw(QPainter *p, const QColor &back = Qt::black);
     void handleKeyPress(const QString &action) {(void) action;}
 
   private:
     void drawRect(QPainter *p, QRect *rect, int i, int c, int w, int h);
+    void rotateHues ();
+
     QSize size;
     MainVisual *pParent;
     int fake_height;
     int number_of_squares;
+    int targetHue;
+    int startHue;
 };
 
 #ifdef OPENGL_SUPPORT
Index: mythplugins/mythmusic/mythmusic/maddecoder.cpp
===================================================================
--- mythplugins/mythmusic/mythmusic/maddecoder.cpp	(revision 16668)
+++ mythplugins/mythmusic/mythmusic/maddecoder.cpp	(working copy)
@@ -278,6 +278,9 @@
 
 void MadDecoder::seek(double pos)
 {
+    if(!input()->isDirectAccess())
+        return;
+
     seekTime = pos;
 }
 
Index: mythplugins/mythmusic/musicdb/scrape-difm.pl
===================================================================
--- mythplugins/mythmusic/musicdb/scrape-difm.pl	(revision 0)
+++ mythplugins/mythmusic/musicdb/scrape-difm.pl	(revision 0)
@@ -0,0 +1,40 @@
+#!/usr/bin/perl
+
+use MythRadioScraper;
+
+if ($#ARGV + 1 != 3) {
+	printf ("Usage: scrape-difm.pl <mode> <radio> <filename>\n");
+	die;
+}
+
+open FILE, $ARGV[2] or die $!;
+
+while (my $line = <FILE>) {
+	my $plsurl = "";
+	my $logourl = "";
+	my $name = "";	
+	my $first = ($line =~ /img src='(http:\/\/www\.di.*\/images\/chbuttons\/.*\.gif)'.*alt='([^']*)/);
+	my $second = ($line =~ /<h2>([^<]*)<\/h2><\/td>/);
+
+	if ($first || $second) {
+		if ($first) {
+			$logourl = $1;
+			$name = $2;
+		} elsif ($second) {
+			$name = $1;
+		}
+		for (my $i = 0; $i < 25; $i++) {
+			$line = <FILE>;
+			if ($line =~ /\"(\/mp3\/.*\.pls).*img src=\"([^\"]*)\".*/) {
+				$plsurl = $1;
+				MythRadioScraper::action ($ARGV[0], $ARGV[1], 
+							  "http://di.fm".$plsurl, 
+							  $logourl, 
+							  $name, 
+							  "");
+			}
+		}
+	}
+}
+
+close (FILE);
Index: mythplugins/mythmusic/musicdb/MythRadioScraper.pm
===================================================================
--- mythplugins/mythmusic/musicdb/MythRadioScraper.pm	(revision 0)
+++ mythplugins/mythmusic/musicdb/MythRadioScraper.pm	(revision 0)
@@ -0,0 +1,43 @@
+package MythRadioScraper;
+
+sub listItem ($$$$) {
+	my ($pls, $img, $name, $genre) = @_;
+	printf ("%s: %s %s %s\n", $name, $genre, $pls, $img);
+}
+
+sub genSQL ($$$$$) {
+	my ($station, $pls, $img, $name, $genre) = @_;
+	printf ("INSERT INTO music_radios SET station = \"%s\",\n", $station);
+	printf ("	channel = \"%s\",\n", $name);
+	printf ("	url = \"%s\",\n", $pls);
+	printf ("	logourl = \"%s\",\n", $img);
+	printf ("	genre = \"%s\",\n", $genre);
+	printf ("	metaformat = \"%%a - %%t\",\n");
+	printf ("	format = \"cast\";\n\n");
+}
+
+sub genDBCheck ($$$$$) {
+	my ($station, $pls, $img, $name, $genre) = @_;
+	printf ("\t\"INSERT INTO music_radios SET station = \\\"%s\\\", \"\n", $station);
+	printf ("\t\"	channel = \\\"%s\\\",\" \n", $name);
+	printf ("\t\"	url = \\\"%s\\\",\" \n", $pls);
+	printf ("\t\"	logourl = \\\"%s\\\",\" \n", $img);
+	printf ("\t\"	genre = \\\"%s\\\",\" \n", $genre);
+	printf ("\t\"	metaformat = \\\"%%a - %%t\\\",\" \n");
+	printf ("\t\"	format = \\\"cast\\\";\", \n\n");
+}
+
+sub action ($$$$$$) {
+	my ($action, $station, $pls, $img, $name, $genre) = @_;
+	if ($action eq "list") {
+		listItem ($pls, $img, $name, $genre);
+	} 
+	if ($action eq "sql") {
+		genSQL ($station, $pls, $img, $name, $genre);
+	}
+	if ($action eq "dbcheck") {
+		genDBCheck ($station, $pls, $img, $name, $genre);
+	}
+}
+
+1;
Index: mythplugins/mythmusic/musicdb/scrape-skyfm.pl
===================================================================
--- mythplugins/mythmusic/musicdb/scrape-skyfm.pl	(revision 0)
+++ mythplugins/mythmusic/musicdb/scrape-skyfm.pl	(revision 0)
@@ -0,0 +1,34 @@
+#!/usr/bin/perl
+
+use MythRadioScraper;
+
+if ($#ARGV + 1 != 3) {
+	printf ("Usage: scrape-difm.pl <mode> <radio> <filename>\n");
+	die;
+}
+
+open FILE, $ARGV[2] or die $!;
+
+while (my $line = <FILE>) {
+	my $plsurl = "";
+	my $logourl = "";
+	my $name = "";
+	if ($line =~ /IMG src=\"(\/images\/chbuttons\/.*\.gif)\"/) {
+		$logourl = $1;
+		$line = <FILE>;
+		if ($line =~ /alt=\"([^\"]*)\"/) {
+			$name = $1;
+			for (my $i = 0; $i < 20; $i++) {
+				$line = <FILE>;
+				if ($line =~ /(http:\/\/www\.sky\.fm\/.*\.pls)/) {
+					$plsurl = $1;
+					MythRadioScraper::action ($ARGV[0], $ARGV[1], 
+								  $plsurl, "http://www.sky.fm".$logourl, $name, 
+								  "");
+				}
+			}
+		}
+	}
+}
+
+close (FILE);
