Index: mythmusic/mythmusic.pro
===================================================================
--- mythmusic/mythmusic.pro	(revision 22996)
+++ mythmusic/mythmusic.pro	(working copy)
@@ -39,7 +39,7 @@
 HEADERS += editmetadata.h smartplaylist.h search.h genres.h
 HEADERS += treebuilders.h importmusic.h
 HEADERS += filescanner.h libvisualplugin.h musicplayer.h miniplayer.h
-HEADERS += playlistcontainer.h
+HEADERS += playlistcontainer.h audiostreammanager.h
 HEADERS += mythlistview-qt3.h mythlistbox-qt3.h
 
 SOURCES += cddecoder.cpp cdrip.cpp decoder.cpp
@@ -58,7 +58,7 @@
 SOURCES += avfdecoder.cpp editmetadata.cpp smartplaylist.cpp search.cpp
 SOURCES += treebuilders.cpp importmusic.cpp
 SOURCES += filescanner.cpp libvisualplugin.cpp musicplayer.cpp miniplayer.cpp
-SOURCES += playlistcontainer.cpp
+SOURCES += playlistcontainer.cpp audiostreammanager.cpp
 SOURCES += mythlistview-qt3.cpp mythlistbox-qt3.cpp
 
 macx {
Index: mythmusic/treecheckitem.cpp
===================================================================
--- mythmusic/treecheckitem.cpp	(revision 22996)
+++ mythmusic/treecheckitem.cpp	(working copy)
@@ -148,6 +148,12 @@
 {
 }
 
+StreamCheckItem::StreamCheckItem(UIListGenericTree *parent, const QString &text,
+                                 const QString &level, int track)
+               : TreeCheckItem(parent, text, level, track)
+{
+}
+
 PlaylistItem::PlaylistItem(UIListGenericTree *parent, const QString &title)
             : UIListGenericTree(parent, title, "PLAYLISTITEM", -1)
 {
Index: mythmusic/metadata.cpp
===================================================================
--- mythmusic/metadata.cpp	(revision 22996)
+++ mythmusic/metadata.cpp	(working copy)
@@ -1067,6 +1067,29 @@
     for (; it != m_all_music.end(); ++it)
         music_map[(*it)->ID()] = *it;
 
+
+    // query all of the streams.
+    MSqlQuery stream_query(MSqlQuery::InitCon());
+    if (!stream_query.exec("SELECT id,uri,name FROM music_streams;"))
+        MythDB::DBError("AllMusic::resync (music_streams)", stream_query);
+
+    if (stream_query.isActive() && stream_query.size() > 0)
+    {
+        // have stream id's start where songs leave off to avoid getting song
+        // metadata for a stream in AllMusic::getMetadata.
+        int idx = music_map.keys().last() + 1;
+        while (stream_query.next())
+        {
+            // this little bit it to prevent adding a
+            // create stream metadata and add it to the list
+            Metadata *m = new Metadata();
+            m->setFilename(stream_query.value(1).toString());
+            m->setTitle(stream_query.value(2).toString());
+            stream_map[idx] = m;
+            idx++;
+        }
+    }
+
     //  Build a tree to reflect current state of
     //  the metadata. Once built, sort it.
 
@@ -1139,7 +1162,19 @@
         new_item->setCheck(false); //  Avoiding -Wall
     }
 }
+bool AllMusic::putStreamsOnTheListView(TreeCheckItem *where)
+{
+    MusicMap::iterator it = stream_map.begin();
 
+    for (; it != stream_map.end(); ++it)
+    {
+        StreamCheckItem *item = new StreamCheckItem(where, it.value()->Title(), QObject::tr("title"), it.key());
+        item->setCheck(false);
+    }
+
+    return true;
+}
+
 QString AllMusic::getLabel(int an_id, bool *error_flag)
 {
     QString a_label;
@@ -1197,6 +1232,10 @@
         {
             return music_map[an_id];
         }
+        else if (stream_map.contains(an_id))
+        {
+            return stream_map[an_id];
+        }
     }
     else if(an_id < 0)
     {
Index: mythmusic/dbcheck.cpp
===================================================================
--- mythmusic/dbcheck.cpp	(revision 22996)
+++ mythmusic/dbcheck.cpp	(working copy)
@@ -11,7 +11,7 @@
 #include "mythtv/mythdb.h"
 #include "mythtv/schemawizard.h"
 
-const QString currentDatabaseVersion = "1017";
+const QString currentDatabaseVersion = "1018";
 
 static bool doUpgradeMusicDatabaseSchema(QString &dbver);
 
@@ -777,5 +777,21 @@
             return false;
     }
 
+    if (dbver == "1017")
+    {
+        // the URI length of 256 is arbitrary.
+        const QString updates[] = {
+                "CREATE TABLE IF NOT EXISTS music_streams ("
+                "    id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,"
+                "    uri VARCHAR(512) NOT NULL,"
+                "    name VARCHAR(128) NOT NULL"
+                ");",
+                ""
+        };
+
+        if (!performActualUpdate(updates, "1018", dbver))
+                    return false;
+    }
+
     return true;
 }
