playlistfileparser.cpp 19.37 KiB
/****************************************************************************
**
** 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