Index: mythmusic/mythmusic.pro =================================================================== --- mythmusic/mythmusic.pro (revision 22973) +++ 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 22973) +++ 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 22973) +++ mythmusic/metadata.cpp (working copy) @@ -1067,6 +1067,25 @@ 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) + { + int idx = 1; + while (stream_query.next()) + { + // 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 +1158,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 +1228,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 22973) +++ 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 22973) +++ 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 22973) +++ 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 22973) +++ mythmusic/metadata.h (working copy) @@ -346,6 +346,8 @@ void setSorting(QString a_paths); bool putYourselfOnTheListView(TreeCheckItem *where); void putCDOnTheListView(CDCheckItem *where); + bool putStreamsOnTheListView(TreeCheckItem *where); + bool putStramOnTheListView(); bool doneLoading(){return m_done_loading;} bool cleanOutThreads(); int getCDTrackCount(){return m_cd_data.count();} @@ -367,6 +369,7 @@ // you NEED to clear and rebuild the map typedef QMap MusicMap; MusicMap music_map; + MusicMap stream_map; typedef QList ValueMetadata; ValueMetadata m_cd_data; // More than one cd player? Index: mythmusic/metaioavfcomment.cpp =================================================================== --- mythmusic/metaioavfcomment.cpp (revision 22973) +++ mythmusic/metaioavfcomment.cpp (working copy) @@ -14,7 +14,7 @@ MetaIOAVFComment::MetaIOAVFComment(void) : MetaIO() { - QMutexLocker locker(avcodeclock); + QMutexLocker locker(&avcodeclock); av_register_all(); } Index: mythmusic/audiostreammanager.cpp =================================================================== --- mythmusic/audiostreammanager.cpp (revision 0) +++ mythmusic/audiostreammanager.cpp (revision 0) @@ -0,0 +1,375 @@ +// -*- Mode: c++ -*- +// Qt headers +#include + +// MythTV headers +#include +#include + +// MythUI headers +#include +#include +#include +#include +#include +#include +#include + +// MythMusic headers +#include "metadata.h" +#include "decoder.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(GetChild("title_text")); + if (m_title_text == NULL) + { + VERBOSE(VB_IMPORTANT, "Unable to get title_text."); + return false; + } + + m_uri_text = dynamic_cast(GetChild("uri_text")); + if (m_uri_text == NULL) + { + VERBOSE(VB_IMPORTANT, "Unable to get URI text."); + return false; + } + + m_addButton = dynamic_cast(GetChild("add_stream")); + if (m_addButton == NULL) + { + VERBOSE(VB_IMPORTANT, "Unable to get add_stream."); + return false; + } + m_addButton->SetText("New Stream"); + + m_streamList = dynamic_cast(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); + } + } + } + + 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); +} + +void AudioStreamManager::StreamAdded(const QString &uri) +{ + // FIXME I don't think this is actually getting meta-data correctly. Also, + // it would be good to properly test the connection to the stream. + // This also stops M3U/ASX files from working (I think). In any case, + // a better way of testing the stream is needed. + + Decoder *decoder = Decoder::create(uri, NULL, NULL, true); + if ( !decoder ) + { + VERBOSE(VB_IMPORTANT, "ERROR: Unable to create decoder for " + uri); + return; + } + + Metadata *metadata = decoder->readMetadata(); + if ( !metadata ) + { + VERBOSE(VB_IMPORTANT, "ERROR: 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) +{ + // TODO FIXME Before performing any database operations, the URI should be + // tested. + + // 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 22973) +++ mythmusic/databasebox.cpp (working copy) @@ -116,6 +116,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 +212,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); @@ -782,6 +784,20 @@ } } + else if (StreamCheckItem *item_ptr = dynamic_cast(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(parent)) + checkParent(item_ptr); + tree->Redraw(); + } + } else if (TreeCheckItem *item_ptr = dynamic_cast(item)) { if (active_playlist) Index: mythmusic/main.cpp =================================================================== --- mythmusic/main.cpp (revision 22973) +++ 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/metaiomp4.cpp =================================================================== --- mythmusic/metaiomp4.cpp (revision 22973) +++ mythmusic/metaiomp4.cpp (working copy) @@ -16,7 +16,7 @@ MetaIOMP4::MetaIOMP4(void) : MetaIO() { - QMutexLocker locker(avcodeclock); + QMutexLocker locker(&avcodeclock); av_register_all(); } Index: mythmusic/audiostreammanager.h =================================================================== --- mythmusic/audiostreammanager.h (revision 0) +++ mythmusic/audiostreammanager.h (revision 0) @@ -0,0 +1,59 @@ +/** + * \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 + +class MythUIText; +class MythUIButton; +class MythUIButtonList; + + +/** \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); + +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 m_uri_map; +}; + +#endif /* AUDIO_STREAM_MANAGER_H_ */ Index: theme/default/music-ui.xml =================================================================== --- theme/default/music-ui.xml (revision 22973) +++ theme/default/music-ui.xml (working copy) @@ -1525,5 +1525,41 @@ 170,150 + + + + + #ffffff + 18 + yes + + + #9999cc + 18 + yes + + + + + + + + 20,140,370,410 + no + + + + + + Index: theme/menus/musicmenu.xml =================================================================== --- theme/menus/musicmenu.xml (revision 22973) +++ theme/menus/musicmenu.xml (working copy) @@ -135,5 +135,12 @@ Konfigurer avspilling og CD-ripping CONFIGPLUGIN mythmusic + +