Index: mythmusic/treecheckitem.h
===================================================================
--- mythmusic/treecheckitem.h	(revision 22996)
+++ mythmusic/treecheckitem.h	(working copy)
@@ -40,6 +40,14 @@
                 const QString &level, int track);
 };
 
+class StreamCheckItem : public TreeCheckItem
+{
+  public:
+    StreamCheckItem(UIListGenericTree *parent, const QString &text,
+                    const QString &level, int track);
+};
+
+
 class PlaylistItem : public UIListGenericTree
 {
   public:
Index: mythmusic/databasebox.h
===================================================================
--- mythmusic/databasebox.h	(revision 22996)
+++ mythmusic/databasebox.h	(working copy)
@@ -93,6 +93,7 @@
     PlaylistTrack       *track_held;
     TreeCheckItem       *allmusic;
     TreeCheckItem       *alllists;
+    TreeCheckItem       *allstreams;
     PlaylistTitle       *allcurrent;
     Playlist            *active_playlist;
 
Index: mythmusic/metadata.h
===================================================================
--- mythmusic/metadata.h	(revision 22996)
+++ mythmusic/metadata.h	(working copy)
@@ -346,6 +346,7 @@
     void        setSorting(QString a_paths);
     bool        putYourselfOnTheListView(TreeCheckItem *where);
     void        putCDOnTheListView(CDCheckItem *where);
+    bool        putStreamsOnTheListView(TreeCheckItem *where);
     bool        doneLoading(){return m_done_loading;}
     bool        cleanOutThreads();
     int         getCDTrackCount(){return m_cd_data.count();}
@@ -367,6 +368,7 @@
     //  you NEED to clear and rebuild the map
     typedef QMap<int, Metadata*> MusicMap;
     MusicMap music_map;
+    MusicMap stream_map;
 
     typedef QList<Metadata>       ValueMetadata;
     ValueMetadata                 m_cd_data; //  More than one cd player?
Index: mythmusic/avfdecoder.cpp
===================================================================
--- mythmusic/avfdecoder.cpp	(revision 22996)
+++ mythmusic/avfdecoder.cpp	(working copy)
@@ -13,8 +13,9 @@
 
     Revision History
         - Initial release
-        - 1/9/2004 - Improved seek support
-        - ?/?/2009 - Extended to support many more filetypes and bug fixes
+        - 01/09/2004 - Improved seek support
+        - ??/??/2009 - Extended to support many more filetypes and bug fixes
+        - 28/12/2010 - Extended to support playlists for streaming audio.
 */
 
 // C++ headers
@@ -25,11 +26,13 @@
 #include <QObject>
 #include <QIODevice>
 #include <QFile>
+#include <QXmlStreamReader>
 
 // Myth headers
 #include <mythcontext.h>
 #include <audiooutput.h>
 #include <mythverbose.h>
+#include <libmythdb/httpcomms.h>
 
 using namespace std;
 
@@ -44,6 +47,20 @@
 #include "metaiomp4.h"
 #include "metaiowavpack.h"
 
+#define PLAYLIST_EXTENSIONS ".m3u|.asx|.pls"
+enum PlaylistType
+{
+        PL_M3U,
+        PL_ASX,
+        PL_PLS,
+        PL_NONE
+};
+#define M3U_HEADER  "#EXTM3U"
+#define M3U_INFO    "#EXTINF"
+#define ASX_TAG     "ref"
+#define ASX_STREAM  "href"
+#define PLS_FILE    "File"
+
 avfDecoder::avfDecoder(const QString &file, DecoderFactory *d, QIODevice *i,
                        AudioOutput *o) :
     Decoder(d, i, o),
@@ -128,6 +145,64 @@
 
 bool avfDecoder::initialize()
 {
+    filename = ((QFile *)input())->fileName();
+    m_pl_elements.clear();
+
+    // check for a playlist
+    int pl_type = PL_NONE;
+    QString extn(PLAYLIST_EXTENSIONS);
+    QStringList list = extn.split("|", QString::SkipEmptyParts);
+    for (int i = 0; i < list.size(); i++)
+    {
+        QString extn = list[i];
+        if (extn == filename.right(extn.size()))
+        {
+            pl_type = i;
+        }
+    }
+
+    switch (pl_type)
+    {
+    case PL_M3U:
+        parseM3U(filename);
+        break;
+    case PL_ASX:
+        parseASX(filename);
+        break;
+    case PL_PLS:
+        parsePLS(filename);
+        break;
+    default:
+        break;
+    }
+
+    // initialize to the end
+    m_pl_it = m_pl_elements.end();
+
+    if (!m_pl_elements.isEmpty())
+    {
+        m_pl_it = m_pl_elements.begin();
+        filename = *m_pl_it;
+    }
+    else if (pl_type != PL_NONE)
+    {
+        // no streams were pulled from the playlist
+        VERBOSE(VB_GENERAL, "No streams were parsed from the playlist");
+        return false;
+    }
+
+    // open up the stream
+    if (!openStream())
+    {
+        return false;
+    }
+
+    inited = TRUE;
+    return TRUE;
+}
+
+bool avfDecoder::openStream()
+{
     bks = blockSize();
 
     inited = user_stop = done = finish = FALSE;
@@ -136,8 +211,6 @@
     seekTime = -1.0;
     totalTime = 0.0;
 
-    filename = ((QFile *)input())->fileName();
-
     if (!m_samples)
     {
         m_samples = (int16_t *)av_mallocz(AVCODEC_MAX_AUDIO_FRAME_SIZE / 2 *
@@ -159,12 +232,26 @@
     // register av codecs
     av_register_all();
 
-    // open the media file
-    // this should populate the input context
     int error;
-    error = av_open_input_file(&m_inputContext, filename.toLocal8Bit().constData(),
-                               m_inputFormat, 0, &m_params);
+    do
+    {
+        // open the media file
+        // this should populate the input context
+        error = av_open_input_file(&m_inputContext,
+                                   filename.toLocal8Bit().constData(),
+                                   m_inputFormat, 0, &m_params);
 
+        if ((error < 0) && (m_pl_it != m_pl_elements.end()))
+        {
+            VERBOSE(VB_GENERAL, "Unable to open stream '" + filename + "'");
+
+            if (++m_pl_it == m_pl_elements.end())
+                break;
+
+            filename = *m_pl_it;
+        }
+    } while ((error < 0) && (m_pl_it != m_pl_elements.end()));
+
     if (error < 0)
     {
         VERBOSE(VB_IMPORTANT, QString("Could not open file (%1)").arg(filename));
@@ -256,14 +343,13 @@
     if (output())
     {
         const AudioSettings settings(
-            16 /*bits*/, m_audioDec->channels, m_audioDec->codec_id, 
+            16 /*bits*/, m_audioDec->channels, m_audioDec->codec_id,
             m_audioDec->sample_rate, false /* AC3/DTS pass through */);
         output()->Reconfigure(settings);
         output()->SetSourceBitrate(m_audioDec->bit_rate);
     }
 
-    inited = TRUE;
-    return TRUE;
+    return true;
 }
 
 void avfDecoder::seek(double pos)
@@ -276,9 +362,14 @@
     inited = user_stop = done = finish = FALSE;
     len = freq = bitrate = 0;
     stat = m_channels = 0;
-    setInput(0);
-    setOutput(0);
 
+    // we need these if we are playing another file in a playlist
+    if (m_pl_it == m_pl_elements.end())
+    {
+        setInput(0);
+        setOutput(0);
+    }
+
     // Cleanup here
     if (m_inputContext)
     {
@@ -347,6 +438,36 @@
             VERBOSE(VB_IMPORTANT, "Read frame failed");
             VERBOSE(VB_FILE, ("... for file '" + filename) + "'");
             unlock();
+
+            if (m_pl_it != m_pl_elements.end())
+            {
+                // advance to the next playlist element
+                if (++m_pl_it == m_pl_elements.end())
+                {
+                    finish = TRUE;
+                    break;
+                }
+
+                // stop the decoder
+                flush(TRUE);
+                output()->Drain();
+                deinit();
+
+                // set the filename to the next playlist element
+                filename = *m_pl_it;
+
+                // open the next palylist element
+                if (!openStream())
+                {
+                    finish = TRUE;
+                    break;
+                }
+
+                av_read_play(m_inputContext);
+
+                continue;
+            }
+
             finish = TRUE;
             break;
         }
@@ -456,6 +577,132 @@
         return new MetaIOAVFComment();
 }
 
+QString avfDecoder::downloadPlaylist(const QString &uri)
+{
+    QString temp_uri = uri;
+
+    // copied from iptvchannelfetcher.cpp
+    if (temp_uri.left(4).toLower() == "file")
+    {
+        QString ret = "";
+        QUrl qurl(temp_uri);
+        QFile file(qurl.toLocalFile());
+        if (!file.open(QIODevice::ReadOnly))
+        {
+            VERBOSE(VB_IMPORTANT, QString("Error Opening '%1'")
+                    .arg(qurl.toLocalFile()) + ENO);
+            return ret;
+        }
+
+        QTextStream stream(&file);
+        while (!stream.atEnd())
+            ret += stream.readLine() + "\n";
+
+        file.close();
+        return ret;
+    }
+
+    QString tmp = HttpComms::getHttp(
+        temp_uri,
+        10000 /* ms        */, 3     /* retries      */,
+        3     /* redirects */, true  /* allow gzip   */,
+        NULL  /* login     */, true); /* in QT thread */
+
+    QString file = QString::fromUtf8(tmp.toAscii().constData());
+    file.replace("\r\n", "\n");
+
+    return file;
+}
+
+void avfDecoder::parseM3U(const QString &uri)
+{
+    QString file = downloadPlaylist(uri);
+    QStringList lines = file.split("\n");
+
+    QStringList::iterator it;
+    for (it = lines.begin(); it != lines.end(); ++it)
+    {
+        // ignore empty lines
+        if (it->isEmpty())
+            continue;
+
+        // ignore the M3U header
+        if (it->startsWith(M3U_HEADER))
+            continue;
+
+        // for now ignore M3U info lines
+        if (it->startsWith(M3U_INFO))
+            continue;
+
+        // add to the playlist elements
+        m_pl_elements.push_back(*it);
+    }
+}
+
+void avfDecoder::parseASX(const QString &uri)
+{
+    QString file = downloadPlaylist(uri);
+    QXmlStreamReader reader(file.toAscii());
+    while (!reader.atEnd())
+    {
+        QXmlStreamReader::TokenType tok_type;
+        tok_type = reader.readNext();
+
+        switch (tok_type)
+        {
+        case QXmlStreamReader::StartElement:
+            if (reader.name().toString().toLower() == ASX_TAG)
+            {
+                QXmlStreamAttributes attribs = reader.attributes();
+                QXmlStreamAttributes::iterator it = attribs.begin();
+                for (; it != attribs.end(); ++it)
+                {
+                    if (it->name().toString().toLower() == ASX_STREAM)
+                    {
+                        m_pl_elements.push_back(it->value().toString());
+                    }
+                }
+            }
+            break;
+        default:
+            break;
+        }
+    }
+
+    if (reader.hasError())
+    {
+        VERBOSE(VB_GENERAL, "Unable to parse ASX stream: " +
+                reader.errorString());
+    }
+}
+
+void avfDecoder::parsePLS(const QString &uri)
+{
+    QString file = downloadPlaylist(uri);
+    QStringList elements, lines = file.split("\n");;
+
+    QStringList::iterator it;
+    for (it = lines.begin(); it != lines.end(); ++it)
+    {
+        // ignore empty lines
+        if (it->isEmpty())
+            continue;
+
+        if (it->startsWith(PLS_FILE))
+        {
+            // get the index
+            int idx = it->indexOf('=');
+
+            // ensure the index is valid
+            if (idx < 0)
+                continue;
+
+            // parse the stream and add it to the playlist elements
+            m_pl_elements.push_back(it->right(it->size() - idx - 1));
+        }
+    }
+}
+
 bool avfDecoderFactory::supports(const QString &source) const
 {
     QStringList list = extension().split("|", QString::SkipEmptyParts);
@@ -465,13 +712,25 @@
             return true;
     }
 
-    return false;
+    // if the extension is unknown, see if the decoder can initialise
+    // since some streams are just a URI (with no file extension)
+    QFile *file = new QFile(source);
+    avfDecoder *decoder = new avfDecoder(source, NULL, file, NULL);
+    bool supported = decoder->initialize();
+
+    // clean up
+    delete decoder;
+    delete file;
+
+    // return the result of initialization
+    return supported;
 }
 
 const QString &avfDecoderFactory::extension() const
 {
     static QString ext(".mp3|.mp2|.ogg|.oga|.flac|.wma|.wav|.ac3|.oma|.omg|"
-                       ".atp|.ra|.dts|.aac|.m4a|.aa3|.tta|.mka|.aiff|.swa|.wv");
+                       ".atp|.ra|.dts|.aac|.m4a|.aa3|.tta|.mka|.aiff|.swa|.wv|"
+                       PLAYLIST_EXTENSIONS);
     return ext;
 }
 
@@ -500,4 +759,3 @@
 
     return decoder;
 }
