pineapple-music/qt/qplaylistfileparser.cpp

606 lines
17 KiB
C++
Raw Normal View History

2022-10-05 14:02:46 +08:00
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qplaylistfileparser_p.h"
#include <qfileinfo.h>
#include <QtCore/QDebug>
#include <QtCore/qiodevice.h>
#include <QtCore/qpointer.h>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QNetworkRequest>
#include "qmediaplayer.h"
#include "qmediametadata.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() = default;
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);
return root.resolved(url);
}
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); }
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] == u'#' ) {
if (m_extendedFormat) {
if (line.startsWith(QLatin1String("#EXTINF:"))) {
m_extraInfo.clear();
int artistStart = line.indexOf(QLatin1String(","), 8);
bool ok = false;
QStringView lineView { line };
int length = lineView.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] = lineView.mid(artistStart + 1,
titleStart - artistStart - 1).trimmed().toString().
replace(QLatin1String("--"), QLatin1String("-"));
m_extraInfo[QMediaMetaData::Title] = lineView.mid(titleStart + 1).trimmed().toString().
replace(QLatin1String("--"), QLatin1String("-"));
} else {
m_extraInfo[QMediaMetaData::Title] = lineView.mid(artistStart + 1).trimmed().toString().
replace(QLatin1String("--"), QLatin1String("-"));
}
}
}
} else if (lineIndex == 0 && line.startsWith(QLatin1String("#EXTM3U"))) {
m_extendedFormat = true;
}
} else {
QUrl url = expandToFullPath(root, line);
m_extraInfo[QMediaMetaData::Url] = url;
m_parent->playlist.append(url);
newItemFound(QVariant::fromValue(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] == u'-') {
if (i == line.length() - 1)
return i;
++i;
if (buf[i] != u'-')
return i - 1;
}
}
return -1;
}
private:
QMediaMetaData 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;
QUrl path = expandToFullPath(root, value);
m_parent->playlist.append(path);
newItemFound(path);
return true;
}
QString getValue(QStringView line) {
int start = line.indexOf(u'=');
if (start < 0)
return QString();
return line.mid(start + 1).trimmed().toString();
}
};
}
/////////////////////////////////////////////////////////////////////////////////////////////////
class QPlaylistFileParserPrivate
{
Q_DECLARE_PUBLIC(QPlaylistFileParser)
public:
QPlaylistFileParserPrivate(QPlaylistFileParser *q)
: q_ptr(q)
, m_stream(nullptr)
, 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;
QPointer<QIODevice> m_stream;
QPlaylistFileParser::FileType m_type;
struct ParserJob
{
QIODevice *m_stream;
QUrl m_media;
QString m_mimeType;
[[nodiscard]] bool isValid() const { return m_stream || !m_media.isEmpty(); }
void reset() { m_stream = nullptr; m_media = QUrl(); m_mimeType = QString(); }
} 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;
QString mimeType;
if (m_source)
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(QMediaPlaylist::FormatError,
QMediaPlaylist::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_stream->bytesAvailable() && !m_aborted) {
int expectedBytes = qMin(READ_LIMIT, int(qMin(m_stream->bytesAvailable(),
qint64(LINE_LIMIT - m_buffer.size()))));
m_buffer.push_back(m_stream->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_stream) {
//some error happened, so exit parsing
return;
}
}
m_scanIndex++;
}
if (m_aborted)
break;
if (m_buffer.length() - processedBytes >= LINE_LIMIT) {
emit q->error(QMediaPlaylist::FormatError, QMediaPlaylist::tr("invalid line in playlist file"));
q->abort();
break;
}
if (!m_stream->bytesAvailable() && (!m_source || !m_source->isFinished())) {
//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() = default;
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;
mimeType = findBySuffixType(mime);
if (mimeType != UNKNOWN)
return mimeType;
FileType suffixType = findBySuffixType(suffix);
if (suffixType != UNKNOWN)
return suffixType;
return UNKNOWN;
}
/*
* Delegating
*/
void QPlaylistFileParser::start(const QUrl &media, QIODevice *stream, const QString &mimeType)
{
if (stream)
start(stream, mimeType);
else
start(media, 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(QMediaPlaylist::AccessDeniedError, QMediaPlaylist::tr("Invalid stream"));
return;
}
if (!d->m_currentParser.isNull()) {
abort();
d->m_pendingJob = { stream, QUrl(), mimeType };
return;
}
playlist.clear();
d->reset();
d->m_mimeType = mimeType;
d->m_stream = stream;
connect(d->m_stream, SIGNAL(readyRead()), this, SLOT(handleData()));
d->handleData();
}
void QPlaylistFileParser::start(const QUrl& request, const QString &mimeType)
{
Q_D(QPlaylistFileParser);
const QUrl &url = request.url();
if (url.isLocalFile() && !QFile::exists(url.toLocalFile())) {
emit error(QMediaPlaylist::AccessDeniedError, QString(QMediaPlaylist::tr("%1 does not exist")).arg(url.toString()));
return;
}
if (!d->m_currentParser.isNull()) {
abort();
d->m_pendingJob = { nullptr, request, mimeType };
return;
}
d->reset();
d->m_root = url;
d->m_mimeType = mimeType;
d->m_source.reset(d->m_mgr.get(QNetworkRequest(request)));
d->m_stream = d->m_source.get();
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(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(handleError()));
if (url.isLocalFile())
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()));
playlist.clear();
}
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(QMediaPlaylist::FormatNotSupportedError, QMediaPlaylist::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_media, m_pendingJob.m_stream, m_pendingJob.m_mimeType);
}
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 = nullptr;
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(QMediaPlaylist::NetworkError, errorString);
abort();
}
QT_END_NAMESPACE