diff options
Diffstat (limited to 'src/multimedia/playback/qplaylistfileparser.cpp')
-rw-r--r-- | src/multimedia/playback/qplaylistfileparser.cpp | 640 |
1 files changed, 640 insertions, 0 deletions
diff --git a/src/multimedia/playback/qplaylistfileparser.cpp b/src/multimedia/playback/qplaylistfileparser.cpp new file mode 100644 index 000000000..d0617d2cf --- /dev/null +++ b/src/multimedia/playback/qplaylistfileparser.cpp @@ -0,0 +1,640 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** 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 The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/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 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qplaylistfileparser_p.h" +#include <qfileinfo.h> +#include <QtCore/QDebug> +#include <QtCore/qiodevice.h> +#include <QtNetwork/QNetworkReply> +#include <QtNetwork/QNetworkRequest> +#include "qmediaplayer.h" +#include "qmediaobject_p.h" +#include "qmediametadata.h" +#include "qmediacontent.h" +#include "qmediaresource.h" + +QT_BEGIN_NAMESPACE + +namespace { + +class ParserBase +{ +public: + explicit ParserBase(QPlaylistFileParser *parent) + : m_parent(parent) + , m_aborted(false) + { + Q_ASSERT(m_parent); + } + + bool parseLine(int lineIndex, const QString& line, const QUrl& root) + { + if (m_aborted) + return false; + + const bool ok = parseLineImpl(lineIndex, line, root); + return ok && !m_aborted; + } + + virtual void abort() { m_aborted = true; } + virtual ~ParserBase() { } + +protected: + virtual bool parseLineImpl(int lineIndex, const QString& line, const QUrl& root) = 0; + + static 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 QUrl::fromUserInput(line, root.adjusted(QUrl::RemoveFilename).toLocalFile(), QUrl::AssumeLocalFile); + 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; + } + + void newItemFound(const QVariant& content) { Q_EMIT m_parent->newItem(content); } + +private: + QPlaylistFileParser *m_parent; + bool m_aborted; +}; + +class M3UParser : public ParserBase +{ +public: + explicit M3UParser(QPlaylistFileParser *q) + : ParserBase(q) + , 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 + + */ + bool parseLineImpl(int lineIndex, const QString& line, const QUrl& root) override + { + 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.midRef(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.midRef(artistStart + 1, + titleStart - artistStart - 1).trimmed().toString(). + replace(QLatin1String("--"), QLatin1String("-")); + m_extraInfo[QMediaMetaData::Title] = line.midRef(titleStart + 1).trimmed().toString(). + replace(QLatin1String("--"), QLatin1String("-")); + } else { + m_extraInfo[QMediaMetaData::Title] = line.midRef(artistStart + 1).trimmed().toString(). + replace(QLatin1String("--"), QLatin1String("-")); + } + } + } + } else if (lineIndex == 0 && line.startsWith(QLatin1String("#EXTM3U"))) { + m_extendedFormat = true; + } + } else { + m_extraInfo[QLatin1String("url")] = expandToFullPath(root, line); + newItemFound(QVariant(m_extraInfo)); + m_extraInfo.clear(); + } + + return true; + } + + 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; + ++i; + if (buf[i] != '-') + return i - 1; + } + } + return -1; + } + +private: + QVariantMap m_extraInfo; + bool m_extendedFormat; +}; + +class PLSParser : public ParserBase +{ +public: + explicit PLSParser(QPlaylistFileParser *q) + : ParserBase(q) + { + } + +/* + * +The format is essentially that of an INI file structured as follows: + +Header + + * [playlist] : This tag indicates that it is a Playlist File + +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 +*/ + bool parseLineImpl(int, const QString &line, const QUrl &root) override + { + // We ignore everything but 'File' entries, since that's the only thing we care about. + if (!line.startsWith(QLatin1String("File"))) + return true; + + QString value = getValue(line); + if (value.isEmpty()) + return true; + + newItemFound(expandToFullPath(root, value)); + + return true; + } + + QString getValue(const QString& line) { + int start = line.indexOf('='); + if (start < 0) + return QString(); + return line.midRef(start + 1).trimmed().toString(); + } +}; +} + +///////////////////////////////////////////////////////////////////////////////////////////////// + +class QPlaylistFileParserPrivate +{ + Q_DECLARE_PUBLIC(QPlaylistFileParser) +public: + QPlaylistFileParserPrivate(QPlaylistFileParser *q) + : q_ptr(q) + , m_stream(0) + , m_type(QPlaylistFileParser::UNKNOWN) + , m_scanIndex(0) + , m_lineIndex(-1) + , m_utf8(false) + , m_aborted(false) + { + } + + void handleData(); + void handleParserFinished(); + void abort(); + void reset(); + + QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> m_source; + QScopedPointer<ParserBase> m_currentParser; + QByteArray m_buffer; + QUrl m_root; + QNetworkAccessManager m_mgr; + QString m_mimeType; + QPlaylistFileParser *q_ptr; + QIODevice *m_stream; + QPlaylistFileParser::FileType m_type; + struct ParserJob + { + QIODevice *m_stream; + QMediaResource m_resource; + bool isValid() const { return m_stream || !m_resource.isNull(); } + void reset() { m_stream = 0; m_resource = QMediaResource(); } + } m_pendingJob; + int m_scanIndex; + int m_lineIndex; + bool m_utf8; + bool m_aborted; + +private: + bool processLine(int startIndex, int length); +}; + +#define LINE_LIMIT 4096 +#define READ_LIMIT 64 + +bool QPlaylistFileParserPrivate::processLine(int startIndex, int length) +{ + Q_Q(QPlaylistFileParser); + m_lineIndex++; + + if (!m_currentParser) { + const QString urlString = m_root.toString(); + const QString &suffix = !urlString.isEmpty() ? QFileInfo(urlString).suffix() : urlString; + const QString &mimeType = m_source->header(QNetworkRequest::ContentTypeHeader).toString(); + m_type = QPlaylistFileParser::findPlaylistType(suffix, !mimeType.isEmpty() ? mimeType : m_mimeType, m_buffer.constData(), quint32(m_buffer.size())); + + switch (m_type) { + case QPlaylistFileParser::UNKNOWN: + emit q->error(QPlaylistFileParser::FormatError, + QPlaylistFileParser::tr("%1 playlist type is unknown").arg(m_root.toString())); + q->abort(); + return false; + case QPlaylistFileParser::M3U: + m_currentParser.reset(new M3UParser(q)); + break; + case QPlaylistFileParser::M3U8: + m_currentParser.reset(new M3UParser(q)); + m_utf8 = true; + break; + case QPlaylistFileParser::PLS: + m_currentParser.reset(new PLSParser(q)); + break; + } + + Q_ASSERT(!m_currentParser.isNull()); + } + + 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 true; + + Q_ASSERT(m_currentParser); + return m_currentParser->parseLine(m_lineIndex, line, m_root); +} + +void QPlaylistFileParserPrivate::handleData() +{ + Q_Q(QPlaylistFileParser); + while (m_source->bytesAvailable() && !m_aborted) { + int expectedBytes = qMin(READ_LIMIT, int(qMin(m_source->bytesAvailable(), + qint64(LINE_LIMIT - m_buffer.size())))); + m_buffer.push_back(m_source->read(expectedBytes)); + int processedBytes = 0; + while (m_scanIndex < m_buffer.length() && !m_aborted) { + char s = m_buffer[m_scanIndex]; + if (s == '\r' || s == '\n') { + int l = m_scanIndex - processedBytes; + if (l > 0) { + if (!processLine(processedBytes, l)) + break; + } + processedBytes = m_scanIndex + 1; + if (!m_source) { + //some error happened, so exit parsing + return; + } + } + m_scanIndex++; + } + + if (m_aborted) + break; + + if (m_buffer.length() - processedBytes >= LINE_LIMIT) { + emit q->error(QPlaylistFileParser::FormatError, QPlaylistFileParser::tr("invalid line in playlist file")); + q->abort(); + break; + } + + 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; + } + + handleParserFinished(); +} + +QPlaylistFileParser::QPlaylistFileParser(QObject *parent) + : QObject(parent) + , d_ptr(new QPlaylistFileParserPrivate(this)) +{ + +} + +QPlaylistFileParser::~QPlaylistFileParser() +{ + +} + +QPlaylistFileParser::FileType QPlaylistFileParser::findByMimeType(const QString &mime) +{ + if (mime == QLatin1String("text/uri-list") || mime == QLatin1String("audio/x-mpegurl") || mime == QLatin1String("audio/mpegurl")) + return QPlaylistFileParser::M3U; + + if (mime == QLatin1String("application/x-mpegURL") || mime == QLatin1String("application/vnd.apple.mpegurl")) + return QPlaylistFileParser::M3U8; + + if (mime == QLatin1String("audio/x-scpls")) + return QPlaylistFileParser::PLS; + + return QPlaylistFileParser::UNKNOWN; +} + +QPlaylistFileParser::FileType QPlaylistFileParser::findBySuffixType(const QString &suffix) +{ + const QString &s = suffix.toLower(); + + if (s == QLatin1String("m3u")) + return QPlaylistFileParser::M3U; + + if (s == QLatin1String("m3u8")) + return QPlaylistFileParser::M3U8; + + if (s == QLatin1String("pls")) + return QPlaylistFileParser::PLS; + + return QPlaylistFileParser::UNKNOWN; +} + +QPlaylistFileParser::FileType QPlaylistFileParser::findByDataHeader(const char *data, quint32 size) +{ + if (!data || size == 0) + return QPlaylistFileParser::UNKNOWN; + + if (size >= 7 && strncmp(data, "#EXTM3U", 7) == 0) + return QPlaylistFileParser::M3U; + + if (size >= 10 && strncmp(data, "[playlist]", 10) == 0) + return QPlaylistFileParser::PLS; + + return QPlaylistFileParser::UNKNOWN; +} + +QPlaylistFileParser::FileType QPlaylistFileParser::findPlaylistType(const QString& suffix, + const QString& mime, + const char *data, + quint32 size) +{ + + FileType dataHeaderType = findByDataHeader(data, size); + if (dataHeaderType != UNKNOWN) + return dataHeaderType; + + FileType mimeType = findByMimeType(mime); + if (mimeType != UNKNOWN) + return mimeType; + + FileType suffixType = findBySuffixType(suffix); + if (suffixType != UNKNOWN) + return suffixType; + + return UNKNOWN; +} + +/* + * Delegating + */ +void QPlaylistFileParser::start(const QMediaContent &media, QIODevice *stream) +{ + const QMediaResource &mediaResource = media.canonicalResource(); + const QString &mimeType = mediaResource.mimeType(); + + if (stream) { + start(stream, mediaResource.mimeType()); + } else { + const QNetworkRequest &request = mediaResource.request(); + const QUrl &url = mediaResource.url(); + + if (request.url().isValid()) + start(request, mimeType); + else + start(QNetworkRequest(url), mimeType); + } +} + +void QPlaylistFileParser::start(QIODevice *stream, const QString &mimeType) +{ + Q_D(QPlaylistFileParser); + const bool validStream = stream ? (stream->isOpen() && stream->isReadable()) : false; + + if (!validStream) { + Q_EMIT error(ResourceError, tr("Invalid stream")); + return; + } + + if (!d->m_currentParser.isNull()) { + abort(); + d->m_pendingJob = { stream, QMediaResource(QUrl(), mimeType) }; + return; + } + + d->reset(); + d->m_mimeType = mimeType; + d->m_stream = stream; + connect(d->m_stream, SIGNAL(readyRead()), this, SLOT(_q_handleData())); + d->handleData(); +} + +void QPlaylistFileParser::start(const QNetworkRequest& request, const QString &mimeType) +{ + Q_D(QPlaylistFileParser); + const QUrl &url = request.url(); + + if (url.isLocalFile() && !QFile::exists(url.toLocalFile())) { + emit error(ResourceError, QString(tr("%1 does not exist")).arg(url.toString())); + return; + } + + if (!d->m_currentParser.isNull()) { + abort(); + d->m_pendingJob = { Q_NULLPTR, QMediaResource(request, mimeType) }; + return; + } + + d->reset(); + d->m_root = url; + d->m_mimeType = mimeType; + d->m_source.reset(d->m_mgr.get(request)); + connect(d->m_source.data(), SIGNAL(readyRead()), this, SLOT(handleData())); + connect(d->m_source.data(), SIGNAL(finished()), this, SLOT(handleData())); + connect(d->m_source.data(), SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(handleError())); + + d->handleData(); +} + +void QPlaylistFileParser::abort() +{ + Q_D(QPlaylistFileParser); + d->abort(); + + if (d->m_source) + d->m_source->disconnect(); + + if (d->m_stream) + disconnect(d->m_stream, SIGNAL(readyRead()), this, SLOT(handleData())); +} + +void QPlaylistFileParser::handleData() +{ + Q_D(QPlaylistFileParser); + d->handleData(); +} + +void QPlaylistFileParserPrivate::handleParserFinished() +{ + Q_Q(QPlaylistFileParser); + const bool isParserValid = !m_currentParser.isNull(); + if (!isParserValid && !m_aborted) + emit q->error(QPlaylistFileParser::FormatNotSupportedError, QPlaylistFileParser::tr("Empty file provided")); + + if (isParserValid && !m_aborted) { + m_currentParser.reset(); + emit q->finished(); + } + + if (!m_aborted) + q->abort(); + + if (!m_source.isNull()) + m_source.reset(); + + if (m_pendingJob.isValid()) + q->start(m_pendingJob.m_resource, m_pendingJob.m_stream); +} + +void QPlaylistFileParserPrivate::abort() +{ + m_aborted = true; + if (!m_currentParser.isNull()) + m_currentParser->abort(); +} + +void QPlaylistFileParserPrivate::reset() +{ + Q_ASSERT(m_currentParser.isNull()); + Q_ASSERT(m_source.isNull()); + m_buffer.clear(); + m_root.clear(); + m_mimeType.clear(); + m_stream = 0; + m_type = QPlaylistFileParser::UNKNOWN; + m_scanIndex = 0; + m_lineIndex = -1; + m_utf8 = false; + m_aborted = false; + m_pendingJob.reset(); +} + +void QPlaylistFileParser::handleError() +{ + Q_D(QPlaylistFileParser); + const QString &errorString = d->m_source->errorString(); + Q_EMIT error(QPlaylistFileParser::NetworkError, errorString); + abort(); +} + +QT_END_NAMESPACE |