-
Index: mythmusic/audiostreammanager.cpp
===================================================================
--- mythmusic/audiostreammanager.cpp	(revision 0)
+++ mythmusic/audiostreammanager.cpp	(revision 0)
@@ -0,0 +1,433 @@
+// -*- Mode: c++ -*-
+// Qt headers
+#include <QString>
+
+// MythTV headers
+#include <mythdb.h>
+#include <mythcontext.h>
+
+// MythUI headers
+#include <libmythui/xmlparsebase.h>
+#include <libmythui/mythmainwindow.h>
+#include <libmythui/mythuitext.h>
+#include <libmythui/mythuibutton.h>
+#include <libmythui/mythuibuttonlist.h>
+#include <libmythui/mythuiprogressbar.h>
+#include <libmythui/myththemedmenu.h>
+#include <libmythui/mythdialogbox.h>
+
+// MythMusic headers
+#include "metadata.h"
+#include "decoder.h"
+#include "avfdecoder.h"
+#include "musicplayer.h"
+#include "audiostreammanager.h"
+
+#define STREAM_TIMEOUT 10000
+
+AudioStreamManager::AudioStreamManager(MythScreenStack *parent)
+    : MythScreenType (parent, "audiostreammanager")
+{
+}
+
+
+AudioStreamManager::~AudioStreamManager()
+{
+}
+
+
+bool AudioStreamManager::Create(void)
+{
+    // Load the theme for this screen
+    if (!XMLParseBase::LoadWindowFromXML(QString("music-ui.xml"), QString("stream_manager"), this))
+    {
+        VERBOSE(VB_IMPORTANT, "Unable audio stream manager to load window from xml.");
+        return false;
+    }
+
+    // UI components
+    m_title_text = dynamic_cast<MythUIText *>(GetChild("title_text"));
+    if (m_title_text == NULL)
+    {
+        VERBOSE(VB_IMPORTANT, "Unable to get title_text.");
+        return false;
+    }
+
+    m_uri_text = dynamic_cast<MythUIText *>(GetChild("uri_text"));
+    if (m_uri_text == NULL)
+    {
+        VERBOSE(VB_IMPORTANT, "Unable to get URI text.");
+        return false;
+    }
+
+    m_addButton = dynamic_cast<MythUIButton *>(GetChild("add_stream"));
+    if (m_addButton == NULL)
+    {
+        VERBOSE(VB_IMPORTANT, "Unable to get add_stream.");
+        return false;
+    }
+    m_addButton->SetText("Add");
+
+    m_streamList = dynamic_cast<MythUIButtonList *>(GetChild("stream_list"));
+    if (m_streamList == NULL)
+    {
+        VERBOSE(VB_IMPORTANT, "Unable to get stream_list.");
+        return false;
+    }
+
+    // set the title
+    m_title_text->SetText(tr("Audio Streams"));
+
+    connect(m_streamList, SIGNAL(itemClicked(MythUIButtonListItem*)),
+            SLOT(StreamClicked(MythUIButtonListItem*)));
+
+    connect(m_streamList, SIGNAL(itemSelected(MythUIButtonListItem*)),
+            SLOT(StreamSelected(MythUIButtonListItem*)));
+
+    connect(m_addButton, SIGNAL(Clicked()), SLOT(AddStream()));
+
+    SetFocusWidget(m_addButton);
+
+    if (!PopulateStreamList())
+    {
+        MythPopupBox::showOkPopup(gContext->GetMainWindow(), tr("ERROR"),
+                                  tr("Unable to populate stream list."));
+        return false;
+    }
+
+    return true;
+}
+
+bool AudioStreamManager::keyPressEvent(QKeyEvent *event)
+{
+    if (GetFocusWidget()->keyPressEvent(event))
+        return true;
+
+    QStringList actions;
+    bool handled = GetMythMainWindow()->TranslateKeyPress("Global", event, actions);
+
+    for (int i = 0; i < actions.size() && !handled; i++)
+    {
+        QString action = actions[i];
+        MythUIType *widget = GetFocusWidget();
+
+        if ((action == "UP") && (widget == m_streamList))
+        {
+            if (m_streamList->GetCurrentPos() == 0)
+            {
+                SetFocusWidget(m_addButton);
+            }
+        }
+        else if (action == "DOWN")
+        {
+            if (widget == m_addButton)
+            {
+                SetFocusWidget(m_streamList);
+            }
+        }
+        else if (action == "DELETE")
+        {
+            if (GetFocusWidget() == m_streamList)
+            {
+                int id = m_streamList->GetItemCurrent()->GetData().toInt();
+                DeleteStream(id);
+            }
+        }
+    }
+
+    if (!handled && MythScreenType::keyPressEvent(event))
+        handled = true;
+
+    return handled;
+}
+
+void AudioStreamManager::StreamClicked(MythUIButtonListItem *item)
+{
+    QStringList buttons;
+    buttons << tr("Edit Title") << tr("Edit URI") << tr("Delete Stream");
+
+    MythMainWindow *main_window = gContext->GetMainWindow();
+    DialogCode c = MythPopupBox::ShowButtonPopup(main_window, tr("Edit Stream"),
+                                                 tr("What would you like to do?"),
+                                                 buttons, kDialogCodeButton0);
+
+    int id = item->GetData().toInt();
+
+    switch (c)
+    {
+    case kDialogCodeButton0:
+        UpdateTitle(item->GetText());
+        break;
+    case kDialogCodeButton1:
+        UpdateURI(m_uri_map[id]);
+        break;
+    case kDialogCodeButton2:
+        DeleteStream(id);
+        break;
+    default:
+        break;
+    }
+}
+
+void AudioStreamManager::StreamSelected(MythUIButtonListItem *item)
+{
+    m_uri_text->SetText(m_uri_map[item->GetData().toInt()]);
+}
+
+void AudioStreamManager::AddStream()
+{
+    MythTextInputDialog *uriBox;
+    MythScreenStack *stack = GetMythMainWindow()->GetStack("popup stack");
+
+    uriBox = new MythTextInputDialog(stack, tr("Stream URI"));
+
+    connect(uriBox, SIGNAL(haveResult(QString)), SLOT(StreamAdded(QString)));
+
+    if (!uriBox->Create())
+    {
+        VERBOSE(VB_IMPORTANT, "ERROR: Unable to create text input dialog.");
+        return;
+    }
+
+    stack->AddScreen(uriBox, false);
+}
+
+Metadata* AudioStreamManager::TestStream(const QString & uri)
+{
+    // TODO timeout if the stream takes too long to test
+
+    // show a progress dialog
+    MythProgressDialog *prog = new MythProgressDialog(tr("Testing Stream"), 4);
+    if (prog == NULL)
+    {
+        VERBOSE(VB_IMPORTANT, "Unable to create progress dialog.");
+        return NULL;
+    }
+
+    // TODO Despite allowing feedback on the state of the stream, it would be
+    //      easier to just see if the stream is supported by the decoder
+    //      instead of initialising the stream (especially since most streams
+    //      are wrapped in playlists). Also, it would probably be better to use
+    //      a MythUI dialog. This would also allow us to not have to check each
+    //      decoder manually
+    prog->setLabel(tr("Connecting"));
+    prog->setProgress(0);
+    QFile *file = new QFile(uri);
+
+    // create the decoder
+    prog->setLabel(tr("Creating decoder"));
+    prog->setProgress(1);
+    avfDecoder *decoder = new avfDecoder(uri, NULL, file, NULL);
+
+    prog->setLabel(tr("Initialising decoder"));
+    prog->setProgress(2);
+    if (!decoder->initialize())
+    {
+        prog->Close();
+        prog->deleteLater();
+        delete decoder;
+        delete file;
+        return NULL;
+    }
+
+    prog->setLabel(tr("Reading metadata"));
+    prog->setProgress(3);
+    Metadata *metadata = decoder->readMetadata();
+
+    prog->Close();
+    prog->deleteLater();
+    delete decoder;
+    delete file;
+
+    return metadata;
+}
+
+void AudioStreamManager::StreamAdded(const QString &uri)
+{
+    Metadata *metadata = TestStream(uri);
+
+    if ( !metadata )
+    {
+        VERBOSE(VB_IMPORTANT, "ERROR: Unable to get metadata for " + uri);
+        MythPopupBox::showOkPopup(gContext->GetMainWindow(), tr("ERROR"),
+                                  tr("Unable to get metadata for ") + uri);
+        return;
+    }
+
+    QString name = metadata->Title();
+
+    MSqlQuery query(MSqlQuery::InitCon());
+    query.prepare("INSERT INTO music_streams (uri,name) VALUES (:URI,:NAME)");
+    query.bindValue(":URI", uri);
+    query.bindValue(":NAME", name);
+
+    if (!query.exec() || !query.isActive())
+    {
+        MythDB::DBError("KeyBindings::CommitAction", query);
+        return;
+    }
+
+    PopulateStreamList();
+}
+
+
+bool AudioStreamManager::PopulateStreamList()
+{
+    MSqlQuery query(MSqlQuery::InitCon());
+    query.prepare("SELECT id, uri, name FROM music_streams;");
+
+    if (!query.exec() || !query.isActive())
+    {
+        VERBOSE(VB_IMPORTANT, "ERROR: Unable to query music_streams;");
+        return false;
+    }
+
+    m_streamList->Reset();
+
+    while (query.next())
+    {
+        int id = query.value(0).toInt();
+        m_uri_map[id] = query.value(1).toString();
+        QString name = query.value(2).toString();
+        QVariant variant(id);
+        new MythUIButtonListItem(m_streamList, name, variant);
+    }
+
+    return true;
+}
+
+void AudioStreamManager::UpdateURI(const QString &uri)
+{
+    MythTextInputDialog *uriBox;
+    MythScreenStack *stack = GetMythMainWindow()->GetStack("popup stack");
+
+    uriBox = new MythTextInputDialog(stack, tr("Update Stream URI"),
+                                     FilterNone, false, uri);
+
+    connect(uriBox, SIGNAL(haveResult(QString)), SLOT(URIUpdated(QString)));
+
+    if (!uriBox->Create())
+    {
+        VERBOSE(VB_IMPORTANT, "ERROR: Unable to create text input dialog.");
+        return;
+    }
+
+    stack->AddScreen(uriBox, false);
+}
+
+void AudioStreamManager::URIUpdated(const QString &uri)
+{
+    // retest the stream
+    if (TestStream(uri) == NULL)
+    {
+        MythPopupBox::showOkPopup(gContext->GetMainWindow(), tr("ERROR"),
+                                  tr("Stream test failed."));
+        return;
+    }
+
+    // get the database id
+    int id = m_streamList->GetItemCurrent()->GetData().toInt();
+
+    // try to update the stream in the database
+    MSqlQuery query(MSqlQuery::InitCon());
+    query.prepare("UPDATE music_streams SET uri = :URI WHERE id = :ID");
+    query.bindValue(":URI", uri);
+    query.bindValue(":ID", id);
+
+    // if updating failed show an error and return
+    if (!query.exec() || !query.isActive())
+    {
+        MythPopupBox::showOkPopup(gContext->GetMainWindow(), tr("ERROR"),
+                                  tr("Unable to update URI."));
+        return;
+    }
+
+    // update the URI locally
+    m_uri_map[id] = uri;
+}
+
+void AudioStreamManager::UpdateTitle(const QString &title)
+{
+    MythTextInputDialog *titleBox;
+    MythScreenStack *stack = GetMythMainWindow()->GetStack("popup stack");
+
+    titleBox = new MythTextInputDialog(stack, tr("Update Stream Title"),
+                                     FilterNone, false, title);
+
+    connect(titleBox, SIGNAL(haveResult(QString)),
+            SLOT(TitleUpdated(QString)));
+
+    if (!titleBox->Create())
+    {
+        VERBOSE(VB_IMPORTANT, "ERROR: Unable to create text input dialog.");
+        return;
+    }
+
+    stack->AddScreen(titleBox, false);
+}
+
+void AudioStreamManager::TitleUpdated(const QString &title)
+{
+    // prevent empty titles
+    if (title.isEmpty())
+    {
+        MythPopupBox::showOkPopup(gContext->GetMainWindow(), tr("ERROR"),
+                tr("Empty titles are not valid."));
+        return;
+    }
+
+    // get the button and the database id
+    MythUIButtonListItem *item = m_streamList->GetItemCurrent();
+    int id = item->GetData().toInt();
+
+    // try to update the stream in the database
+    MSqlQuery query(MSqlQuery::InitCon());
+    query.prepare("UPDATE music_streams SET name = :NAME WHERE id = :ID");
+    query.bindValue(":NAME", title);
+    query.bindValue(":ID", id);
+
+    // if updating failed show an error and return
+    if (!query.exec() || !query.isActive())
+    {
+        MythPopupBox::showOkPopup(gContext->GetMainWindow(), tr("ERROR"),
+                                  tr("Unable to update Title."));
+        return;
+    }
+
+    item->SetText(title);
+}
+
+void AudioStreamManager::DeleteStream(int id)
+{
+    // confirm before deleting
+    DialogCode d = MythPopupBox::Show2ButtonPopup(gContext->GetMainWindow(),
+                                                  tr("Really Delete"),
+                                                  tr("Are you sure you want "
+                                                     "to delete this stream?"),
+                                                  tr("Yes"), tr("No"),
+                                                  kDialogCodeButton1);
+
+    // do nothing if No was selected
+    if (d == kDialogCodeButton1)
+    {
+        return;
+    }
+
+
+    MSqlQuery query(MSqlQuery::InitCon());
+    query.prepare("DELETE FROM music_streams WHERE id = :ID");
+    query.bindValue(":ID", id);
+
+    // if updating failed show an error and return
+    if (!query.exec() || !query.isActive())
+    {
+        MythPopupBox::showOkPopup(gContext->GetMainWindow(), tr("ERROR"),
+                                  tr("Unable to delete stream."));
+        return;
+    }
+
+    // refresh the streams
+    PopulateStreamList();
+}
+
+
Index: mythmusic/databasebox.cpp
===================================================================
--- mythmusic/databasebox.cpp	(revision 22996)
+++ mythmusic/databasebox.cpp	(working copy)
@@ -90,17 +90,19 @@
             m_lines.push_back(line);
     }
 
