-
Sze Howe Koh authored
qmultimedia.h is included in more places, but qmediametadata.h is larger. This patch should reduce unnecessary #include-ing. Change-Id: I4a3d174bafc555d794bb75087c1f6b79745ae903 Reviewed-by:
Yoann Lopes <yoann.lopes@digia.com>
f4348fe3
/****************************************************************************
**
** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
**
** This file is part of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia. For licensing terms and
** conditions see http://qt.digia.com/licensing. For further information
** use the contact form at http://qt.digia.com/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights. These rights are described in the Digia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3.0 as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL included in the
** packaging of this file. Please review the following information to
** ensure the GNU General Public License version 3.0 requirements will be
** met: http://www.gnu.org/copyleft/gpl.html.
**
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "playlistfileparser_p.h"
#include <qfileinfo.h>
#include <QtNetwork/QNetworkReply>
#include "qmediaobject_p.h"
#include "qmediametadata.h"
QT_BEGIN_NAMESPACE
namespace {
class ParserBase : public QObject
{
Q_OBJECT
public:
ParserBase(QObject *parent)
: QObject(parent)
{
}
virtual void parseLine(int lineIndex, const QString& line, const QUrl& root) = 0;
Q_SIGNALS:
void newItem(const QVariant& content);
void finished();
void error(QPlaylistFileParser::ParserError err, const QString& errorMsg);
};
class M3UParser : public ParserBase
{
public:
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
M3UParser(QObject *parent)
: ParserBase(parent)
, m_extendedFormat(false)
{
}
/*
*
Extended M3U directives
#EXTM3U - header - must be first line of file
#EXTINF - extra info - length (seconds), title
#EXTINF - extra info - length (seconds), artist '-' title
Example
#EXTM3U
#EXTINF:123, Sample artist - Sample title
C:\Documents and Settings\I\My Music\Sample.mp3
#EXTINF:321,Example Artist - Example title
C:\Documents and Settings\I\My Music\Greatest Hits\Example.ogg
*/
void parseLine(int lineIndex, const QString& line, const QUrl& root)
{
if (line[0] == '#' ) {
if (m_extendedFormat) {
if (line.startsWith(QLatin1String("#EXTINF:"))) {
m_extraInfo.clear();
int artistStart = line.indexOf(QLatin1String(","), 8);
bool ok = false;
int length = line.mid(8, artistStart < 8 ? -1 : artistStart - 8).trimmed().toInt(&ok);
if (ok && length > 0) {
//convert from second to milisecond
m_extraInfo[QMediaMetaData::Duration] = QVariant(length * 1000);
}
if (artistStart > 0) {
int titleStart = getSplitIndex(line, artistStart);
if (titleStart > artistStart) {
m_extraInfo[QMediaMetaData::Author] = line.mid(artistStart + 1,
titleStart - artistStart - 1).trimmed().
replace(QLatin1String("--"), QLatin1String("-"));
m_extraInfo[QMediaMetaData::Title] = line.mid(titleStart + 1).trimmed().
replace(QLatin1String("--"), QLatin1String("-"));
} else {
m_extraInfo[QMediaMetaData::Title] = line.mid(artistStart + 1).trimmed().
replace(QLatin1String("--"), QLatin1String("-"));
}
}
}
} else if (lineIndex == 0 && line.startsWith(QLatin1String("#EXTM3U"))) {
m_extendedFormat = true;
}
} else {
m_extraInfo[QLatin1String("url")] = expandToFullPath(root, line);
emit newItem(QVariant(m_extraInfo));
m_extraInfo.clear();
}
}
int getSplitIndex(const QString& line, int startPos)
{
if (startPos < 0)
startPos = 0;
const QChar* buf = line.data();
for (int i = startPos; i < line.length(); ++i) {
if (buf[i] == '-') {
if (i == line.length() - 1)
return i;
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
++i;
if (buf[i] != '-')
return i - 1;
}
}
return -1;
}
QUrl expandToFullPath(const QUrl& root, const QString& line)
{
// On Linux, backslashes are not converted to forward slashes :/
if (line.startsWith(QLatin1String("//")) || line.startsWith(QLatin1String("\\\\"))) {
// Network share paths are not resolved
return QUrl::fromLocalFile(line);
}
QUrl url(line);
if (url.scheme().isEmpty()) {
// Resolve it relative to root
if (root.isLocalFile())
return root.resolved(QUrl::fromLocalFile(line));
else
return root.resolved(url);
} else if (url.scheme().length() == 1) {
// Assume it's a drive letter for a Windows path
url = QUrl::fromLocalFile(line);
}
return url;
}
private:
bool m_extendedFormat;
QVariantMap m_extraInfo;
};
class PLSParser : public ParserBase
{
public:
PLSParser(QObject *parent)
: ParserBase(parent)
, m_state(Header)
, m_count(0)
, m_readFlags(0)
{
}
enum ReadFlags
{
FileRead = 0x1,
TitleRead = 0x2,
LengthRead = 0x4,
All = FileRead | TitleRead | LengthRead
};
enum State
{
Header,
Track,
Footer
};
/*
*
The format is essentially that of an INI file structured as follows:
Header
* [playlist] : This tag indicates that it is a Playlist File
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
Track Entry
Assuming track entry #X
* FileX : Variable defining location of stream.
* TitleX : Defines track title.
* LengthX : Length in seconds of track. Value of -1 indicates indefinite.
Footer
* NumberOfEntries : This variable indicates the number of tracks.
* Version : Playlist version. Currently only a value of 2 is valid.
[playlist]
File1=Alternative\everclear - SMFTA.mp3
Title1=Everclear - So Much For The Afterglow
Length1=233
File2=http://www.site.com:8000/listen.pls
Title2=My Cool Stream
Length5=-1
NumberOfEntries=2
Version=2
*/
inline bool containsFlag(const ReadFlags& flag)
{
return (m_readFlags & int(flag)) == flag;
}
inline void setFlag(const ReadFlags& flag)
{
m_readFlags |= int(flag);
}
void parseLine(int lineIndex, const QString& line, const QUrl&)
{
switch (m_state) {
case Header:
if (line == QLatin1String("[playlist]")) {
m_state = Track;
setCount(1);
}
break;
case Track:
if (!containsFlag(FileRead) && line.startsWith(m_fileName)) {
m_item[QLatin1String("url")] = getValue(lineIndex, line);
setFlag(FileRead);
} else if (!containsFlag(TitleRead) && line.startsWith(m_titleName)) {
m_item[QMediaMetaData::Title] = getValue(lineIndex, line);
setFlag(TitleRead);
} else if (!containsFlag(LengthRead) && line.startsWith(m_lengthName)) {
//convert from seconds to miliseconds
int length = getValue(lineIndex, line).toInt();
if (length > 0)
m_item[QMediaMetaData::Duration] = length * 1000;
setFlag(LengthRead);
} else if (line.startsWith(QLatin1String("NumberOfEntries"))) {
m_state = Footer;
int entries = getValue(lineIndex, line).toInt();
int count = m_readFlags == 0 ? (m_count - 1) : m_count;
if (entries != count) {
emit error(QPlaylistFileParser::FormatError, QString(tr("Error parsing playlist: %1, expected count = %2")).
arg(line, QString::number(count)));
}
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
break;
}
if (m_readFlags == int(All)) {
emit newItem(m_item);
setCount(m_count + 1);
}
break;
case Footer:
if (line.startsWith(QLatin1String("Version"))) {
int version = getValue(lineIndex, line).toInt();
if (version != 2)
emit error(QPlaylistFileParser::FormatError, QString(tr("Error parsing playlist at line[%1], expected version = 2")).arg(line));
}
break;
}
}
QString getValue(int lineIndex, const QString& line) {
int start = line.indexOf('=');
if (start < 0) {
emit error(QPlaylistFileParser::FormatError, QString(tr("Error parsing playlist at line[%1]:%2")).arg(QString::number(lineIndex), line));
return QString();
}
return line.mid(start + 1).trimmed();
}
void setCount(int count) {
m_count = count;
m_fileName = QString(tr("File%1")).arg(count);
m_titleName = QString(tr("Title%1")).arg(count);
m_lengthName = QString(tr("Length%1")).arg(count);
m_item.clear();
m_readFlags = 0;
}
private:
State m_state;
int m_count;
QString m_titleName;
QString m_fileName;
QString m_lengthName;
QVariantMap m_item;
int m_readFlags;
};
}
/////////////////////////////////////////////////////////////////////////////////////////////////
class QPlaylistFileParserPrivate : public QObject
{
Q_OBJECT
Q_DECLARE_NON_CONST_PUBLIC(QPlaylistFileParser)
public:
QPlaylistFileParserPrivate()
: m_source(0)
, m_scanIndex(0)
, m_utf8(false)
, m_lineIndex(-1)
, m_type(QPlaylistFileParser::UNKNOWN)
, m_currentParser(0)
{
}
void _q_handleData();
void _q_handleError();
void _q_handleParserError(QPlaylistFileParser::ParserError err, const QString& errorMsg);
void _q_handleParserFinished();
QNetworkReply *m_source;
QByteArray m_buffer;
351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
int m_scanIndex;
QUrl m_root;
bool m_utf8;
int m_lineIndex;
QPlaylistFileParser::FileType m_type;
ParserBase *m_currentParser;
QNetworkAccessManager m_mgr;
QPlaylistFileParser *q_ptr;
private:
void processLine(int startIndex, int length);
};
#define LINE_LIMIT 4096
#define READ_LIMIT 64
void QPlaylistFileParserPrivate::processLine(int startIndex, int length)
{
Q_Q(QPlaylistFileParser);
m_lineIndex++;
if (!m_currentParser) {
Q_ASSERT(!m_currentParser);
QString mimeType = m_source->header(QNetworkRequest::ContentTypeHeader).toString();
m_type = QPlaylistFileParser::findPlaylistType(m_root.toString(), mimeType, m_buffer.constData(), m_buffer.size());
switch (m_type) {
case QPlaylistFileParser::UNKNOWN:
emit q->error(QPlaylistFileParser::FormatError, QString(tr("%1 playlist type is unknown")).arg(m_root.toString()));
q->stop();
return;
case QPlaylistFileParser::M3U:
m_currentParser = new M3UParser(this);
break;
case QPlaylistFileParser::M3U8:
m_currentParser = new M3UParser(this);
m_utf8 = true;
break;
case QPlaylistFileParser::PLS:
m_currentParser = new PLSParser(this);
break;
}
Q_ASSERT(m_currentParser);
connect(m_currentParser, SIGNAL(newItem(QVariant)), q, SIGNAL(newItem(QVariant)));
connect(m_currentParser, SIGNAL(finished()), q, SLOT(_q_handleParserFinished()));
connect(m_currentParser, SIGNAL(error(QPlaylistFileParser::ParserError, QString)),
q, SLOT(_q_handleParserError(QPlaylistFileParser::ParserError, QString)));
}
QString line;
if (m_utf8) {
line = QString::fromUtf8(m_buffer.constData() + startIndex, length).trimmed();
} else {
line = QString::fromLatin1(m_buffer.constData() + startIndex, length).trimmed();
}
if (line.isEmpty())
return;
Q_ASSERT(m_currentParser);
m_currentParser->parseLine(m_lineIndex, line, m_root);
}
void QPlaylistFileParserPrivate::_q_handleData()
{
Q_Q(QPlaylistFileParser);
while (m_source->bytesAvailable()) {
int expectedBytes = qMin(READ_LIMIT, int(qMin(m_source->bytesAvailable(),
421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
qint64(LINE_LIMIT - m_buffer.size()))));
m_buffer.push_back(m_source->read(expectedBytes));
int processedBytes = 0;
while (m_scanIndex < m_buffer.length()) {
char s = m_buffer[m_scanIndex];
if (s == '\r' || s == '\n') {
int l = m_scanIndex - processedBytes;
if (l > 0)
processLine(processedBytes, l);
processedBytes = m_scanIndex + 1;
if (!m_source) {
//some error happened, so exit parsing
return;
}
}
m_scanIndex++;
}
if (m_buffer.length() - processedBytes >= LINE_LIMIT) {
qWarning() << "error parsing playlist["<< m_root << "] with line content >= 4096 bytes.";
emit q->error(QPlaylistFileParser::FormatError, tr("invalid line in playlist file"));
q->stop();
return;
}
if (m_source->isFinished() && !m_source->bytesAvailable()) {
//last line
processLine(processedBytes, -1);
break;
}
Q_ASSERT(m_buffer.length() == m_scanIndex);
if (processedBytes == 0)
continue;
int copyLength = m_buffer.length() - processedBytes;
if (copyLength > 0) {
Q_ASSERT(copyLength <= READ_LIMIT);
m_buffer = m_buffer.right(copyLength);
} else {
m_buffer.clear();
}
m_scanIndex = 0;
}
if (m_source->isFinished()) {
_q_handleParserFinished();
}
}
void QPlaylistFileParserPrivate::_q_handleError()
{
Q_Q(QPlaylistFileParser);
emit q->error(QPlaylistFileParser::NetworkError, m_source->errorString());
q->stop();
}
void QPlaylistFileParserPrivate::_q_handleParserError(QPlaylistFileParser::ParserError err, const QString& errorMsg)
{
Q_Q(QPlaylistFileParser);
emit q->error(err, errorMsg);
}
void QPlaylistFileParserPrivate::_q_handleParserFinished()
{
Q_Q(QPlaylistFileParser);
bool isParserValid = (m_currentParser != 0);
if (!isParserValid)
emit q->error(QPlaylistFileParser::FormatNotSupportedError, tr("Empty file provided"));
491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
q->stop();
if (isParserValid)
emit q->finished();
}
QPlaylistFileParser::QPlaylistFileParser(QObject *parent)
:QObject(parent), d_ptr(new QPlaylistFileParserPrivate)
{
d_func()->q_ptr = this;
}
QPlaylistFileParser::FileType QPlaylistFileParser::findPlaylistType(const QString& uri, const QString& mime, const void *data, quint32 size)
{
if (!data || !size)
return UNKNOWN;
FileType uriType = UNKNOWN;
QString suffix = QFileInfo(uri).suffix().toLower();
if (suffix == QLatin1String("m3u"))
uriType = M3U;
else if (suffix == QLatin1String("m3u8"))
uriType = M3U8;
else if (suffix == QLatin1String("pls"))
uriType = PLS;
FileType mimeType = UNKNOWN;
if (mime == QLatin1String("text/uri-list") || mime == QLatin1String("audio/x-mpegurl") || mime == QLatin1String("audio/mpegurl"))
mimeType = QPlaylistFileParser::M3U;
else if (mime == QLatin1String("application/x-mpegURL") || mime == QLatin1String("application/vnd.apple.mpegurl"))
mimeType = QPlaylistFileParser::M3U8;
else if (mime == QLatin1String("audio/x-scpls"))
mimeType = QPlaylistFileParser::PLS;
FileType bufferType = UNKNOWN;
if (size >= 7 && strncmp((const char*)data, "#EXTM3U", 7) == 0)
bufferType = M3U;
else if (size >= 10 && strncmp((const char*)data, "[playlist]", 10) == 0)
bufferType = PLS;
else {
// Make sure every line is a text string
quint32 n;
for (n = 0; n < size; n++)
if (!QChar(QLatin1Char(((const char*)data)[n])).isPrint())
break;
if (n == size)
bufferType = M3U;
}
if (bufferType == M3U && (uriType == M3U8 || mimeType == M3U8))
bufferType = M3U8;
if (bufferType != UNKNOWN)
return bufferType;
if (uriType != UNKNOWN)
return uriType;
if (mimeType != UNKNOWN)
return mimeType;
return UNKNOWN;
}
void QPlaylistFileParser::start(const QNetworkRequest& request, bool utf8)
{
Q_D(QPlaylistFileParser);
stop();
561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
d->m_type = UNKNOWN;
d->m_utf8 = utf8;
d->m_root = request.url();
if (d->m_root.isLocalFile() && !QFile::exists(d->m_root.toLocalFile())) {
emit error(NetworkError, QString(tr("%1 does not exist")).arg(d->m_root.toString()));
return;
}
d->m_source = d->m_mgr.get(request);
connect(d->m_source, SIGNAL(readyRead()), this, SLOT(_q_handleData()));
connect(d->m_source, SIGNAL(finished()), this, SLOT(_q_handleData()));
connect(d->m_source, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(_q_handleError()));
d->_q_handleData();
}
void QPlaylistFileParser::stop()
{
Q_D(QPlaylistFileParser);
if (d->m_currentParser) {
disconnect(d->m_currentParser, SIGNAL(newItem(QVariant)), this, SIGNAL(newItem(QVariant)));
disconnect(d->m_currentParser, SIGNAL(finished()), this, SLOT(_q_handleParserFinished()));
disconnect(d->m_currentParser, SIGNAL(error(QPlaylistFileParser::ParserError, QString)),
this, SLOT(_q_handleParserError(QPlaylistFileParser::ParserError, QString)));
d->m_currentParser->deleteLater();
d->m_currentParser = 0;
}
d->m_buffer.clear();
d->m_scanIndex = 0;
d->m_lineIndex = -1;
if (d->m_source) {
disconnect(d->m_source, SIGNAL(readyRead()), this, SLOT(_q_handleData()));
disconnect(d->m_source, SIGNAL(finished()), this, SLOT(_q_handleData()));
disconnect(d->m_source, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(_q_handleError()));
d->m_source->deleteLater();
d->m_source = 0;
}
}
#include "moc_playlistfileparser_p.cpp"
#include "playlistfileparser.moc"
QT_END_NAMESPACE