1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "qplaylistfileparser_p.h"
41#include <qfileinfo.h>
42#include <QtCore/QDebug>
43#include <QtCore/qiodevice.h>
44#include <QtNetwork/QNetworkReply>
45#include <QtNetwork/QNetworkRequest>
46#include "qmediaplayer.h"
47#include "qmediaobject_p.h"
48#include "qmediametadata.h"
49#include "qmediacontent.h"
50#include "qmediaresource.h"
51
52QT_BEGIN_NAMESPACE
53
54namespace {
55
56class ParserBase
57{
58public:
59 explicit ParserBase(QPlaylistFileParser *parent)
60 : m_parent(parent)
61 , m_aborted(false)
62 {
63 Q_ASSERT(m_parent);
64 }
65
66 bool parseLine(int lineIndex, const QString& line, const QUrl& root)
67 {
68 if (m_aborted)
69 return false;
70
71 const bool ok = parseLineImpl(lineIndex, line, root);
72 return ok && !m_aborted;
73 }
74
75 virtual void abort() { m_aborted = true; }
76 virtual ~ParserBase() { }
77
78protected:
79 virtual bool parseLineImpl(int lineIndex, const QString& line, const QUrl& root) = 0;
80
81 static QUrl expandToFullPath(const QUrl &root, const QString &line)
82 {
83 // On Linux, backslashes are not converted to forward slashes :/
84 if (line.startsWith(s: QLatin1String("//")) || line.startsWith(s: QLatin1String("\\\\"))) {
85 // Network share paths are not resolved
86 return QUrl::fromLocalFile(localfile: line);
87 }
88
89 QUrl url(line);
90 if (url.scheme().isEmpty()) {
91 // Resolve it relative to root
92 if (root.isLocalFile())
93 return QUrl::fromUserInput(userInput: line, workingDirectory: root.adjusted(options: QUrl::RemoveFilename).toLocalFile(), options: QUrl::AssumeLocalFile);
94 else
95 return root.resolved(relative: url);
96 } else if (url.scheme().length() == 1) {
97 // Assume it's a drive letter for a Windows path
98 url = QUrl::fromLocalFile(localfile: line);
99 }
100
101 return url;
102 }
103
104 void newItemFound(const QVariant& content) { Q_EMIT m_parent->newItem(content); }
105
106private:
107 QPlaylistFileParser *m_parent;
108 bool m_aborted;
109};
110
111class M3UParser : public ParserBase
112{
113public:
114 explicit M3UParser(QPlaylistFileParser *q)
115 : ParserBase(q)
116 , m_extendedFormat(false)
117 {
118 }
119
120 /*
121 *
122 Extended M3U directives
123
124 #EXTM3U - header - must be first line of file
125 #EXTINF - extra info - length (seconds), title
126 #EXTINF - extra info - length (seconds), artist '-' title
127
128 Example
129
130 #EXTM3U
131 #EXTINF:123, Sample artist - Sample title
132 C:\Documents and Settings\I\My Music\Sample.mp3
133 #EXTINF:321,Example Artist - Example title
134 C:\Documents and Settings\I\My Music\Greatest Hits\Example.ogg
135
136 */
137 bool parseLineImpl(int lineIndex, const QString& line, const QUrl& root) override
138 {
139 if (line[0] == '#' ) {
140 if (m_extendedFormat) {
141 if (line.startsWith(s: QLatin1String("#EXTINF:"))) {
142 m_extraInfo.clear();
143 int artistStart = line.indexOf(s: QLatin1String(","), from: 8);
144 bool ok = false;
145 int length = line.midRef(position: 8, n: artistStart < 8 ? -1 : artistStart - 8).trimmed().toInt(ok: &ok);
146 if (ok && length > 0) {
147 //convert from second to milisecond
148 m_extraInfo[QMediaMetaData::Duration] = QVariant(length * 1000);
149 }
150 if (artistStart > 0) {
151 int titleStart = getSplitIndex(line, startPos: artistStart);
152 if (titleStart > artistStart) {
153 m_extraInfo[QMediaMetaData::Author] = line.midRef(position: artistStart + 1,
154 n: titleStart - artistStart - 1).trimmed().toString().
155 replace(before: QLatin1String("--"), after: QLatin1String("-"));
156 m_extraInfo[QMediaMetaData::Title] = line.midRef(position: titleStart + 1).trimmed().toString().
157 replace(before: QLatin1String("--"), after: QLatin1String("-"));
158 } else {
159 m_extraInfo[QMediaMetaData::Title] = line.midRef(position: artistStart + 1).trimmed().toString().
160 replace(before: QLatin1String("--"), after: QLatin1String("-"));
161 }
162 }
163 }
164 } else if (lineIndex == 0 && line.startsWith(s: QLatin1String("#EXTM3U"))) {
165 m_extendedFormat = true;
166 }
167 } else {
168 m_extraInfo[QLatin1String("url")] = expandToFullPath(root, line);
169 newItemFound(content: QVariant(m_extraInfo));
170 m_extraInfo.clear();
171 }
172
173 return true;
174 }
175
176 int getSplitIndex(const QString& line, int startPos)
177 {
178 if (startPos < 0)
179 startPos = 0;
180 const QChar* buf = line.data();
181 for (int i = startPos; i < line.length(); ++i) {
182 if (buf[i] == '-') {
183 if (i == line.length() - 1)
184 return i;
185 ++i;
186 if (buf[i] != '-')
187 return i - 1;
188 }
189 }
190 return -1;
191 }
192
193private:
194 QVariantMap m_extraInfo;
195 bool m_extendedFormat;
196};
197
198class PLSParser : public ParserBase
199{
200public:
201 explicit PLSParser(QPlaylistFileParser *q)
202 : ParserBase(q)
203 {
204 }
205
206/*
207 *
208The format is essentially that of an INI file structured as follows:
209
210Header
211
212 * [playlist] : This tag indicates that it is a Playlist File
213
214Track Entry
215Assuming track entry #X
216
217 * FileX : Variable defining location of stream.
218 * TitleX : Defines track title.
219 * LengthX : Length in seconds of track. Value of -1 indicates indefinite.
220
221Footer
222
223 * NumberOfEntries : This variable indicates the number of tracks.
224 * Version : Playlist version. Currently only a value of 2 is valid.
225
226[playlist]
227
228File1=Alternative\everclear - SMFTA.mp3
229
230Title1=Everclear - So Much For The Afterglow
231
232Length1=233
233
234File2=http://www.site.com:8000/listen.pls
235
236Title2=My Cool Stream
237
238Length5=-1
239
240NumberOfEntries=2
241
242Version=2
243*/
244 bool parseLineImpl(int, const QString &line, const QUrl &root) override
245 {
246 // We ignore everything but 'File' entries, since that's the only thing we care about.
247 if (!line.startsWith(s: QLatin1String("File")))
248 return true;
249
250 QString value = getValue(line);
251 if (value.isEmpty())
252 return true;
253
254 newItemFound(content: expandToFullPath(root, line: value));
255
256 return true;
257 }
258
259 QString getValue(const QString& line) {
260 int start = line.indexOf(c: '=');
261 if (start < 0)
262 return QString();
263 return line.midRef(position: start + 1).trimmed().toString();
264 }
265};
266}
267
268/////////////////////////////////////////////////////////////////////////////////////////////////
269
270class QPlaylistFileParserPrivate
271{
272 Q_DECLARE_PUBLIC(QPlaylistFileParser)
273public:
274 QPlaylistFileParserPrivate(QPlaylistFileParser *q)
275 : q_ptr(q)
276 , m_stream(nullptr)
277 , m_type(QPlaylistFileParser::UNKNOWN)
278 , m_scanIndex(0)
279 , m_lineIndex(-1)
280 , m_utf8(false)
281 , m_aborted(false)
282 {
283 }
284
285 void handleData();
286 void handleParserFinished();
287 void abort();
288 void reset();
289
290 QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> m_source;
291 QScopedPointer<ParserBase> m_currentParser;
292 QByteArray m_buffer;
293 QUrl m_root;
294 QNetworkAccessManager m_mgr;
295 QString m_mimeType;
296 QPlaylistFileParser *q_ptr;
297 QIODevice *m_stream;
298 QPlaylistFileParser::FileType m_type;
299 struct ParserJob
300 {
301 QIODevice *m_stream;
302 QMediaContent m_media;
303 QString m_mimeType;
304 bool isValid() const { return m_stream || !m_media.isNull(); }
305 void reset() { m_stream = nullptr; m_media = QMediaContent(); m_mimeType = QString(); }
306 } m_pendingJob;
307 int m_scanIndex;
308 int m_lineIndex;
309 bool m_utf8;
310 bool m_aborted;
311
312private:
313 bool processLine(int startIndex, int length);
314};
315
316#define LINE_LIMIT 4096
317#define READ_LIMIT 64
318
319bool QPlaylistFileParserPrivate::processLine(int startIndex, int length)
320{
321 Q_Q(QPlaylistFileParser);
322 m_lineIndex++;
323
324 if (!m_currentParser) {
325 const QString urlString = m_root.toString();
326 const QString &suffix = !urlString.isEmpty() ? QFileInfo(urlString).suffix() : urlString;
327 const QString &mimeType = m_source->header(header: QNetworkRequest::ContentTypeHeader).toString();
328 m_type = QPlaylistFileParser::findPlaylistType(suffix, mime: !mimeType.isEmpty() ? mimeType : m_mimeType, data: m_buffer.constData(), size: quint32(m_buffer.size()));
329
330 switch (m_type) {
331 case QPlaylistFileParser::UNKNOWN:
332 emit q->error(err: QPlaylistFileParser::FormatError,
333 errorMsg: QPlaylistFileParser::tr(s: "%1 playlist type is unknown").arg(a: m_root.toString()));
334 q->abort();
335 return false;
336 case QPlaylistFileParser::M3U:
337 m_currentParser.reset(other: new M3UParser(q));
338 break;
339 case QPlaylistFileParser::M3U8:
340 m_currentParser.reset(other: new M3UParser(q));
341 m_utf8 = true;
342 break;
343 case QPlaylistFileParser::PLS:
344 m_currentParser.reset(other: new PLSParser(q));
345 break;
346 }
347
348 Q_ASSERT(!m_currentParser.isNull());
349 }
350
351 QString line;
352
353 if (m_utf8) {
354 line = QString::fromUtf8(str: m_buffer.constData() + startIndex, size: length).trimmed();
355 } else {
356 line = QString::fromLatin1(str: m_buffer.constData() + startIndex, size: length).trimmed();
357 }
358 if (line.isEmpty())
359 return true;
360
361 Q_ASSERT(m_currentParser);
362 return m_currentParser->parseLine(lineIndex: m_lineIndex, line, root: m_root);
363}
364
365void QPlaylistFileParserPrivate::handleData()
366{
367 Q_Q(QPlaylistFileParser);
368 while (m_source->bytesAvailable() && !m_aborted) {
369 int expectedBytes = qMin(READ_LIMIT, b: int(qMin(a: m_source->bytesAvailable(),
370 b: qint64(LINE_LIMIT - m_buffer.size()))));
371 m_buffer.push_back(a: m_source->read(maxlen: expectedBytes));
372 int processedBytes = 0;
373 while (m_scanIndex < m_buffer.length() && !m_aborted) {
374 char s = m_buffer[m_scanIndex];
375 if (s == '\r' || s == '\n') {
376 int l = m_scanIndex - processedBytes;
377 if (l > 0) {
378 if (!processLine(startIndex: processedBytes, length: l))
379 break;
380 }
381 processedBytes = m_scanIndex + 1;
382 if (!m_source) {
383 //some error happened, so exit parsing
384 return;
385 }
386 }
387 m_scanIndex++;
388 }
389
390 if (m_aborted)
391 break;
392
393 if (m_buffer.length() - processedBytes >= LINE_LIMIT) {
394 emit q->error(err: QPlaylistFileParser::FormatError, errorMsg: QPlaylistFileParser::tr(s: "invalid line in playlist file"));
395 q->abort();
396 break;
397 }
398
399 if (m_source->isFinished() && !m_source->bytesAvailable()) {
400 //last line
401 processLine(startIndex: processedBytes, length: -1);
402 break;
403 }
404
405 Q_ASSERT(m_buffer.length() == m_scanIndex);
406 if (processedBytes == 0)
407 continue;
408
409 int copyLength = m_buffer.length() - processedBytes;
410 if (copyLength > 0) {
411 Q_ASSERT(copyLength <= READ_LIMIT);
412 m_buffer = m_buffer.right(len: copyLength);
413 } else {
414 m_buffer.clear();
415 }
416 m_scanIndex = 0;
417 }
418
419 handleParserFinished();
420}
421
422QPlaylistFileParser::QPlaylistFileParser(QObject *parent)
423 : QObject(parent)
424 , d_ptr(new QPlaylistFileParserPrivate(this))
425{
426
427}
428
429QPlaylistFileParser::~QPlaylistFileParser()
430{
431
432}
433
434QPlaylistFileParser::FileType QPlaylistFileParser::findByMimeType(const QString &mime)
435{
436 if (mime == QLatin1String("text/uri-list") || mime == QLatin1String("audio/x-mpegurl") || mime == QLatin1String("audio/mpegurl"))
437 return QPlaylistFileParser::M3U;
438
439 if (mime == QLatin1String("application/x-mpegURL") || mime == QLatin1String("application/vnd.apple.mpegurl"))
440 return QPlaylistFileParser::M3U8;
441
442 if (mime == QLatin1String("audio/x-scpls"))
443 return QPlaylistFileParser::PLS;
444
445 return QPlaylistFileParser::UNKNOWN;
446}
447
448QPlaylistFileParser::FileType QPlaylistFileParser::findBySuffixType(const QString &suffix)
449{
450 const QString &s = suffix.toLower();
451
452 if (s == QLatin1String("m3u"))
453 return QPlaylistFileParser::M3U;
454
455 if (s == QLatin1String("m3u8"))
456 return QPlaylistFileParser::M3U8;
457
458 if (s == QLatin1String("pls"))
459 return QPlaylistFileParser::PLS;
460
461 return QPlaylistFileParser::UNKNOWN;
462}
463
464QPlaylistFileParser::FileType QPlaylistFileParser::findByDataHeader(const char *data, quint32 size)
465{
466 if (!data || size == 0)
467 return QPlaylistFileParser::UNKNOWN;
468
469 if (size >= 7 && strncmp(s1: data, s2: "#EXTM3U", n: 7) == 0)
470 return QPlaylistFileParser::M3U;
471
472 if (size >= 10 && strncmp(s1: data, s2: "[playlist]", n: 10) == 0)
473 return QPlaylistFileParser::PLS;
474
475 return QPlaylistFileParser::UNKNOWN;
476}
477
478QPlaylistFileParser::FileType QPlaylistFileParser::findPlaylistType(const QString& suffix,
479 const QString& mime,
480 const char *data,
481 quint32 size)
482{
483
484 FileType dataHeaderType = findByDataHeader(data, size);
485 if (dataHeaderType != UNKNOWN)
486 return dataHeaderType;
487
488 FileType mimeType = findByMimeType(mime);
489 if (mimeType != UNKNOWN)
490 return mimeType;
491
492 FileType suffixType = findBySuffixType(suffix);
493 if (suffixType != UNKNOWN)
494 return suffixType;
495
496 return UNKNOWN;
497}
498
499/*
500 * Delegating
501 */
502void QPlaylistFileParser::start(const QMediaContent &media, QIODevice *stream, const QString &mimeType)
503{
504 if (stream)
505 start(stream, mimeType);
506 else
507 start(request: media.request(), mimeType);
508}
509
510void QPlaylistFileParser::start(QIODevice *stream, const QString &mimeType)
511{
512 Q_D(QPlaylistFileParser);
513 const bool validStream = stream ? (stream->isOpen() && stream->isReadable()) : false;
514
515 if (!validStream) {
516 Q_EMIT error(err: ResourceError, errorMsg: tr(s: "Invalid stream"));
517 return;
518 }
519
520 if (!d->m_currentParser.isNull()) {
521 abort();
522 d->m_pendingJob = { .m_stream: stream, .m_media: QUrl(), .m_mimeType: mimeType };
523 return;
524 }
525
526 d->reset();
527 d->m_mimeType = mimeType;
528 d->m_stream = stream;
529 connect(sender: d->m_stream, SIGNAL(readyRead()), receiver: this, SLOT(_q_handleData()));
530 d->handleData();
531}
532
533void QPlaylistFileParser::start(const QNetworkRequest& request, const QString &mimeType)
534{
535 Q_D(QPlaylistFileParser);
536 const QUrl &url = request.url();
537
538 if (url.isLocalFile() && !QFile::exists(fileName: url.toLocalFile())) {
539 emit error(err: ResourceError, errorMsg: QString(tr(s: "%1 does not exist")).arg(a: url.toString()));
540 return;
541 }
542
543 if (!d->m_currentParser.isNull()) {
544 abort();
545 d->m_pendingJob = { .m_stream: nullptr, .m_media: request, .m_mimeType: mimeType };
546 return;
547 }
548
549 d->reset();
550 d->m_root = url;
551 d->m_mimeType = mimeType;
552 d->m_source.reset(other: d->m_mgr.get(request));
553 connect(sender: d->m_source.data(), SIGNAL(readyRead()), receiver: this, SLOT(handleData()));
554 connect(sender: d->m_source.data(), SIGNAL(finished()), receiver: this, SLOT(handleData()));
555 connect(sender: d->m_source.data(), SIGNAL(errorOccurred(QNetworkReply::NetworkError)), receiver: this, SLOT(handleError()));
556
557 if (url.isLocalFile())
558 d->handleData();
559}
560
561void QPlaylistFileParser::abort()
562{
563 Q_D(QPlaylistFileParser);
564 d->abort();
565
566 if (d->m_source)
567 d->m_source->disconnect();
568
569 if (d->m_stream)
570 disconnect(sender: d->m_stream, SIGNAL(readyRead()), receiver: this, SLOT(handleData()));
571}
572
573void QPlaylistFileParser::handleData()
574{
575 Q_D(QPlaylistFileParser);
576 d->handleData();
577}
578
579void QPlaylistFileParserPrivate::handleParserFinished()
580{
581 Q_Q(QPlaylistFileParser);
582 const bool isParserValid = !m_currentParser.isNull();
583 if (!isParserValid && !m_aborted)
584 emit q->error(err: QPlaylistFileParser::FormatNotSupportedError, errorMsg: QPlaylistFileParser::tr(s: "Empty file provided"));
585
586 if (isParserValid && !m_aborted) {
587 m_currentParser.reset();
588 emit q->finished();
589 }
590
591 if (!m_aborted)
592 q->abort();
593
594 if (!m_source.isNull())
595 m_source.reset();
596
597 if (m_pendingJob.isValid())
598 q->start(media: m_pendingJob.m_media, stream: m_pendingJob.m_stream, mimeType: m_pendingJob.m_mimeType);
599}
600
601void QPlaylistFileParserPrivate::abort()
602{
603 m_aborted = true;
604 if (!m_currentParser.isNull())
605 m_currentParser->abort();
606}
607
608void QPlaylistFileParserPrivate::reset()
609{
610 Q_ASSERT(m_currentParser.isNull());
611 Q_ASSERT(m_source.isNull());
612 m_buffer.clear();
613 m_root.clear();
614 m_mimeType.clear();
615 m_stream = 0;
616 m_type = QPlaylistFileParser::UNKNOWN;
617 m_scanIndex = 0;
618 m_lineIndex = -1;
619 m_utf8 = false;
620 m_aborted = false;
621 m_pendingJob.reset();
622}
623
624void QPlaylistFileParser::handleError()
625{
626 Q_D(QPlaylistFileParser);
627 const QString &errorString = d->m_source->errorString();
628 Q_EMIT error(err: QPlaylistFileParser::NetworkError, errorMsg: errorString);
629 abort();
630}
631
632QT_END_NAMESPACE
633

source code of qtmultimedia/src/multimedia/playback/qplaylistfileparser.cpp