-    if (m_lines.size() < 3)
+    if (m_lines.size() < 6)
     {
         DialogBox *dlg = new DialogBox(
             gContext->GetMainWindow(),
-            tr("The theme you are using does not contain any info "
+            tr("The theme you are using does not contain enough info "
                "lines in the music element. Please contact the theme "
                "creator and ask if they could please update it."));
 
         dlg->AddButton(tr("OK"));
         dlg->exec();
         dlg->deleteLater();
+
+        return;
     }
 
     connect(tree, SIGNAL(itemEntered(UIListTreeType *, UIListGenericTree *)),
@@ -116,6 +118,7 @@
         cditem = new CDCheckItem(rootNode, tr("Blechy Blech Blah"), "cd", 0);
     alllists = new TreeCheckItem(rootNode, tr("All My Playlists"), "genre", 0);
     allcurrent = new PlaylistTitle(rootNode, tr("Active Play Queue"));
+    allstreams = new StreamCheckItem(rootNode, tr("All Internet Streams"), "genre", 0);
 
     tree->SetTree(rootNode);
 
@@ -211,6 +214,7 @@
         //  Good, now lets grab some QListItems
         if (gMusicData->all_music->putYourselfOnTheListView(allmusic))
         {
+            gMusicData->all_music->putStreamsOnTheListView(allstreams);
             allmusic->setText(tr("All My Music"));
             fill_list_timer->stop();
             gMusicData->all_playlists->setActiveWidget(allcurrent);
@@ -688,7 +692,6 @@
                 timeStr.sprintf("%02d:%02d", maxm, maxs);
 
             tmpstr = tr("Length:\t") + timeStr;
-
             m_lines.at(line++)->SetText(tmpstr);
         }
 
@@ -782,6 +785,20 @@
         }
 
     }
