1256 lines
43 KiB
C++
1256 lines
43 KiB
C++
/*
|
|
* Cantata
|
|
*
|
|
* Copyright (c) 2011-2013 Craig Drummond <craig.p.drummond@gmail.com>
|
|
*
|
|
* ----
|
|
*
|
|
* 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 "streamsmodel.h"
|
|
#include "icon.h"
|
|
#include "icons.h"
|
|
#include "networkaccessmanager.h"
|
|
#include "localize.h"
|
|
#include "utils.h"
|
|
#include "mpdconnection.h"
|
|
#include "mpdparseutils.h"
|
|
#include "settings.h"
|
|
#include "playqueuemodel.h"
|
|
#include "itemview.h"
|
|
#include "action.h"
|
|
#include "stdactions.h"
|
|
#include "digitallyimported.h"
|
|
#include "qjson/parser.h"
|
|
#include "qtiocompressor/qtiocompressor.h"
|
|
#include <QModelIndex>
|
|
#include <QString>
|
|
#include <QVariant>
|
|
#include <QMimeData>
|
|
#include <QXmlStreamReader>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QTimer>
|
|
#include <QLocale>
|
|
#if QT_VERSION >= 0x050000
|
|
#include <QUrlQuery>
|
|
#endif
|
|
#ifdef Q_OS_WIN
|
|
#include <QDesktopServices>
|
|
#endif
|
|
#ifdef ENABLE_KDE_SUPPORT
|
|
#include <KDE/KGlobal>
|
|
K_GLOBAL_STATIC(StreamsModel, instance)
|
|
#endif
|
|
|
|
StreamsModel * StreamsModel::self()
|
|
{
|
|
#ifdef ENABLE_KDE_SUPPORT
|
|
return instance;
|
|
#else
|
|
static StreamsModel *instance=0;
|
|
if(!instance) {
|
|
instance=new StreamsModel;
|
|
}
|
|
return instance;
|
|
#endif
|
|
}
|
|
|
|
const QString StreamsModel::constPrefix("cantata-");
|
|
|
|
static const char * constOrigUrlProperty = "orig-url";
|
|
|
|
static QString constRadioTimeHost=QLatin1String("opml.radiotime.com");
|
|
static QString constRadioTimeUrl=QLatin1String("http://")+constRadioTimeHost+QLatin1String("/Browse.ashx");
|
|
static QString constFavouritesUrl=QLatin1String("cantata://internal");
|
|
static QString constIceCastUrl=QLatin1String("http://dir.xiph.org/yp.xml");
|
|
static QString constSomaFMUrl=QLatin1String("http://somafm.com/channels.xml");
|
|
|
|
static QString constDigitallyImportedUrl=QLatin1String("http://www.di.fm");
|
|
static QString constJazzRadioUrl=QLatin1String("http://www.jazzradio.com");
|
|
static QString constRockRadioUrl=QLatin1String("http://www.rockradio.com");
|
|
static QString constSkyFmUrl=QLatin1String("http://www.sky.fm");
|
|
static QStringList constDiUrls=QStringList() << constDigitallyImportedUrl << constJazzRadioUrl << constSkyFmUrl << constRockRadioUrl;
|
|
|
|
static const QString constDiChannelListHost=QLatin1String("api.v2.audioaddict.com");
|
|
static const QString constDiChannelListUrl=QLatin1String("http://")+constDiChannelListHost+("/v1/%1/mobile/batch_update?asset_group_key=mobile_icons&stream_set_key=");
|
|
static const QString constDiStdUrl=QLatin1String("http://%1/public3/%2.pls");
|
|
|
|
static QString constShoutCastApiKey=QLatin1String("fa1669MuiRPorUBw");
|
|
static QString constShoutCastHost=QLatin1String("api.shoutcast.com");
|
|
static QString constShoutCastUrl=QLatin1String("http://")+constShoutCastHost+QLatin1String("/genre/primary?f=xml&k=")+constShoutCastApiKey;
|
|
|
|
static const QLatin1String constFavouritesFileName("streams.xml.gz");
|
|
|
|
QString StreamsModel::favouritesDir()
|
|
{
|
|
return Settings::self()->storeStreamsInMpdDir() ? MPDConnection::self()->getDetails().dir : Utils::configDir(QString(), false);
|
|
}
|
|
|
|
static QString getInternalFile(bool createDir=false)
|
|
{
|
|
if (Settings::self()->storeStreamsInMpdDir()) {
|
|
return MPDConnection::self()->getDetails().dir+constFavouritesFileName;
|
|
}
|
|
return Utils::configDir(QString(), createDir)+constFavouritesFileName;
|
|
}
|
|
|
|
static QIcon getIcon(const QString &name)
|
|
{
|
|
#ifdef Q_OS_WIN
|
|
QString dir(QCoreApplication::applicationDirPath()+"/streamicons/");
|
|
#else
|
|
QString dir(QString(INSTALL_PREFIX"/share/")+QCoreApplication::applicationName()+"/streamicons/");
|
|
#endif
|
|
if (QFile::exists(dir+name+".svg")) {
|
|
return QIcon(dir+name+".svg");
|
|
} else if (QFile::exists(dir+name+".png")) {
|
|
return QIcon(dir+name+".png");
|
|
}
|
|
return Icons::self()->streamCategoryIcon;
|
|
}
|
|
|
|
StreamsModel::StreamsModel(QObject *parent)
|
|
: ActionModel(parent)
|
|
, root(new CategoryItem(QString(), "root"))
|
|
, favouritesIsWriteable(true)
|
|
, favouritesModified(false)
|
|
, favouritesSaveTimer(0)
|
|
{
|
|
root->children.append(new CategoryItem(constRadioTimeUrl+QLatin1String("?locale=")+QLocale::system().name(), i18n("TuneIn"), root, getIcon("tunein")));
|
|
root->children.append(new CategoryItem(constIceCastUrl, i18n("IceCast"), root, getIcon("icecast")));
|
|
root->children.append(new CategoryItem(constShoutCastUrl, i18n("ShoutCast"), root, getIcon("shoutcast")));
|
|
root->children.append(new CategoryItem(constSomaFMUrl, i18n("SomaFM"), root, getIcon("somafm")));
|
|
root->children.append(new CategoryItem(constDigitallyImportedUrl, i18n("Digitally Imported"), root, getIcon("digitallyimported")));
|
|
root->children.append(new CategoryItem(constJazzRadioUrl, i18n("JazzRadio.com"), root, getIcon("jazzradio")));
|
|
root->children.append(new CategoryItem(constRockRadioUrl, i18n("RockRadio.com"), root, getIcon("rockradio")));
|
|
root->children.append(new CategoryItem(constSkyFmUrl, i18n("Sky.fm"), root, getIcon("skyfm")));
|
|
favourites=new CategoryItem(constFavouritesUrl, i18n("Favourites"), root, getIcon("favourites"));
|
|
favourites->isFavourites=true;
|
|
root->children.append(favourites);
|
|
buildListenLive();
|
|
}
|
|
|
|
StreamsModel::~StreamsModel()
|
|
{
|
|
delete root;
|
|
}
|
|
|
|
QModelIndex StreamsModel::index(int row, int column, const QModelIndex &parent) const
|
|
{
|
|
if (!hasIndex(row, column, parent)) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
const CategoryItem * p = parent.isValid() ? static_cast<CategoryItem *>(parent.internalPointer()) : root;
|
|
const Item * c = row<p->children.count() ? p->children.at(row) : 0;
|
|
return c ? createIndex(row, column, (void *)c) : QModelIndex();
|
|
}
|
|
|
|
QModelIndex StreamsModel::parent(const QModelIndex &index) const
|
|
{
|
|
if (!index.isValid()) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
Item * parent = toItem(index)->parent;
|
|
|
|
if (!parent || parent == root || !parent->parent) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
return createIndex(static_cast<CategoryItem *>(parent->parent)->children.indexOf(parent), 0, parent);
|
|
}
|
|
|
|
QVariant StreamsModel::headerData(int /*section*/, Qt::Orientation /*orientation*/, int /*role*/) const
|
|
{
|
|
return QVariant();
|
|
}
|
|
|
|
int StreamsModel::rowCount(const QModelIndex &parent) const
|
|
{
|
|
if (parent.isValid()) {
|
|
Item *item = toItem(parent);
|
|
return item->isCategory() ? static_cast<CategoryItem *>(item)->children.count() : 0;
|
|
}
|
|
|
|
return root->children.count();
|
|
}
|
|
|
|
int StreamsModel::columnCount(const QModelIndex &) const
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
QVariant StreamsModel::data(const QModelIndex &index, int role) const
|
|
{
|
|
const Item *item = toItem(index);
|
|
|
|
switch (role) {
|
|
case Qt::DecorationRole:
|
|
if (item->isCategory()) {
|
|
const CategoryItem *cat=static_cast<const CategoryItem *>(item);
|
|
return cat->icon.isNull() ? Icons::self()->streamCategoryIcon : cat->icon;
|
|
} else {
|
|
return Icons::self()->radioStreamIcon;
|
|
}
|
|
case Qt::DisplayRole:
|
|
return item->name;
|
|
case Qt::ToolTipRole:
|
|
return item->isCategory() ? item->name : (item->name+QLatin1String("<br><small><i>")+item->url+QLatin1String("</i></small>"));
|
|
case ItemView::Role_SubText:
|
|
if (item->isCategory()) {
|
|
const CategoryItem *cat=static_cast<const CategoryItem *>(item);
|
|
switch (cat->state) {
|
|
case CategoryItem::Initial:
|
|
return i18n("No Loaded");
|
|
case CategoryItem::Fetching:
|
|
return i18n("Loading...");
|
|
default:
|
|
#ifdef ENABLE_KDE_SUPPORT
|
|
return i18np("1 Entry", "%1 Entries", cat->children.count());
|
|
#else
|
|
return QTP_ENTRIES_STR(cat->children.count());
|
|
#endif
|
|
}
|
|
}
|
|
break;
|
|
case ItemView::Role_Actions:
|
|
if (!item->isCategory()){
|
|
QVariant v;
|
|
v.setValue<QList<Action *> >(QList<Action *>() << StdActions::self()->replacePlayQueueAction);
|
|
return v;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return QVariant();
|
|
}
|
|
|
|
Qt::ItemFlags StreamsModel::flags(const QModelIndex &index) const
|
|
{
|
|
if (index.isValid()) {
|
|
return Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsEnabled;
|
|
} else {
|
|
return Qt::NoItemFlags;
|
|
}
|
|
}
|
|
|
|
bool StreamsModel::hasChildren(const QModelIndex &index) const
|
|
{
|
|
return index.isValid() ? toItem(index)->isCategory() : true;
|
|
}
|
|
|
|
bool StreamsModel::canFetchMore(const QModelIndex &index) const
|
|
{
|
|
if (index.isValid()) {
|
|
Item *item = toItem(index);
|
|
return item->isCategory() && CategoryItem::Initial==static_cast<CategoryItem *>(item)->state && !item->url.isEmpty();
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void StreamsModel::fetchMore(const QModelIndex &index)
|
|
{
|
|
if (!index.isValid()) {
|
|
return;
|
|
}
|
|
|
|
Item *item = toItem(index);
|
|
if (item->isCategory() && !item->url.isEmpty()) {
|
|
CategoryItem *cat=static_cast<CategoryItem *>(item);
|
|
if (item->url==constFavouritesUrl && !getInternalFile().startsWith(QLatin1String("http://"))) {
|
|
cat->state=CategoryItem::Fetching;
|
|
emit dataChanged(index, index);
|
|
loadFavourites(index);
|
|
cat->state=CategoryItem::Fetched;
|
|
} else {
|
|
QNetworkRequest req;
|
|
if (constDiUrls.contains(cat->url)) {
|
|
req=QNetworkRequest(constDiChannelListUrl.arg(cat->url.split(".").at(1)));
|
|
DigitallyImported::self()->addAuthHeader(req);
|
|
} else if (QUrl(item->url).host()==constShoutCastHost) {
|
|
req=QNetworkRequest(cat->url);
|
|
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
|
} else {
|
|
req=QNetworkRequest(cat->url);
|
|
}
|
|
|
|
if (cat==favourites && !favourites->children.isEmpty()) {
|
|
beginRemoveRows(index, 0, favourites->children.count()-1);
|
|
qDeleteAll(favourites->children);
|
|
favourites->children.clear();
|
|
endRemoveRows();
|
|
}
|
|
|
|
QNetworkReply *job=NetworkAccessManager::self()->get(req);
|
|
job->setProperty(constOrigUrlProperty, cat->url);
|
|
if (jobs.isEmpty()) {
|
|
emit loading();
|
|
}
|
|
jobs.insert(job, cat);
|
|
connect(job, SIGNAL(finished()), SLOT(jobFinished()));
|
|
cat->state=CategoryItem::Fetching;
|
|
}
|
|
emit dataChanged(index, index);
|
|
}
|
|
}
|
|
|
|
void StreamsModel::saveFavourites(bool force)
|
|
{
|
|
if (force) {
|
|
if (favouritesSaveTimer) {
|
|
favouritesSaveTimer->stop();
|
|
}
|
|
persistFavourites();
|
|
} else if (!QFile::exists(getInternalFile(false)) || !QFileInfo(getInternalFile(false)).isWritable()) {
|
|
if (favouritesSaveTimer) {
|
|
favouritesSaveTimer->stop();
|
|
}
|
|
persistFavourites(); // Call persist now so as to log errors immediately
|
|
} else {
|
|
if (!favouritesSaveTimer) {
|
|
favouritesSaveTimer=new QTimer(this);
|
|
connect(favouritesSaveTimer, SIGNAL(timeout()), this, SLOT(persistFavourites()));
|
|
}
|
|
favouritesSaveTimer->start(10*1000);
|
|
}
|
|
}
|
|
|
|
bool StreamsModel::checkFavouritesWritable()
|
|
{
|
|
QString dirName=favouritesDir();
|
|
bool isHttp=dirName.startsWith("http:/");
|
|
favouritesIsWriteable=!isHttp && QFileInfo(dirName).isWritable();
|
|
if (favouritesIsWriteable) {
|
|
QString fileName=getInternalFile(false);
|
|
if (QFile::exists(fileName) && !QFileInfo(fileName).isWritable()) {
|
|
favouritesIsWriteable=false;
|
|
}
|
|
}
|
|
return favouritesIsWriteable;
|
|
}
|
|
|
|
void StreamsModel::reloadFavourites()
|
|
{
|
|
fetchMore(createIndex(root->children.indexOf(favourites), 0, favourites));
|
|
}
|
|
|
|
void StreamsModel::removeFromFavourites(const QModelIndex &index)
|
|
{
|
|
Item *item=static_cast<Item *>(index.internalPointer());
|
|
int pos=favourites->children.indexOf(item);
|
|
|
|
if (-1!=pos) {
|
|
QModelIndex index=createIndex(root->children.indexOf(favourites), 0, (void *)favourites);
|
|
beginRemoveRows(index, pos, pos);
|
|
delete favourites->children.takeAt(pos);
|
|
endRemoveRows();
|
|
favouritesModified=true;
|
|
saveFavourites();
|
|
}
|
|
}
|
|
|
|
void StreamsModel::addToFavourites(const QString &url, const QString &name)
|
|
{
|
|
QSet<QString> existingNames;
|
|
|
|
foreach (Item *i, favourites->children) {
|
|
if (i->url==url) {
|
|
return;
|
|
}
|
|
existingNames.insert(i->name);
|
|
}
|
|
|
|
QString n=name;
|
|
int i=1;
|
|
for (; i<100 && existingNames.contains(n); ++i) {
|
|
n=name+QLatin1String("_")+QString::number(i);
|
|
}
|
|
|
|
if (i<100) {
|
|
QModelIndex index=createIndex(root->children.indexOf(favourites), 0, (void *)favourites);
|
|
beginInsertRows(index, favourites->children.count(), favourites->children.count());
|
|
favourites->children.append(new Item(url, n, favourites));
|
|
endInsertRows();
|
|
favouritesModified=true;
|
|
saveFavourites();
|
|
}
|
|
}
|
|
|
|
QString StreamsModel::favouritesNameForUrl(const QString &u)
|
|
{
|
|
foreach (Item *i, favourites->children) {
|
|
if (i->url==u) {
|
|
return i->name;
|
|
}
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
bool StreamsModel::nameExistsInFavourites(const QString &n)
|
|
{
|
|
foreach (Item *i, favourites->children) {
|
|
if (i->name==n) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void StreamsModel::updateFavouriteStream(Item *item)
|
|
{
|
|
int pos=favourites->children.indexOf(item);
|
|
|
|
if (-1==pos) {
|
|
return;
|
|
}
|
|
QModelIndex index=createIndex(favourites->children.indexOf(item), 0, (void *)item);
|
|
favouritesModified=true;
|
|
saveFavourites();
|
|
emit dataChanged(index, index);
|
|
}
|
|
|
|
bool StreamsModel::importXml(const QString &fileName)
|
|
{
|
|
return loadXml(fileName, createIndex(root->children.indexOf(favourites), 0, favourites));
|
|
}
|
|
|
|
bool StreamsModel::saveXml(const QString &fileName, const QList<Item *> &items)
|
|
{
|
|
QFile file(fileName);
|
|
|
|
if (fileName.endsWith(".xml")) {
|
|
return file.open(QIODevice::WriteOnly) && saveXml(&file, items.isEmpty() ? favourites->children : items, true);
|
|
} else {
|
|
QtIOCompressor compressor(&file);
|
|
compressor.setStreamFormat(QtIOCompressor::GzipFormat);
|
|
return compressor.open(QIODevice::WriteOnly) && saveXml(&compressor, items.isEmpty() ? favourites->children : items, false);
|
|
}
|
|
}
|
|
|
|
bool StreamsModel::validProtocol(const QString &file)
|
|
{
|
|
QString scheme=QUrl(file).scheme();
|
|
return scheme.isEmpty() || MPDConnection::self()->urlHandlers().contains(scheme);
|
|
}
|
|
|
|
QString StreamsModel::modifyUrl(const QString &u, bool addPrefix, const QString &name)
|
|
{
|
|
return MPDParseUtils::addStreamName(!addPrefix || !u.startsWith("http:") ? u : (constPrefix+u), name);
|
|
}
|
|
|
|
static QString addDiHash(const StreamsModel::Item *item)
|
|
{
|
|
return item->parent && constDiUrls.contains(item->parent->url)
|
|
? DigitallyImported::self()->modifyUrl(item->url) : item->url;
|
|
}
|
|
|
|
static void filenames(QStringList &fn, bool addPrefix, const StreamsModel::CategoryItem *cat)
|
|
{
|
|
foreach (const StreamsModel::Item *i, cat->children) {
|
|
if (i->isCategory()) {
|
|
::filenames(fn, addPrefix, static_cast<const StreamsModel::CategoryItem *>(i));
|
|
} else if (!fn.contains(i->url) && StreamsModel::validProtocol(i->url)) {
|
|
fn << StreamsModel::modifyUrl(addDiHash(i), addPrefix, i->name);
|
|
}
|
|
}
|
|
}
|
|
|
|
QStringList StreamsModel::filenames(const QModelIndexList &indexes, bool addPrefix) const
|
|
{
|
|
QStringList fnames;
|
|
foreach(QModelIndex index, indexes) {
|
|
Item *item=static_cast<Item *>(index.internalPointer());
|
|
|
|
if (item->isCategory()) {
|
|
::filenames(fnames, addPrefix, static_cast<const StreamsModel::CategoryItem *>(item));
|
|
} else if (!fnames.contains(item->url) && validProtocol(item->url)) {
|
|
fnames << modifyUrl(addDiHash(item), addPrefix, item->name);
|
|
}
|
|
}
|
|
|
|
return fnames;
|
|
}
|
|
|
|
QMimeData * StreamsModel::mimeData(const QModelIndexList &indexes) const
|
|
{
|
|
QMimeData *mimeData = new QMimeData();
|
|
PlayQueueModel::encode(*mimeData, PlayQueueModel::constFileNameMimeType, filenames(indexes, true));
|
|
return mimeData;
|
|
}
|
|
|
|
QStringList StreamsModel::mimeTypes() const
|
|
{
|
|
QStringList types;
|
|
types << PlayQueueModel::constFileNameMimeType;
|
|
return types;
|
|
}
|
|
|
|
void StreamsModel::jobFinished()
|
|
{
|
|
QNetworkReply *job=dynamic_cast<QNetworkReply *>(sender());
|
|
|
|
if (!job) {
|
|
return;
|
|
}
|
|
|
|
job->deleteLater();
|
|
|
|
if (jobs.contains(job)) {
|
|
CategoryItem *cat=jobs[job];
|
|
cat->state=CategoryItem::Fetched;
|
|
jobs.remove(job);
|
|
|
|
QModelIndex index=createIndex(cat->parent->children.indexOf(cat), 0, (void *)cat);
|
|
if (QNetworkReply::NoError==job->error()) {
|
|
QList<Item *> newItems;
|
|
if (cat==favourites) {
|
|
newItems=loadXml(job, true);
|
|
} else if (QLatin1String("http")==job->url().scheme()) {
|
|
QString url=job->url().toString();
|
|
if (constRadioTimeHost==job->url().host()) {
|
|
newItems=parseRadioTimeResponse(job, cat);
|
|
} else if (constIceCastUrl==url) {
|
|
newItems=parseIceCastResponse(job, cat);
|
|
} else if (constSomaFMUrl==url) {
|
|
newItems=parseSomaFmResponse(job, cat);
|
|
} else if (constDiChannelListHost==job->url().host()) {
|
|
newItems=parseDigitallyImportedResponse(job, cat, job->property(constOrigUrlProperty).toString());
|
|
} else if (constShoutCastHost==job->url().host()) {
|
|
newItems=parseShoutCastResponse(job, cat, job->property(constOrigUrlProperty).toString());
|
|
} else {
|
|
newItems=parseListenLiveResponse(job, cat);
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
void StreamsModel::persistFavourites()
|
|
{
|
|
if (favouritesModified) {
|
|
QString fileName=getInternalFile(true);
|
|
favouritesModified=false;
|
|
if (favourites->children.isEmpty()) {
|
|
// No entries, so remove file...
|
|
if (QFile::exists(fileName) && !QFile::remove(fileName)) {
|
|
emit error(i18n("Failed to save stream list. Please check %1 is writable.").arg(fileName));
|
|
reloadFavourites();
|
|
}
|
|
} else if (saveXml(fileName, favourites->children)) {
|
|
Utils::setFilePerms(fileName);
|
|
} else {
|
|
emit error(i18n("Failed to save stream list. Please check %1 is writable.").arg(fileName));
|
|
reloadFavourites();
|
|
}
|
|
}
|
|
}
|
|
|
|
QList<StreamsModel::Item *> StreamsModel::parseRadioTimeResponse(QIODevice *dev, CategoryItem *cat)
|
|
{
|
|
QList<Item *> newItems;
|
|
QXmlStreamReader doc(dev);
|
|
while (!doc.atEnd()) {
|
|
doc.readNext();
|
|
if (doc.isStartElement() && QLatin1String("outline")==doc.name()) {
|
|
Item *item = parseRadioTimeEntry(doc, cat);
|
|
if (item) {
|
|
newItems.append(item);
|
|
}
|
|
}
|
|
}
|
|
return newItems;
|
|
}
|
|
|
|
static QString fixSingleGenre(const QString &g)
|
|
{
|
|
if (!g.isEmpty()) {
|
|
QString genre=Song::capitalize(g);
|
|
genre[0]=genre[0].toUpper();
|
|
genre=genre.trimmed();
|
|
genre=genre.replace(QLatin1String("Afrocaribbean"), QLatin1String("Afro-Caribbean"));
|
|
genre=genre.replace(QLatin1String("Afro Caribbean"), QLatin1String("Afro-Caribbean"));
|
|
if (genre.length() < 3 ||
|
|
QLatin1String("The")==genre || QLatin1String("All")==genre ||
|
|
QLatin1String("Various")==genre || QLatin1String("Unknown")==genre ||
|
|
QLatin1String("Misc")==genre || QLatin1String("Mix")==genre || QLatin1String("100%")==genre ||
|
|
genre.contains("ÃÂ") || // Broken unicode.
|
|
genre.contains(QRegExp("^#x[0-9a-f][0-9a-f]"))) { // Broken XML entities.
|
|
return QString();
|
|
}
|
|
|
|
if (genre==QLatin1String("R&B") || genre==QLatin1String("R B") || genre==QLatin1String("Rnb") || genre==QLatin1String("RnB")) {
|
|
return QLatin1String("R&B");
|
|
}
|
|
if (genre==QLatin1String("Classic") || genre==QLatin1String("Classical")) {
|
|
return QLatin1String("Classical");
|
|
}
|
|
if (genre==QLatin1String("Christian") || genre.startsWith(QLatin1String("Christian "))) {
|
|
return QLatin1String("Christian");
|
|
}
|
|
if (genre==QLatin1String("Rock") || genre.startsWith(QLatin1String("Rock "))) {
|
|
return QLatin1String("Rock");
|
|
}
|
|
if (genre==QLatin1String("Easy") || genre==QLatin1String("Easy Listening")) {
|
|
return QLatin1String("Easy Listening");
|
|
}
|
|
if (genre==QLatin1String("Hit") || genre==QLatin1String("Hits") || genre==QLatin1String("Easy listening")) {
|
|
return QLatin1String("Hits");
|
|
}
|
|
if (genre==QLatin1String("Hip") || genre==QLatin1String("Hiphop") || genre==QLatin1String("Hip Hop") || genre==QLatin1String("Hop Hip")) {
|
|
return QLatin1String("Hip Hop");
|
|
}
|
|
if (genre==QLatin1String("News") || genre==QLatin1String("News talk")) {
|
|
return QLatin1String("News");
|
|
}
|
|
if (genre==QLatin1String("Top40") || genre==QLatin1String("Top 40") || genre==QLatin1String("40Top") || genre==QLatin1String("40 Top")) {
|
|
return QLatin1String("Top 40");
|
|
}
|
|
|
|
QStringList small=QStringList() << QLatin1String("Adult Contemporary") << QLatin1String("Alternative")
|
|
<< QLatin1String("Community Radio") << QLatin1String("Local Service")
|
|
<< QLatin1String("Multiultural") << QLatin1String("News")
|
|
<< QLatin1String("Student") << QLatin1String("Urban");
|
|
|
|
foreach (const QString &s, small) {
|
|
if (genre==s || genre.startsWith(s+" ") || genre.endsWith(" "+s)) {
|
|
return s;
|
|
}
|
|
}
|
|
|
|
// Convert XX's to XXs
|
|
if (genre.contains(QRegExp("^[0-9]0's$"))) {
|
|
genre=genre.remove('\'');
|
|
}
|
|
if (genre.length()>25 && (0==genre.indexOf(QRegExp("^[0-9]0s ")) || 0==genre.indexOf(QRegExp("^[0-9]0 ")))) {
|
|
int pos=genre.indexOf(' ');
|
|
if (pos>1) {
|
|
genre=genre.left(pos);
|
|
}
|
|
}
|
|
// Convert 80 -> 80s.
|
|
return genre.contains(QRegExp("^[0-9]0$")) ? genre + 's' : genre;
|
|
}
|
|
return g;
|
|
}
|
|
|
|
static QStringList fixGenres(const QString &genre)
|
|
{
|
|
QString g(genre);
|
|
int pos=g.indexOf("<br");
|
|
if (pos>3) {
|
|
g=g.left(pos);
|
|
}
|
|
pos=g.indexOf("(");
|
|
if (pos>3) {
|
|
g=g.left(pos);
|
|
}
|
|
|
|
g=Song::capitalize(g);
|
|
QStringList genres=g.split('|', QString::SkipEmptyParts);
|
|
QStringList allGenres;
|
|
|
|
foreach (const QString &genre, genres) {
|
|
allGenres+=genre.split('/', QString::SkipEmptyParts);
|
|
}
|
|
|
|
QStringList fixed;
|
|
foreach (const QString &genre, allGenres) {
|
|
QString g=fixSingleGenre(genre).trimmed();
|
|
if (!g.isEmpty()) {
|
|
fixed.append(g);
|
|
}
|
|
}
|
|
return fixed;
|
|
}
|
|
|
|
static void trimGenres(QMap<QString, QList<StreamsModel::Item *> > &genres)
|
|
{
|
|
QString other=i18n("Other");
|
|
QSet<QString> genreSet = genres.keys().toSet();
|
|
foreach (const QString &genre, genreSet) {
|
|
if (other!=genre && genres[genre].count() < 2) {
|
|
genres[other]+=genres[genre];
|
|
genres.remove(genre);
|
|
}
|
|
}
|
|
}
|
|
|
|
QList<StreamsModel::Item *> StreamsModel::parseIceCastResponse(QIODevice *dev, CategoryItem *cat)
|
|
{
|
|
QList<Item *> newItems;
|
|
QXmlStreamReader doc(dev);
|
|
QSet<QString> names;
|
|
QMap<QString, QList<Item *> > genres;
|
|
while (!doc.atEnd()) {
|
|
doc.readNext();
|
|
if (doc.isStartElement() && QLatin1String("entry")==doc.name()) {
|
|
QString name;
|
|
QString url;
|
|
QStringList stationGenres;
|
|
while (!doc.atEnd()) {
|
|
doc.readNext();
|
|
|
|
if (QXmlStreamReader::StartElement==doc.tokenType()) {
|
|
QStringRef elem = doc.name();
|
|
|
|
if (QLatin1String("server_name")==elem) {
|
|
name=doc.readElementText().trimmed();
|
|
} else if (QLatin1String("listen_url")==elem) {
|
|
url=doc.readElementText().trimmed();
|
|
} else if (QLatin1String("genre")==elem) {
|
|
stationGenres=fixGenres(doc.readElementText().trimmed());
|
|
}
|
|
} else if (doc.isEndElement() && QLatin1String("entry")==doc.name()) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!name.isEmpty() && !url.isEmpty() && !names.contains(name)) {
|
|
names.insert(name);
|
|
if (stationGenres.isEmpty()) {
|
|
stationGenres.append(i18n("Other"));
|
|
}
|
|
foreach (const QString &g, stationGenres) {
|
|
genres[g].append(new Item(url, name, cat));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
trimGenres(genres);
|
|
QMap<QString, QList<Item *> >::ConstIterator it(genres.constBegin());
|
|
QMap<QString, QList<Item *> >::ConstIterator end(genres.constEnd());
|
|
|
|
for (; it!=end; ++it) {
|
|
CategoryItem *genre=new CategoryItem(QString(), it.key(), cat);
|
|
genre->state=CategoryItem::Fetched;
|
|
foreach (Item *i, it.value()) {
|
|
i->parent=genre;
|
|
genre->children.append(i);
|
|
}
|
|
newItems.append(genre);
|
|
}
|
|
|
|
return newItems;
|
|
}
|
|
|
|
QList<StreamsModel::Item *> StreamsModel::parseSomaFmResponse(QIODevice *dev, CategoryItem *cat)
|
|
{
|
|
QList<Item *> newItems;
|
|
QXmlStreamReader doc(dev);
|
|
while (!doc.atEnd()) {
|
|
doc.readNext();
|
|
if (doc.isStartElement() && QLatin1String("channel")==doc.name()) {
|
|
Item *item = parseSomaFmEntry(doc, cat);
|
|
if (item) {
|
|
newItems.append(item);
|
|
}
|
|
}
|
|
}
|
|
return newItems;
|
|
}
|
|
|
|
QList<StreamsModel::Item *> StreamsModel::parseDigitallyImportedResponse(QIODevice *dev, CategoryItem *cat, const QString &origUrl)
|
|
{
|
|
QList<Item *> newItems;
|
|
QJson::Parser parser;
|
|
QVariantMap data = parser.parse(dev).toMap();
|
|
QString listenHost=QLatin1String("listen.")+QUrl(origUrl).host().remove("www.");
|
|
|
|
if (data.contains("channel_filters")) {
|
|
QVariantList filters = data["channel_filters"].toList();
|
|
|
|
foreach (const QVariant &filter, filters) {
|
|
// Find the filter called "All"
|
|
QVariantMap filterMap = filter.toMap();
|
|
if (filterMap.value("name", QString()).toString() != "All") {
|
|
continue;
|
|
}
|
|
|
|
// Add all its stations to the result
|
|
QVariantList channels = filterMap.value("channels", QVariantList()).toList();
|
|
foreach (const QVariant &channel, channels) {
|
|
QVariantMap channelMap = channel.toMap();
|
|
QString url=constDiStdUrl.arg(listenHost).arg(channelMap.value("key").toString());
|
|
newItems.append(new Item(url, channelMap.value("name").toString(), cat));
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return newItems;
|
|
}
|
|
|
|
struct ListenLiveStream {
|
|
enum Format {
|
|
Unknown,
|
|
WMA,
|
|
OGG,
|
|
MP3,
|
|
AAC
|
|
};
|
|
|
|
ListenLiveStream() : format(Unknown), bitrate(0) { }
|
|
bool operator<(const ListenLiveStream &o) const { return weight()>o.weight(); }
|
|
|
|
int weight() const { return ((bitrate&0xff)<<8)+(format&0x0f); }
|
|
|
|
void setFormat(const QString &f) {
|
|
if (QLatin1String("mp3")==f.toLower()) {
|
|
format=MP3;
|
|
} else if (QLatin1String("aacplus")==f.toLower()) {
|
|
format=AAC;
|
|
} else if (QLatin1String("ogg vorbis")==f.toLower()) {
|
|
format=OGG;
|
|
} else if (QLatin1String("windows media")==f.toLower()) {
|
|
format=WMA;
|
|
} else {
|
|
format=Unknown;
|
|
}
|
|
}
|
|
|
|
QString url;
|
|
Format format;
|
|
unsigned int bitrate;
|
|
};
|
|
|
|
struct ListenLiveStationEntry {
|
|
ListenLiveStationEntry() { clear(); }
|
|
void clear() { name=location=comment=QString(); streams.clear(); }
|
|
QString name;
|
|
QString location;
|
|
QString comment;
|
|
QList<ListenLiveStream> streams;
|
|
};
|
|
|
|
static QString getString(QString &str, const QString &start, const QString &end)
|
|
{
|
|
QString rv;
|
|
int b=str.indexOf(start);
|
|
int e=-1==b ? -1 : str.indexOf(end, b+start.length());
|
|
if (-1!=e) {
|
|
rv=str.mid(b+start.length(), e-(b+start.length())).trimmed();
|
|
str=str.mid(e+end.length());
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
QList<StreamsModel::Item *> StreamsModel::parseListenLiveResponse(QIODevice *dev, CategoryItem *cat)
|
|
{
|
|
QList<Item *> newItems;
|
|
QSet<QString> names;
|
|
QMap<QString, QList<Item *> > genres;
|
|
|
|
if (dev) {
|
|
ListenLiveStationEntry entry;
|
|
|
|
while (!dev->atEnd()) {
|
|
QString line=dev->readLine().trimmed().replace("> <", "><").replace("<td><b><a href", "<td><a href")
|
|
.replace("</b></a></b>", "</b></a>").replace("<br />", "<br/>")
|
|
.replace("</a> ,", "</a>,");
|
|
if ("<tr>"==line) {
|
|
entry.clear();
|
|
} else if (line.startsWith("<td><a href=")) {
|
|
if (entry.name.isEmpty()) {
|
|
entry.name=getString(line, "<b>", "</b>");
|
|
QString extra=getString(line, "</a>", "</td>");
|
|
if (!extra.isEmpty()) {
|
|
entry.name+=" "+extra;
|
|
}
|
|
} else {
|
|
// Station URLs...
|
|
QString url;
|
|
QString bitrate;
|
|
int idx=0;
|
|
do {
|
|
url=getString(line, "href=\"", "\"");
|
|
bitrate=getString(line, ">", " Kbps");
|
|
bool sameFormatAsLast=line.startsWith("</a>,");
|
|
if (!url.isEmpty() && !bitrate.isEmpty() && !url.startsWith(QLatin1String("javascript")) && idx<entry.streams.count()) {
|
|
if (sameFormatAsLast && 0!=idx) {
|
|
ListenLiveStream stream;
|
|
stream.format=entry.streams[idx-1].format;
|
|
entry.streams.insert(idx, stream);
|
|
}
|
|
entry.streams[idx].url=url;
|
|
entry.streams[idx].bitrate=bitrate.toUInt();
|
|
idx++;
|
|
}
|
|
} while (!url.isEmpty() && !bitrate.isEmpty());
|
|
}
|
|
} else if (line.startsWith("<td><img src")) {
|
|
// Station formats...
|
|
QString format;
|
|
do {
|
|
format=getString(line, "alt=\"", "\"");
|
|
if (!format.isEmpty()) {
|
|
ListenLiveStream stream;
|
|
stream.setFormat(format);
|
|
entry.streams.append(stream);
|
|
}
|
|
} while (!format.isEmpty());
|
|
} else if (line.startsWith("<td>")) {
|
|
if (entry.location.isEmpty()) {
|
|
entry.location=getString(line, "<td>", "</td>");
|
|
} else {
|
|
entry.comment=getString(line, "<td>", "</td>");
|
|
}
|
|
} else if ("</tr>"==line) {
|
|
if (entry.streams.count()) {
|
|
qSort(entry.streams);
|
|
QString name;
|
|
QString url=entry.streams.at(0).url;
|
|
|
|
if (QLatin1String("National")==entry.location || entry.name.endsWith("("+entry.location+")")) {
|
|
name=entry.name;
|
|
} else if (entry.name.endsWith(")")) {
|
|
name=entry.name.left(entry.name.length()-1)+", "+entry.location+")";
|
|
} else {
|
|
name=entry.name+" ("+entry.location+")";
|
|
}
|
|
|
|
if (!names.contains(name) && !name.isEmpty() && !url.isEmpty()) {
|
|
QStringList stationGenres=fixGenres(entry.comment);
|
|
if (stationGenres.isEmpty()) {
|
|
stationGenres.append(i18n("Other"));
|
|
}
|
|
foreach (const QString &g, stationGenres) {
|
|
genres[g].append(new Item(url, name, cat));
|
|
}
|
|
names.insert(name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
QMap<QString, QList<Item *> >::ConstIterator it(genres.constBegin());
|
|
QMap<QString, QList<Item *> >::ConstIterator end(genres.constEnd());
|
|
CategoryItem *all=0;
|
|
|
|
if (!genres.isEmpty()) {
|
|
all=new CategoryItem(QString(), i18n("All"), cat);
|
|
all->state=CategoryItem::Fetched;
|
|
all->isAll=true;
|
|
newItems.append(all);
|
|
}
|
|
for (; it!=end; ++it) {
|
|
CategoryItem *genre=new CategoryItem(QString(), it.key(), cat);
|
|
genre->state=CategoryItem::Fetched;
|
|
foreach (Item *i, it.value()) {
|
|
i->parent=genre;
|
|
genre->children.append(i);
|
|
all->children.append(new Item(i->url, i->name, all));
|
|
}
|
|
newItems.append(genre);
|
|
}
|
|
|
|
return newItems;
|
|
}
|
|
|
|
QList<StreamsModel::Item *> StreamsModel::parseShoutCastResponse(QIODevice *dev, CategoryItem *cat, const QString &origUrl)
|
|
{
|
|
bool isRoot=origUrl==constShoutCastUrl;
|
|
bool wasLinks=false;
|
|
QList<Item *> newItems;
|
|
QXmlStreamReader doc(dev);
|
|
while (!doc.atEnd()) {
|
|
doc.readNext();
|
|
if (doc.isStartElement() && QLatin1String("genrelist")==doc.name()) {
|
|
wasLinks=true;
|
|
newItems+=parseShoutCastLinks(doc, cat);
|
|
} else if (doc.isStartElement() && QLatin1String("stationlist")==doc.name()) {
|
|
newItems+=parseShoutCastStations(doc, cat);
|
|
}
|
|
}
|
|
|
|
if (!isRoot && wasLinks) {
|
|
// Get stations...
|
|
QUrl url(QLatin1String("http://")+constShoutCastHost+QLatin1String("/legacy/genresearch"));
|
|
#if QT_VERSION < 0x050000
|
|
QUrl &query=url;
|
|
#else
|
|
QUrlQuery query;
|
|
#endif
|
|
query.addQueryItem("k", constShoutCastApiKey);
|
|
query.addQueryItem("genre", cat->name);
|
|
#if QT_VERSION >= 0x050000
|
|
url.setQuery(query);
|
|
#endif
|
|
|
|
QNetworkRequest req(url);
|
|
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
|
QNetworkReply *job=NetworkAccessManager::self()->get(req);
|
|
job->setProperty(constOrigUrlProperty, url.toString());
|
|
if (jobs.isEmpty()) {
|
|
emit loading();
|
|
}
|
|
jobs.insert(job, cat);
|
|
connect(job, SIGNAL(finished()), SLOT(jobFinished()));
|
|
}
|
|
return newItems;
|
|
}
|
|
|
|
QList<StreamsModel::Item *> StreamsModel::parseShoutCastLinks(QXmlStreamReader &doc, CategoryItem *cat)
|
|
{
|
|
QList<Item *> newItems;
|
|
while (!doc.atEnd()) {
|
|
doc.readNext();
|
|
if (doc.isStartElement() && QLatin1String("genre")==doc.name()) {
|
|
newItems.append(new CategoryItem(QLatin1String("http://")+constShoutCastHost+QLatin1String("/genre/secondary?parentid=")+
|
|
doc.attributes().value("id").toString()+QLatin1String("&f=xml&k=")+constShoutCastApiKey,
|
|
doc.attributes().value("name").toString(), cat));
|
|
} else if (doc.isEndElement() && QLatin1String("genrelist")==doc.name()) {
|
|
return newItems;
|
|
}
|
|
}
|
|
|
|
return newItems;
|
|
}
|
|
|
|
QList<StreamsModel::Item *> StreamsModel::parseShoutCastStations(QXmlStreamReader &doc, CategoryItem *cat)
|
|
{
|
|
QList<Item *> newItems;
|
|
while (!doc.atEnd()) {
|
|
doc.readNext();
|
|
if (doc.isStartElement() && QLatin1String("station")==doc.name()) {
|
|
newItems.append(new Item(QLatin1String("http://yp.shoutcast.com/sbin/tunein-station.pls?id=")+
|
|
doc.attributes().value("id").toString(),
|
|
doc.attributes().value("name").toString(), cat));
|
|
} else if (doc.isEndElement() && QLatin1String("stationlist")==doc.name()) {
|
|
return newItems;
|
|
}
|
|
}
|
|
|
|
return newItems;
|
|
}
|
|
|
|
StreamsModel::Item * StreamsModel::parseRadioTimeEntry(QXmlStreamReader &doc, CategoryItem *parent)
|
|
{
|
|
Item *item=0;
|
|
CategoryItem *cat=0;
|
|
while (!doc.atEnd()) {
|
|
if (doc.isStartElement()) {
|
|
QString text=doc.attributes().value("text").toString();
|
|
if (!text.isEmpty()) {
|
|
QString url=doc.attributes().value("URL").toString();
|
|
bool isStation=QLatin1String("audio")==doc.attributes().value("type").toString();
|
|
if (isStation) {
|
|
item=new Item(url, text, parent);
|
|
} else {
|
|
item=cat=new CategoryItem(url, text, parent);
|
|
}
|
|
}
|
|
}
|
|
|
|
doc.readNext();
|
|
if (doc.isStartElement() && QLatin1String("outline")==doc.name()) {
|
|
Item *child = parseRadioTimeEntry(doc, cat);
|
|
if (child) {
|
|
if (cat) {
|
|
cat->state=CategoryItem::Fetched;
|
|
cat->children.append(child);
|
|
} else {
|
|
delete child;
|
|
}
|
|
}
|
|
} else if (doc.isEndElement() && QLatin1String("outline")==doc.name()) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
StreamsModel::Item * StreamsModel::parseSomaFmEntry(QXmlStreamReader &doc, CategoryItem *parent)
|
|
{
|
|
QString name;
|
|
QString url;
|
|
QString streamFormat;
|
|
|
|
while (!doc.atEnd()) {
|
|
doc.readNext();
|
|
|
|
if (QXmlStreamReader::StartElement==doc.tokenType()) {
|
|
QStringRef elem = doc.name();
|
|
|
|
if (QLatin1String("title")==elem) {
|
|
name=doc.readElementText().trimmed();
|
|
} else if (QLatin1String("fastpls")==elem) {
|
|
if (streamFormat.isEmpty() || QLatin1String("mp3")!=streamFormat) {
|
|
streamFormat=doc.attributes().value("format").toString();
|
|
url=doc.readElementText();
|
|
}
|
|
}
|
|
} else if (doc.isEndElement() && QLatin1String("channel")==doc.name()) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return name.isEmpty() || url.isEmpty() ? 0 : new Item(url, name, parent);
|
|
}
|
|
|
|
void StreamsModel::loadFavourites(const QModelIndex &index)
|
|
{
|
|
if (!favourites->children.isEmpty()) {
|
|
beginRemoveRows(index, 0, favourites->children.count()-1);
|
|
qDeleteAll(favourites->children);
|
|
favourites->children.clear();
|
|
endRemoveRows();
|
|
}
|
|
loadXml(getInternalFile(false), index);
|
|
}
|
|
|
|
bool StreamsModel::loadXml(const QString &fileName, const QModelIndex &index)
|
|
{
|
|
QFile file(fileName);
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
|
return false;
|
|
}
|
|
// Check for gzip header...
|
|
QByteArray header=file.read(2);
|
|
bool isCompressed=((unsigned char)header[0])==0x1f && ((unsigned char)header[1])==0x8b;
|
|
file.seek(0);
|
|
|
|
QtIOCompressor compressor(&file);
|
|
if (isCompressed) {
|
|
compressor.setStreamFormat(QtIOCompressor::GzipFormat);
|
|
if (!compressor.open(QIODevice::ReadOnly)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
QList<Item *> newItems=loadXml(isCompressed ? (QIODevice *)&compressor : (QIODevice *)&file, true);
|
|
|
|
if (!newItems.isEmpty()) {
|
|
beginInsertRows(index, favourites->children.count(), (favourites->children.count()+newItems.count())-1);
|
|
favourites->children+=newItems;
|
|
endInsertRows();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
QList<StreamsModel::Item *> StreamsModel::loadXml(QIODevice *dev, bool isInternal)
|
|
{
|
|
QList<Item *> newItems;
|
|
QXmlStreamReader doc(dev);
|
|
QSet<QString> existingUrls;
|
|
QSet<QString> existingNames;
|
|
|
|
if (!isInternal) {
|
|
foreach (Item *i, favourites->children) {
|
|
existingUrls.insert(i->url);
|
|
existingNames.insert(i->name);
|
|
}
|
|
}
|
|
|
|
while (!doc.atEnd()) {
|
|
doc.readNext();
|
|
|
|
if (doc.isStartElement() && QLatin1String("stream")==doc.name()) {
|
|
QString name=doc.attributes().value("name").toString();
|
|
QString origName=name;
|
|
QString url=doc.attributes().value("url").toString();
|
|
|
|
if (!name.isEmpty() && !name.isEmpty() && (isInternal || !existingUrls.contains(url))) {
|
|
int i=1;
|
|
if (!isInternal) {
|
|
for (; i<100 && existingNames.contains(name); ++i) {
|
|
name=origName+QLatin1String("_")+QString::number(i);
|
|
}
|
|
}
|
|
|
|
if (i<100) {
|
|
existingNames.insert(name);
|
|
existingUrls.insert(url);
|
|
newItems.append(new Item(url, name, favourites));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return newItems;
|
|
}
|
|
|
|
bool StreamsModel::saveXml(QIODevice *dev, const QList<Item *> &items, bool format) const
|
|
{
|
|
QXmlStreamWriter doc(dev);
|
|
doc.writeStartDocument();
|
|
doc.writeStartElement("streams");
|
|
doc.writeAttribute("version", "1.0");
|
|
if (format) {
|
|
doc.setAutoFormatting(true);
|
|
doc.setAutoFormattingIndent(1);
|
|
} else {
|
|
doc.setAutoFormatting(false);
|
|
}
|
|
|
|
foreach (const Item *i, items) {
|
|
doc.writeStartElement("stream");
|
|
doc.writeAttribute("name", i->name);
|
|
doc.writeAttribute("url", i->url);
|
|
doc.writeEndElement();
|
|
}
|
|
doc.writeEndElement();
|
|
doc.writeEndDocument();
|
|
return true;
|
|
}
|
|
|
|
void StreamsModel::buildListenLive()
|
|
{
|
|
QFile f(":listenlive.xml");
|
|
if (f.open(QIODevice::ReadOnly)) {
|
|
CategoryItem *listen=new CategoryItem(QString(), i18n("Listen Live"), root, getIcon("listenlive"));
|
|
CategoryItem *region=listen;
|
|
CategoryItem *prevRegion=listen;
|
|
listen->state=CategoryItem::Fetched;
|
|
root->children.append(listen);
|
|
QXmlStreamReader doc(&f);
|
|
while (!doc.atEnd()) {
|
|
doc.readNext();
|
|
if (doc.isStartElement()) {
|
|
if (QLatin1String("listing")==doc.name()) {
|
|
region->children.append(new CategoryItem(doc.attributes().value("url").toString(),
|
|
doc.attributes().value("name").toString(),
|
|
region));
|
|
} else if (QLatin1String("region")==doc.name()) {
|
|
prevRegion=region;
|
|
region=new CategoryItem(QString(), doc.attributes().value("name").toString(), prevRegion);
|
|
region->state=CategoryItem::Fetched;
|
|
prevRegion->children.append(region);
|
|
}
|
|
} else if (QLatin1String("region")==doc.name()) {
|
|
region=prevRegion;
|
|
}
|
|
}
|
|
}
|
|
}
|