chore: port to Qt 6

This commit is contained in:
Gary Wang
2022-10-05 14:02:46 +08:00
parent 95eddb3af0
commit 7744d3bf2d
16 changed files with 1731 additions and 207 deletions

653
qt/qmediaplaylist.cpp Normal file
View File

@ -0,0 +1,653 @@
// 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 "qmediaplaylist.h"
#include "qmediaplaylist_p.h"
#include "qplaylistfileparser_p.h"
#include <QtCore/qlist.h>
#include <QtCore/qfile.h>
#include <QtCore/qurl.h>
#include <QtCore/qcoreevent.h>
#include <QtCore/qcoreapplication.h>
#include <QRandomGenerator>
QT_BEGIN_NAMESPACE
class QM3uPlaylistWriter
{
public:
QM3uPlaylistWriter(QIODevice *device)
:m_device(device), m_textStream(new QTextStream(m_device))
{
}
~QM3uPlaylistWriter()
{
delete m_textStream;
}
bool writeItem(const QUrl& item)
{
*m_textStream << item.toString() << Qt::endl;
return true;
}
private:
QIODevice *m_device;
QTextStream *m_textStream;
};
int QMediaPlaylistPrivate::nextPosition(int steps) const
{
if (playlist.count() == 0)
return -1;
int next = currentPos + steps;
switch (playbackMode) {
case QMediaPlaylist::CurrentItemOnce:
return steps != 0 ? -1 : currentPos;
case QMediaPlaylist::CurrentItemInLoop:
return currentPos;
case QMediaPlaylist::Sequential:
if (next >= playlist.size())
next = -1;
break;
case QMediaPlaylist::Loop:
next %= playlist.count();
break;
}
return next;
}
int QMediaPlaylistPrivate::prevPosition(int steps) const
{
if (playlist.count() == 0)
return -1;
int next = currentPos;
if (next < 0)
next = playlist.size();
next -= steps;
switch (playbackMode) {
case QMediaPlaylist::CurrentItemOnce:
return steps != 0 ? -1 : currentPos;
case QMediaPlaylist::CurrentItemInLoop:
return currentPos;
case QMediaPlaylist::Sequential:
if (next < 0)
next = -1;
break;
case QMediaPlaylist::Loop:
next %= playlist.size();
if (next < 0)
next += playlist.size();
break;
}
return next;
}
/*!
\class QMediaPlaylist
\inmodule QtMultimedia
\ingroup multimedia
\ingroup multimedia_playback
\brief The QMediaPlaylist class provides a list of media content to play.
QMediaPlaylist is intended to be used with other media objects,
like QMediaPlayer.
QMediaPlaylist allows to access the service intrinsic playlist functionality
if available, otherwise it provides the local memory playlist implementation.
\snippet multimedia-snippets/media.cpp Movie playlist
Depending on playlist source implementation, most of the playlist mutating
operations can be asynchronous.
QMediaPlayList currently supports M3U playlists (file extension .m3u and .m3u8).
\sa QUrl
*/
/*!
\enum QMediaPlaylist::PlaybackMode
The QMediaPlaylist::PlaybackMode describes the order items in playlist are played.
\value CurrentItemOnce The current item is played only once.
\value CurrentItemInLoop The current item is played repeatedly in a loop.
\value Sequential Playback starts from the current and moves through each successive item until the last is reached and then stops.
The next item is a null item when the last one is currently playing.
\value Loop Playback restarts at the first item after the last has finished playing.
\value Random Play items in random order.
*/
/*!
Create a new playlist object with the given \a parent.
*/
QMediaPlaylist::QMediaPlaylist(QObject *parent)
: QObject(parent)
, d_ptr(new QMediaPlaylistPrivate)
{
Q_D(QMediaPlaylist);
d->q_ptr = this;
}
/*!
Destroys the playlist.
*/
QMediaPlaylist::~QMediaPlaylist()
{
delete d_ptr;
}
/*!
\property QMediaPlaylist::playbackMode
This property defines the order that items in the playlist are played.
\sa QMediaPlaylist::PlaybackMode
*/
QMediaPlaylist::PlaybackMode QMediaPlaylist::playbackMode() const
{
return d_func()->playbackMode;
}
void QMediaPlaylist::setPlaybackMode(QMediaPlaylist::PlaybackMode mode)
{
Q_D(QMediaPlaylist);
if (mode == d->playbackMode)
return;
d->playbackMode = mode;
emit playbackModeChanged(mode);
}
/*!
Returns position of the current media content in the playlist.
*/
int QMediaPlaylist::currentIndex() const
{
return d_func()->currentPos;
}
/*!
Returns the current media content.
*/
QUrl QMediaPlaylist::currentMedia() const
{
Q_D(const QMediaPlaylist);
if (d->currentPos < 0 || d->currentPos >= d->playlist.size())
return QUrl();
return d_func()->playlist.at(d_func()->currentPos);
}
/*!
Returns the index of the item, which would be current after calling next()
\a steps times.
Returned value depends on the size of playlist, current position
and playback mode.
\sa QMediaPlaylist::playbackMode(), previousIndex()
*/
int QMediaPlaylist::nextIndex(int steps) const
{
return d_func()->nextPosition(steps);
}
/*!
Returns the index of the item, which would be current after calling previous()
\a steps times.
\sa QMediaPlaylist::playbackMode(), nextIndex()
*/
int QMediaPlaylist::previousIndex(int steps) const
{
return d_func()->prevPosition(steps);
}
/*!
Returns the number of items in the playlist.
\sa isEmpty()
*/
int QMediaPlaylist::mediaCount() const
{
return d_func()->playlist.count();
}
/*!
Returns true if the playlist contains no items, otherwise returns false.
\sa mediaCount()
*/
bool QMediaPlaylist::isEmpty() const
{
return mediaCount() == 0;
}
/*!
Returns the media content at \a index in the playlist.
*/
QUrl QMediaPlaylist::media(int index) const
{
Q_D(const QMediaPlaylist);
if (index < 0 || index >= d->playlist.size())
return QUrl();
return d->playlist.at(index);
}
/*!
Append the media \a content to the playlist.
Returns true if the operation is successful, otherwise returns false.
*/
void QMediaPlaylist::addMedia(const QUrl &content)
{
Q_D(QMediaPlaylist);
int pos = d->playlist.size();
emit mediaAboutToBeInserted(pos, pos);
d->playlist.append(content);
emit mediaInserted(pos, pos);
}
/*!
Append multiple media content \a items to the playlist.
Returns true if the operation is successful, otherwise returns false.
*/
void QMediaPlaylist::addMedia(const QList<QUrl> &items)
{
if (!items.size())
return;
Q_D(QMediaPlaylist);
int first = d->playlist.size();
int last = first + items.size() - 1;
emit mediaAboutToBeInserted(first, last);
d_func()->playlist.append(items);
emit mediaInserted(first, last);
}
/*!
Insert the media \a content to the playlist at position \a pos.
Returns true if the operation is successful, otherwise returns false.
*/
bool QMediaPlaylist::insertMedia(int pos, const QUrl &content)
{
Q_D(QMediaPlaylist);
pos = qBound(0, pos, d->playlist.size());
emit mediaAboutToBeInserted(pos, pos);
d->playlist.insert(pos, content);
emit mediaInserted(pos, pos);
return true;
}
/*!
Insert multiple media content \a items to the playlist at position \a pos.
Returns true if the operation is successful, otherwise returns false.
*/
bool QMediaPlaylist::insertMedia(int pos, const QList<QUrl> &items)
{
if (!items.size())
return true;
Q_D(QMediaPlaylist);
pos = qBound(0, pos, d->playlist.size());
int last = pos + items.size() - 1;
emit mediaAboutToBeInserted(pos, last);
auto newList = d->playlist.mid(0, pos);
newList += items;
newList += d->playlist.mid(pos);
d->playlist = newList;
emit mediaInserted(pos, last);
return true;
}
/*!
Move the item from position \a from to position \a to.
Returns true if the operation is successful, otherwise false.
\since 5.7
*/
bool QMediaPlaylist::moveMedia(int from, int to)
{
Q_D(QMediaPlaylist);
if (from < 0 || from > d->playlist.count() ||
to < 0 || to > d->playlist.count())
return false;
d->playlist.move(from, to);
emit mediaChanged(from, to);
return true;
}
/*!
Remove the item from the playlist at position \a pos.
Returns true if the operation is successful, otherwise return false.
*/
bool QMediaPlaylist::removeMedia(int pos)
{
return removeMedia(pos, pos);
}
/*!
Remove items in the playlist from \a start to \a end inclusive.
Returns true if the operation is successful, otherwise return false.
*/
bool QMediaPlaylist::removeMedia(int start, int end)
{
Q_D(QMediaPlaylist);
if (end < start || end < 0 || start >= d->playlist.count())
return false;
start = qBound(0, start, d->playlist.size() - 1);
end = qBound(0, end, d->playlist.size() - 1);
emit mediaAboutToBeRemoved(start, end);
d->playlist.remove(start, end - start + 1);
emit mediaRemoved(start, end);
return true;
}
/*!
Remove all the items from the playlist.
Returns true if the operation is successful, otherwise return false.
*/
void QMediaPlaylist::clear()
{
Q_D(QMediaPlaylist);
int size = d->playlist.size();
emit mediaAboutToBeRemoved(0, size - 1);
d->playlist.clear();
emit mediaRemoved(0, size - 1);
}
/*!
Load playlist from \a location. If \a format is specified, it is used,
otherwise format is guessed from location name and data.
New items are appended to playlist.
QMediaPlaylist::loaded() signal is emitted if playlist was loaded successfully,
otherwise the playlist emits loadFailed().
*/
void QMediaPlaylist::load(const QUrl &location, const char *format)
{
Q_D(QMediaPlaylist);
d->error = NoError;
d->errorString.clear();
d->ensureParser();
d->parser->start(location, QString::fromUtf8(format));
}
/*!
Load playlist from QIODevice \a device. If \a format is specified, it is used,
otherwise format is guessed from device data.
New items are appended to playlist.
QMediaPlaylist::loaded() signal is emitted if playlist was loaded successfully,
otherwise the playlist emits loadFailed().
*/
void QMediaPlaylist::load(QIODevice *device, const char *format)
{
Q_D(QMediaPlaylist);
d->error = NoError;
d->errorString.clear();
d->ensureParser();
d->parser->start(device, QString::fromUtf8(format));
}
/*!
Save playlist to \a location. If \a format is specified, it is used,
otherwise format is guessed from location name.
Returns true if playlist was saved successfully, otherwise returns false.
*/
bool QMediaPlaylist::save(const QUrl &location, const char *format) const
{
Q_D(const QMediaPlaylist);
d->error = NoError;
d->errorString.clear();
if (!d->checkFormat(format))
return false;
QFile file(location.toLocalFile());
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
d->error = AccessDeniedError;
d->errorString = tr("The file could not be accessed.");
return false;
}
return save(&file, format);
}
/*!
Save playlist to QIODevice \a device using format \a format.
Returns true if playlist was saved successfully, otherwise returns false.
*/
bool QMediaPlaylist::save(QIODevice *device, const char *format) const
{
Q_D(const QMediaPlaylist);
d->error = NoError;
d->errorString.clear();
if (!d->checkFormat(format))
return false;
QM3uPlaylistWriter writer(device);
for (const auto &entry : d->playlist)
writer.writeItem(entry);
return true;
}
/*!
Returns the last error condition.
*/
QMediaPlaylist::Error QMediaPlaylist::error() const
{
return d_func()->error;
}
/*!
Returns the string describing the last error condition.
*/
QString QMediaPlaylist::errorString() const
{
return d_func()->errorString;
}
/*!
Shuffle items in the playlist.
*/
void QMediaPlaylist::shuffle()
{
Q_D(QMediaPlaylist);
QList<QUrl> playlist;
// keep the current item when shuffling
QUrl current;
if (d->currentPos != -1)
current = d->playlist.takeAt(d->currentPos);
while (!d->playlist.isEmpty())
playlist.append(d->playlist.takeAt(QRandomGenerator::global()->bounded(int(d->playlist.size()))));
if (d->currentPos != -1)
playlist.insert(d->currentPos, current);
d->playlist = playlist;
emit mediaChanged(0, d->playlist.count());
}
/*!
Advance to the next media content in playlist.
*/
void QMediaPlaylist::next()
{
Q_D(QMediaPlaylist);
d->currentPos = d->nextPosition(1);
emit currentIndexChanged(d->currentPos);
emit currentMediaChanged(currentMedia());
}
/*!
Return to the previous media content in playlist.
*/
void QMediaPlaylist::previous()
{
Q_D(QMediaPlaylist);
d->currentPos = d->prevPosition(1);
emit currentIndexChanged(d->currentPos);
emit currentMediaChanged(currentMedia());
}
/*!
Activate media content from playlist at position \a playlistPosition.
*/
void QMediaPlaylist::setCurrentIndex(int playlistPosition)
{
Q_D(QMediaPlaylist);
if (playlistPosition < 0 || playlistPosition >= d->playlist.size())
playlistPosition = -1;
d->currentPos = playlistPosition;
emit currentIndexChanged(d->currentPos);
emit currentMediaChanged(currentMedia());
}
/*!
\fn void QMediaPlaylist::mediaInserted(int start, int end)
This signal is emitted after media has been inserted into the playlist.
The new items are those between \a start and \a end inclusive.
*/
/*!
\fn void QMediaPlaylist::mediaRemoved(int start, int end)
This signal is emitted after media has been removed from the playlist.
The removed items are those between \a start and \a end inclusive.
*/
/*!
\fn void QMediaPlaylist::mediaChanged(int start, int end)
This signal is emitted after media has been changed in the playlist
between \a start and \a end positions inclusive.
*/
/*!
\fn void QMediaPlaylist::currentIndexChanged(int position)
Signal emitted when playlist position changed to \a position.
*/
/*!
\fn void QMediaPlaylist::playbackModeChanged(QMediaPlaylist::PlaybackMode mode)
Signal emitted when playback mode changed to \a mode.
*/
/*!
\fn void QMediaPlaylist::mediaAboutToBeInserted(int start, int end)
Signal emitted when items are to be inserted at \a start and ending at \a end.
*/
/*!
\fn void QMediaPlaylist::mediaAboutToBeRemoved(int start, int end)
Signal emitted when item are to be deleted at \a start and ending at \a end.
*/
/*!
\fn void QMediaPlaylist::currentMediaChanged(const QUrl &content)
Signal emitted when current media changes to \a content.
*/
/*!
\property QMediaPlaylist::currentIndex
\brief Current position.
*/
/*!
\property QMediaPlaylist::currentMedia
\brief Current media content.
*/
/*!
\fn QMediaPlaylist::loaded()
Signal emitted when playlist finished loading.
*/
/*!
\fn QMediaPlaylist::loadFailed()
Signal emitted if failed to load playlist.
*/
/*!
\enum QMediaPlaylist::Error
This enum describes the QMediaPlaylist error codes.
\value NoError No errors.
\value FormatError Format error.
\value FormatNotSupportedError Format not supported.
\value NetworkError Network error.
\value AccessDeniedError Access denied error.
*/
QT_END_NAMESPACE
#include "moc_qmediaplaylist.cpp"