+    else if (StreamCheckItem *item_ptr = dynamic_cast<StreamCheckItem*>(item))
+    {
+        if (active_playlist)
+        {
+            if (item_ptr->getCheck() > 0)
+                item_ptr->setCheck(0);
+            else
+                item_ptr->setCheck(2);
+            doSelected(item_ptr, false);
+            if (item_ptr = dynamic_cast<StreamCheckItem*>(parent))
+                checkParent(item_ptr);
+            tree->Redraw();
+        }
+    }
     else if (TreeCheckItem *item_ptr = dynamic_cast<TreeCheckItem*>(item))
     {
         if (active_playlist)
Index: mythmusic/main.cpp
===================================================================
--- mythmusic/main.cpp	(revision 22996)
+++ mythmusic/main.cpp	(working copy)
@@ -34,6 +34,7 @@
 #include "filescanner.h"
 #include "musicplayer.h"
 #include "config.h"
+#include "audiostreammanager.h"
 #ifndef USING_MINGW
 #include "cdrip.h"
 #include "importmusic.h"
@@ -211,6 +212,19 @@
 //   connect(import, SIGNAL(Changed()), SLOT(RebuildMusicTree()));
 }
 
+void manageStreams(void)
+{
+    MythScreenStack *mainStack = GetMythMainWindow()->GetMainStack();
+    AudioStreamManager *manager = new AudioStreamManager(mainStack);
+    if (!manager->Create())
+    {
+        delete manager;
+        return;
+    }
+
+    mainStack->AddScreen(manager);
+}
+
 void RebuildMusicTree(void)
 {
     if (!gMusicData->all_music || !gMusicData->all_playlists)
@@ -291,6 +305,10 @@
                 postMusic();
         }
     }
