/* * Cantata * * Copyright (c) 2011-2014 Craig Drummond * * ---- * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "streamsearchmodel.h" #include "widgets/icons.h" #include "roles.h" #include "support/localize.h" #include "playqueuemodel.h" #include "network/networkaccessmanager.h" #include "gui/stdactions.h" #include "gui/settings.h" #include "gui/plurals.h" #include #include #include #include #include #include #if QT_VERSION >= 0x050000 #include #endif StreamSearchModel::StreamSearchModel(QObject *parent) : ActionModel(parent) , category(TuneIn) , root(new StreamsModel::CategoryItem(QString(), "root")) , filterRoot(0) , unmatchedStrings(0) , radioTimeSearchUrl("http://opml.radiotime.com/Search.ashx") , shoutCastSearchUrl(QLatin1String("http://")+StreamsModel::constShoutCastHost+QLatin1String("/legacy/genrelist")) , dirbleSearchUrl(QLatin1String("http://")+StreamsModel::constDirbleHost+QLatin1String("/v1/search/apikey/")+StreamsModel::constDirbleApiKey+QLatin1String("/search/")) { } StreamSearchModel::~StreamSearchModel() { clear(); delete root; } QModelIndex StreamSearchModel::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) { return QModelIndex(); } const StreamsModel::CategoryItem * p = parent.isValid() ? static_cast(parent.internalPointer()) : root; const StreamsModel::Item * c = rowchildren.count() ? p->children.at(row) : 0; return c ? createIndex(row, column, (void *)c) : QModelIndex(); } QModelIndex StreamSearchModel::parent(const QModelIndex &index) const { if (!index.isValid()) { return QModelIndex(); } StreamsModel::Item * parent = toItem(index)->parent; if (!parent || parent == root || !parent->parent) { return QModelIndex(); } return createIndex(static_cast(parent->parent)->children.indexOf(parent), 0, parent); } QVariant StreamSearchModel::headerData(int /*section*/, Qt::Orientation /*orientation*/, int /*role*/) const { return QVariant(); } int StreamSearchModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { StreamsModel::Item *item = toItem(parent); return item->isCategory() ? static_cast(item)->children.count() : 0; } return root->children.count(); } int StreamSearchModel::columnCount(const QModelIndex &) const { return 1; } QVariant StreamSearchModel::data(const QModelIndex &index, int role) const { const StreamsModel::Item *item = toItem(index); switch (role) { case Qt::DecorationRole: return item->isCategory() ? Icons::self()->streamCategoryIcon : Icons::self()->radioStreamIcon; case Qt::DisplayRole: return item->name; case Qt::ToolTipRole: if (!Settings::self()->infoTooltips()) { return QVariant(); } return item->isCategory() ? item->name : (item->name+QLatin1String("
")+item->url+QLatin1String("")); case Cantata::Role_SubText: if (item->isCategory()) { const StreamsModel::CategoryItem *cat=static_cast(item); switch (cat->state) { case StreamsModel::CategoryItem::Initial: return i18n("Not Loaded"); case StreamsModel::CategoryItem::Fetching: return i18n("Loading..."); default: return Plurals::entries(cat->children.count()); } } else { return item->subText.isEmpty() ? QLatin1String("-") : item->subText; } break; case Cantata::Role_Actions: if (item->isCategory()){ if (static_cast(item)->canBookmark) { QVariant v; v.setValue >(QList() << StreamsModel::self()->addBookmarkAct()); return v; } } else { QVariant v; v.setValue >(QList() << StdActions::self()->replacePlayQueueAction << StreamsModel::self()->addToFavouritesAct()); return v; } break; default: break; } return QVariant(); } Qt::ItemFlags StreamSearchModel::flags(const QModelIndex &index) const { if (index.isValid()) { if (toItem(index)->isCategory()) { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } else { return Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsEnabled; } } else { return Qt::NoItemFlags; } } bool StreamSearchModel::hasChildren(const QModelIndex &index) const { return index.isValid() ? toItem(index)->isCategory() : true; } bool StreamSearchModel::canFetchMore(const QModelIndex &index) const { if (index.isValid()) { StreamsModel::Item *item = toItem(index); return item->isCategory() && StreamsModel::CategoryItem::Initial==static_cast(item)->state && !item->url.isEmpty(); } else { return false; } } void StreamSearchModel::fetchMore(const QModelIndex &index) { if (!index.isValid()) { return; } StreamsModel::Item *item = toItem(index); if (item->isCategory() && !item->url.isEmpty()) { StreamsModel::CategoryItem *cat=static_cast(item); NetworkJob *job=NetworkAccessManager::self()->get(cat->url); if (jobs.isEmpty()) { emit loading(); } jobs.insert(job, cat); connect(job, SIGNAL(finished()), this, SLOT(jobFinished())); cat->state=StreamsModel::CategoryItem::Fetching; emit dataChanged(index, index); } } QStringList StreamSearchModel::filenames(const QModelIndexList &indexes, bool addPrefix) const { QStringList fnames; foreach(QModelIndex index, indexes) { StreamsModel::Item *item=static_cast(index.internalPointer()); if (!item->isCategory() && !fnames.contains(item->url)) { fnames << StreamsModel::modifyUrl(item->url, addPrefix, item->name); } } return fnames; } QMimeData * StreamSearchModel::mimeData(const QModelIndexList &indexes) const { QMimeData *mimeData = new QMimeData(); PlayQueueModel::encode(*mimeData, PlayQueueModel::constFileNameMimeType, filenames(indexes, true)); return mimeData; } QStringList StreamSearchModel::mimeTypes() const { QStringList types; types << PlayQueueModel::constFileNameMimeType; return types; } void StreamSearchModel::clear() { cancelAll(); if (!root->children.isEmpty()) { beginRemoveRows(QModelIndex(), 0, root->children.count()-1); if (Filter!=category) { qDeleteAll(root->children); } root->children.clear(); endRemoveRows(); } currentSearch=QString(); } static int getParam(const QString &key, QString &query) { int index=query.indexOf(" "+key+"="); int val=0; if (-1!=index) { int endPos=query.indexOf(" ", index+3); int end=endPos; if (-1==end) { end=query.length()-1; } int start=index+key.length()+2; val=query.mid(start, (end+1)-start).toInt(); if (endPos>start) { query=query.left(index)+query.mid(endPos); query=query.trimmed(); } else { query=query.left(index); } } return val; } void StreamSearchModel::search(const QString &searchTerm, bool stationsOnly) { if (searchTerm==currentSearch) { return; } clear(); currentSearch=searchTerm; QUrl searchUrl; #if QT_VERSION < 0x050000 QUrl &query=searchUrl; #else QUrlQuery query; #endif switch (category) { case Filter: unmatchedStrings=0; filterStrings=searchTerm.split(" ", QString::SkipEmptyParts); if (!filterStrings.isEmpty()) { QList newItems=getStreams(filterRoot); if (!newItems.isEmpty()) { beginInsertRows(QModelIndex(), 0, newItems.count()-1); root->children+=newItems; endInsertRows(); } } emit loaded(); return; case TuneIn: { searchUrl=QUrl(radioTimeSearchUrl); if (stationsOnly) { query.addQueryItem("types", "station"); } query.addQueryItem("query", searchTerm); QString locale=QLocale::system().name(); if (!locale.isEmpty()) { query.addQueryItem("locale", locale); } break; } case ShoutCast: { searchUrl=QUrl(shoutCastSearchUrl); QString search=searchTerm; int limit=getParam("limit", search); int bitrate=getParam("br", search); if (0==bitrate) { bitrate=getParam("bitrate", search); } query.addQueryItem("k", StreamsModel::constShoutCastApiKey); query.addQueryItem("search", search); query.addQueryItem("limit", QString::number(limit<1 ? 100 : limit)); if (bitrate>=32 && bitrate<=512) { query.addQueryItem("br", QString::number(bitrate)); } break; } case Dirble: searchUrl=QUrl(dirbleSearchUrl+searchTerm); break; } #if QT_VERSION >= 0x050000 searchUrl.setQuery(query); #endif NetworkJob *job=NetworkAccessManager::self()->get(searchUrl); if (jobs.isEmpty()) { emit loading(); } jobs.insert(job, root); connect(job, SIGNAL(finished()), this, SLOT(jobFinished())); } void StreamSearchModel::cancelAll() { if (!jobs.isEmpty()) { QList jobList=jobs.keys(); foreach (NetworkJob *j, jobList) { j->cancelAndDelete(); } jobs.clear(); emit loaded(); } } void StreamSearchModel::setCat(Category c) { clear(); category=c; if (c!=Filter) { filterRoot=0; } } void StreamSearchModel::jobFinished() { NetworkJob *job=dynamic_cast(sender()); if (!job) { return; } job->deleteLater(); if (jobs.contains(job)) { StreamsModel::CategoryItem *cat=jobs[job]; cat->state=StreamsModel::CategoryItem::Fetched; jobs.remove(job); QModelIndex index=cat==root ? QModelIndex() : createIndex(cat->parent->children.indexOf(cat), 0, (void *)cat); if (job->ok()) { QList newItems; switch(category) { case TuneIn: newItems=StreamsModel::parseRadioTimeResponse(job->actualJob(), cat, true); break; case ShoutCast: newItems=StreamsModel::parseShoutCastSearchResponse(job->actualJob(), cat); break; case Dirble: newItems=StreamsModel::parseDirbleStations(job->actualJob(), cat); break; default : break; } if (!newItems.isEmpty()) { beginInsertRows(index, cat->children.count(), (cat->children.count()+newItems.count())-1); cat->children+=newItems; endInsertRows(); } } emit dataChanged(index, index); if (jobs.isEmpty()) { emit loaded(); } } } bool StreamSearchModel::matchesFilter(const QString &str) const { if (filterStrings.isEmpty()) { return true; } uint ums = unmatchedStrings; int numStrings = filterStrings.count(); for (int i = 0; i < numStrings; ++i) { if (str.contains(filterStrings.at(i), Qt::CaseInsensitive)) { ums &= ~(1< StreamSearchModel::getStreams(StreamsModel::CategoryItem *cat) { QList streams; if (cat) { foreach (StreamsModel::Item *i, cat->children) { if (i->isCategory()) { streams+=getStreams(static_cast(i)); } else if (matchesFilter(i->name)) { streams.append(i); } } } return streams; }