96
qt/qmediaplaylist.h Normal file
View File

@ -0,0 +1,96 @@
// 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
#ifndef QMEDIAPLAYLIST_H
#define QMEDIAPLAYLIST_H
#include <QtCore/qobject.h>
#include <QtMultimedia/qtmultimediaglobal.h>
#include <QtMultimedia/qmediaenumdebug.h>
QT_BEGIN_NAMESPACE
class QMediaPlaylistPrivate;
class QMediaPlaylist : public QObject
{
Q_OBJECT
Q_PROPERTY(QMediaPlaylist::PlaybackMode playbackMode READ playbackMode WRITE setPlaybackMode NOTIFY playbackModeChanged)
Q_PROPERTY(QUrl currentMedia READ currentMedia NOTIFY currentMediaChanged)
Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
public:
enum PlaybackMode { CurrentItemOnce, CurrentItemInLoop, Sequential, Loop };
Q_ENUM(PlaybackMode)
enum Error { NoError, FormatError, FormatNotSupportedError, NetworkError, AccessDeniedError };
Q_ENUM(Error)
explicit QMediaPlaylist(QObject *parent = nullptr);
virtual ~QMediaPlaylist();
PlaybackMode playbackMode() const;
void setPlaybackMode(PlaybackMode mode);
int currentIndex() const;
QUrl currentMedia() const;
int nextIndex(int steps = 1) const;
int previousIndex(int steps = 1) const;
QUrl media(int index) const;
int mediaCount() const;
bool isEmpty() const;
void addMedia(const QUrl &content);
void addMedia(const QList<QUrl> &items);
bool insertMedia(int index, const QUrl &content);
bool insertMedia(int index, const QList<QUrl> &items);
bool moveMedia(int from, int to);
bool removeMedia(int pos);
bool removeMedia(int start, int end);
void clear();
void load(const QUrl &location, const char *format = nullptr);
void load(QIODevice *device, const char *format = nullptr);
bool save(const QUrl &location, const char *format = nullptr) const;
bool save(QIODevice *device, const char *format) const;
Error error() const;
QString errorString() const;
public Q_SLOTS:
void shuffle();
void next();
void previous();
void setCurrentIndex(int index);
Q_SIGNALS:
void currentIndexChanged(int index);
void playbackModeChanged(QMediaPlaylist::PlaybackMode mode);
void currentMediaChanged(const QUrl&);
void mediaAboutToBeInserted(int start, int end);
void mediaInserted(int start, int end);
void mediaAboutToBeRemoved(int start, int end);
void mediaRemoved(int start, int end);
void mediaChanged(int start, int end);
void loaded();
void loadFailed();
private:
QMediaPlaylistPrivate *d_ptr;
Q_DECLARE_PRIVATE(QMediaPlaylist)
};
QT_END_NAMESPACE
Q_MEDIA_ENUM_DEBUG(QMediaPlaylist, PlaybackMode)
Q_MEDIA_ENUM_DEBUG(QMediaPlaylist, Error)
#endif // QMEDIAPLAYLIST_H