+    else if (sel == "music_manage_streams")
+    {
+        manageStreams();
+    }
 }
 
 int runMenu(QString which_menu)
Index: mythmusic/avfdecoder.h
===================================================================
--- mythmusic/avfdecoder.h	(revision 22996)
+++ mythmusic/avfdecoder.h	(working copy)
@@ -30,6 +30,11 @@
 
     void flush(bool = FALSE);
     void deinit();
+    bool openStream();
+    QString downloadPlaylist(const QString &uri);
+    void parseM3U(const QString &uri);
+    void parseASX(const QString &uri);
+    void parsePLS(const QString &uri);
 
     bool inited, user_stop;
     int stat;
@@ -57,6 +62,8 @@
     AVCodecContext *m_audioEnc, *m_audioDec;
     AVPacket m_pkt1;
     AVPacket *m_pkt;
+    QStringList m_pl_elements;
+    QStringList::iterator m_pl_it;
 
     int errcode;
 
Index: mythmusic/audiostreammanager.h
===================================================================
--- mythmusic/audiostreammanager.h	(revision 0)
+++ mythmusic/audiostreammanager.h	(revision 0)
@@ -0,0 +1,62 @@
+/**
+ * \file AudioStreamManager.h
+ * \author Micah Galizia
+ * \brief Provides an interface to add online streams in mythmusic.
+ */
+
+#ifndef AUDIO_STREAM_MANAGER_H_
+#define AUDIO_STREAM_MANAGER_H_
+
+// MythUI
+#include <mythscreentype.h>
+
+class MythUIText;
+class MythUIButton;
+class MythUIButtonList;
+class Metadata;
+
+
+/** \class AudioStreamManager
+ *  \brief Screen that manages audio streams.
+ */
+class AudioStreamManager : public MythScreenType
+{
+    Q_OBJECT
+public:
+    AudioStreamManager(MythScreenStack *parent);
+
+    virtual ~AudioStreamManager();
+
+    virtual bool Create(void);
+
+    virtual bool keyPressEvent(QKeyEvent *);
+
+protected:
+
+    virtual bool PopulateStreamList();
+
+    virtual void UpdateURI(const QString &uri);
+
+    virtual void UpdateTitle(const QString &title);
+
+    virtual void DeleteStream(int id);
+
+    virtual Metadata* TestStream(const QString & uri);
+
+private slots:
+    void StreamClicked(MythUIButtonListItem*);
+    void StreamSelected(MythUIButtonListItem*);
+    void AddStream();
+    void StreamAdded(const QString &);
+    void URIUpdated(const QString &);
+    void TitleUpdated(const QString &);
+private:
+
+    MythUIButton       *m_addButton;
+    MythUIButtonList   *m_streamList;
+    MythUIText         *m_title_text;
+    MythUIText         *m_uri_text;
+    QMap<int, QString>  m_uri_map;
+};
+
+#endif /* AUDIO_STREAM_MANAGER_H_ */
Index: theme/default/music-ui.xml
===================================================================
--- theme/default/music-ui.xml	(revision 22996)
+++ theme/default/music-ui.xml	(working copy)
@@ -1525,5 +1525,40 @@
             <position>170,150</position>
         </progressbar>
     </window>
