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 MusicMap; MusicMap music_map; + MusicMap stream_map; typedef QList 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 #include #include +#include // Myth headers #include #include #include +#include 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 + +// MythTV headers +#include +#include + +// MythUI headers +#include +#include +#include +#include +#include +#include +#include +#include + +// 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(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("Add"); + + 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); + } + } + 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(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 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 + +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 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 @@ 170,150 + + + + + #ffffff + 18 + yes + + + #9999cc + 18 + yes + + + + + + + + 20,140,370,410 + no + + + + + Index: theme/menus/musicmenu.xml =================================================================== --- theme/menus/musicmenu.xml (revision 22996) +++ theme/menus/musicmenu.xml (working copy) @@ -135,5 +135,12 @@ Konfigurer avspilling og CD-ripping CONFIGPLUGIN mythmusic + +