112
qt/qmediaplaylist_p.h Normal file
View File

@ -0,0 +1,112 @@
// 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
#ifndef QMEDIAPLAYLIST_P_H
#define QMEDIAPLAYLIST_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include "qmediaplaylist.h"
#include "qplaylistfileparser_p.h"
#include <QtCore/qdebug.h>
#ifdef Q_MOC_RUN
# pragma Q_MOC_EXPAND_MACROS
#endif
QT_BEGIN_NAMESPACE
class QMediaPlaylistControl;
class QMediaPlaylistPrivate
{
Q_DECLARE_PUBLIC(QMediaPlaylist)
public:
QMediaPlaylistPrivate()
: error(QMediaPlaylist::NoError)
{
}
virtual ~QMediaPlaylistPrivate()
{
if (parser)
delete parser;
}
void loadFailed(QMediaPlaylist::Error error, const QString &errorString)
{
this->error = error;
this->errorString = errorString;
emit q_ptr->loadFailed();
}
void loadFinished()
{
q_ptr->addMedia(parser->playlist);
emit q_ptr->loaded();
}
bool checkFormat(const char *format) const
{
QLatin1String f(format);
QPlaylistFileParser::FileType type = format ? QPlaylistFileParser::UNKNOWN : QPlaylistFileParser::M3U8;
if (format) {
if (f == QLatin1String("m3u") || f == QLatin1String("text/uri-list") ||
f == QLatin1String("audio/x-mpegurl") || f == QLatin1String("audio/mpegurl"))
type = QPlaylistFileParser::M3U;
else if (f == QLatin1String("m3u8") || f == QLatin1String("application/x-mpegURL") ||
f == QLatin1String("application/vnd.apple.mpegurl"))
type = QPlaylistFileParser::M3U8;
}
if (type == QPlaylistFileParser::UNKNOWN || type == QPlaylistFileParser::PLS) {
error = QMediaPlaylist::FormatNotSupportedError;
errorString = QMediaPlaylist::tr("This file format is not supported.");
return false;
}
return true;
}
void ensureParser()
{
if (parser)
return;
parser = new QPlaylistFileParser(q_ptr);
QObject::connect(parser, &QPlaylistFileParser::finished, [this]() { loadFinished(); });
QObject::connect(parser, &QPlaylistFileParser::error,
[this](QMediaPlaylist::Error err, const QString& errorMsg) { loadFailed(err, errorMsg); });
}
int nextPosition(int steps) const;
int prevPosition(int steps) const;
QList<QUrl> playlist;
int currentPos = -1;
QMediaPlaylist::PlaybackMode playbackMode = QMediaPlaylist::Sequential;
QPlaylistFileParser *parser = nullptr;
mutable QMediaPlaylist::Error error;
mutable QString errorString;
QMediaPlaylist *q_ptr;
};
QT_END_NAMESPACE
#endif // QMEDIAPLAYLIST_P_H