+    
+    <window name="stream_manager">
+    
+    	<font name="active" face="Arial">
+            <color>#ffffff</color>
+            <size>18</size>
+            <bold>yes</bold>
+        </font>
 
+        <font name="inactive" face="Arial">
+            <color>#9999cc</color>
+            <size>18</size>
+            <bold>yes</bold>
+        </font>
+        
+        <textarea name="title_text" from="basetextarea">
+            <area>20,20,760,40</area>
+            <value>Contexts</value>
+            <align>center</align>
+        </textarea>
+                
+        <button name="add_stream" from="basebutton">
+            <position>20,80</position>
+        </button>
+        
+		<buttonlist name="stream_list" from="basebuttonlist">
+            <area>20,140,370,410</area>
+            <showarrow>no</showarrow>
+        </buttonlist>
+
+        <textarea name="uri_text" from="basetextarea">
+            <area>20,570,760,30</area>
+            <font>basemedium</font>
+        </textarea>
+    </window>
+
 </mythuitheme>
Index: theme/menus/musicmenu.xml
===================================================================
--- theme/menus/musicmenu.xml	(revision 22996)
+++ theme/menus/musicmenu.xml	(working copy)
@@ -135,5 +135,12 @@
      <description lang="NB">Konfigurer avspilling og CD-ripping</description>
      <action>CONFIGPLUGIN mythmusic</action>
    </button>
+   
+   <button>
+     <type>MUSIC_MANAGE_STREAMS</type>
+     <text>Manage Online Stream</text>
+     <description>Manage internet audio stream.</description>
+     <action>MUSIC_MANAGE_STREAMS</action>
+   </button>
 
 </mythmenu>
