/* * Cantata * * Copyright (c) 2011-2017 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 "streamfetcher.h" #include "network/networkaccessmanager.h" #include "mpd-interface/mpdconnection.h" #include "mpd-interface/mpdparseutils.h" #include "models/streamsmodel.h" #include #include #include #ifdef _MSC_VER #define strncasecmp _strnicmp #define strcasecmp _stricmp #endif #include static bool debugEnabled=false; #define DBUG if (debugEnabled) qWarning() << "StreamFetcher" << __FUNCTION__ void StreamFetcher::enableDebug() { debugEnabled=true; } static const int constMaxRedirects = 3; static const int constMaxData = 1024; static const int constTimeout = 3*1000; static QString parsePlaylist(const QByteArray &data, const QString &key, const QSet &handlers) { QStringList lines=QString(data).split('\n', QString::SkipEmptyParts); foreach (QString line, lines) { if (line.startsWith(key, Qt::CaseInsensitive)) { foreach (const QString &handler, handlers) { QString protocol(handler+QLatin1String("://")); int index=line.indexOf(protocol, Qt::CaseInsensitive); if (index>-1 && index<7) { line.remove('\n'); line.remove('\r'); return line.mid(index); } } } } return QString(); } static QString parseExt3Mu(const QByteArray &data, const QSet &handlers) { QStringList lines=QString(data).split(QRegExp(QLatin1String("(\r\n|\n|\r)")), QString::SkipEmptyParts); foreach (QString line, lines) { foreach (const QString &handler, handlers) { QString protocol(handler+QLatin1String("://")); if (line.startsWith(protocol, Qt::CaseInsensitive)) { line.remove('\n'); line.remove('\r'); return line; } } } return QString(); } static QString parseAsx(const QByteArray &data, const QSet &handlers) { QStringList lines=QString(data).split(QRegExp(QLatin1String("(\r\n|\n|\r|/>)")), QString::SkipEmptyParts); foreach (QString line, lines) { int ref=line.indexOf(QLatin1String(" &handlers) { // XSPF / SPIFF QXmlStreamReader reader(data); while (!reader.atEnd()) { reader.readNext(); if (QXmlStreamReader::StartElement==reader.tokenType() && QLatin1String("location")==reader.name()) { QString loc=reader.readElementText().trimmed(); foreach (const QString &handler, handlers) { if (loc.startsWith(handler+QLatin1String("://"))) { return loc; } } } } return QString(); } static QString parse(const QByteArray &data) { QSet handlers=MPDConnection::self()->urlHandlers(); if (data.length()>10 && !strncasecmp(data.constData(), "[playlist]", 10)) { DBUG << "playlist"; return parsePlaylist(data, QLatin1String("File"), handlers); } else if (data.length()>7 && (!strncasecmp(data.constData(), "#EXTM3U", 7) || !strncasecmp(data.constData(), "http://", 7))) { DBUG << "ext3mu"; return parseExt3Mu(data, handlers); } else if (data.length()>5 && !strncasecmp(data.constData(), "11 && !strncasecmp(data.constData(), "[reference]", 11)) { DBUG << "playlist/ref"; return parsePlaylist(data, QLatin1String("Ref"), handlers); } else if (data.length()>5 && !strncasecmp(data.constData(), "get(u, constTimeout); DBUG << "Check" << u.toString(); connect(job, SIGNAL(readyRead()), this, SLOT(dataReady())); connect(job, SIGNAL(finished()), this, SLOT(jobFinished())); return; } else { DBUG << "use orig" << current; done.append(MPDParseUtils::addStreamName(current, currentName)); } } if (todo.isEmpty() && !done.isEmpty()) { job=0; emit result(done, row, playQueueAction, prio, decreasePriority); emit status(QString()); } } void StreamFetcher::cancel() { todo.clear(); done.clear(); row=0; data.clear(); current=QString(); cancelJob(); emit status(QString()); } void StreamFetcher::dataReady() { NetworkJob *reply=qobject_cast(sender()); if (reply!=job) { return; } data+=job->readAll(); if (data.count()>constMaxData) { NetworkJob *thisJob=job; jobFinished(thisJob); // If jobFinished did not redirect, then we need to ensure job is cancelled. if (thisJob==job) { cancelJob(); } } } void StreamFetcher::jobFinished() { NetworkJob *reply=qobject_cast(sender()); if (reply) { jobFinished(reply); } } void StreamFetcher::jobFinished(NetworkJob *reply) { // We only handle 1 job at a time! if (reply==job) { bool redirected=false; if (!reply->error()) { QString u=parse(data); if (u.isEmpty() || u==current) { DBUG << "use (empty/current)" << current; done.append(MPDParseUtils::addStreamName(current.startsWith(StreamsModel::constPrefix) ? current.mid(StreamsModel::constPrefix.length()) : current, currentName)); } else if (u.startsWith(QLatin1String("http://")) && ++redirectsget(u, constTimeout); connect(job, SIGNAL(readyRead()), this, SLOT(dataReady())); connect(job, SIGNAL(finished()), this, SLOT(jobFinished())); redirected=true; } else { DBUG << "use" << u; done.append(MPDParseUtils::addStreamName(u, currentName)); } } else { DBUG << "error " << reply->errorString() << " - use" << current; done.append(MPDParseUtils::addStreamName(current.startsWith(StreamsModel::constPrefix) ? current.mid(StreamsModel::constPrefix.length()) : current, currentName)); } if (!redirected) { doNext(); } } reply->deleteLater(); } void StreamFetcher::cancelJob() { if (job) { disconnect(job, SIGNAL(readyRead()), this, SLOT(dataReady())); disconnect(job, SIGNAL(finished()), this, SLOT(jobFinished())); job->cancelAndDelete(); job=0; } }