605
qt/qplaylistfileparser.cpp Normal file
View File

@ -0,0 +1,605 @@
// 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

View File

@ -0,0 +1,80 @@
// 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
#ifndef PLAYLISTFILEPARSER_P_H
#define PLAYLISTFILEPARSER_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include "qtmultimediaglobal.h"
#include "qmediaplaylist.h"
#include <QtCore/qobject.h>
QT_BEGIN_NAMESPACE
class QIODevice;
class QUrl;
class QNetworkRequest;
class QPlaylistFileParserPrivate;
class QPlaylistFileParser : public QObject
{
Q_OBJECT
public:
QPlaylistFileParser(QObject *parent = nullptr);
~QPlaylistFileParser();
enum FileType
{
UNKNOWN,
M3U,
M3U8, // UTF-8 version of M3U
PLS
};
void start(const QUrl &media, QIODevice *stream = nullptr, const QString &mimeType = QString());
void start(const QUrl &request, const QString &mimeType = QString());
void start(QIODevice *stream, const QString &mimeType = QString());
void abort();
QList<QUrl> playlist;
Q_SIGNALS:
void newItem(const QVariant& content);
void finished();
void error(QMediaPlaylist::Error err, const QString& errorMsg);
private Q_SLOTS:
void handleData();
void handleError();
private:
static FileType findByMimeType(const QString &mime);
static FileType findBySuffixType(const QString &suffix);
static FileType findByDataHeader(const char *data, quint32 size);
static FileType findPlaylistType(QIODevice *device,
const QString& mime);
static FileType findPlaylistType(const QString &suffix,
const QString& mime,
const char *data = nullptr,
quint32 size = 0);
Q_DISABLE_COPY(QPlaylistFileParser)
Q_DECLARE_PRIVATE(QPlaylistFileParser)
QScopedPointer<QPlaylistFileParserPrivate> d_ptr;
};
QT_END_NAMESPACE
#endif // PLAYLISTFILEPARSER_P_H