diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f4b14c..d061e78 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.12) project(pineapple-music LANGUAGES CXX) @@ -10,10 +10,10 @@ set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt5 COMPONENTS Widgets Multimedia Network LinguistTools REQUIRED) +find_package(Qt6 6.4 COMPONENTS Widgets Multimedia Network LinguistTools REQUIRED) find_package(PkgConfig) if (PKG_CONFIG_FOUND) @@ -26,6 +26,9 @@ set (PMUSIC_CPP_FILES seekableslider.cpp playlistmodel.cpp singleapplicationmanager.cpp + + qt/qplaylistfileparser.cpp + qt/qmediaplaylist.cpp ) set (PMUSIC_HEADER_FILES @@ -33,6 +36,10 @@ set (PMUSIC_HEADER_FILES seekableslider.h playlistmodel.h singleapplicationmanager.h + + qt/qplaylistfileparser_p.h + qt/qmediaplaylist.h + qt/qmediaplaylist_p.h ) set (PMUSIC_UI_FILES @@ -45,7 +52,7 @@ set (EXE_NAME pmusic) file (GLOB PMUSIC_TS_FILES languages/*.ts) set (PMUSIC_CPP_FILES_FOR_I18N ${PMUSIC_CPP_FILES} ${PMUSIC_UI_FILES}) -qt5_create_translation(PMUSIC_QM_FILES ${PMUSIC_CPP_FILES_FOR_I18N} ${PMUSIC_TS_FILES}) +qt_create_translation(PMUSIC_QM_FILES ${PMUSIC_CPP_FILES_FOR_I18N} ${PMUSIC_TS_FILES}) add_executable(${EXE_NAME} ${PMUSIC_HEADER_FILES} @@ -68,7 +75,7 @@ if (NOT TagLib_FOUND) endif () target_include_directories(${EXE_NAME} PRIVATE ${TagLib_INCLUDE_DIRS}) -target_link_libraries(${EXE_NAME} PRIVATE Qt5::Widgets Qt5::Multimedia Qt5::Network ${TagLib_LINK_LIBRARIES}) +target_link_libraries(${EXE_NAME} PRIVATE Qt::Widgets Qt::Multimedia Qt::Network ${TagLib_LINK_LIBRARIES}) # Extra build settings if (WIN32) diff --git a/FlacPic.h b/FlacPic.h index 2bb6462..148e858 100755 --- a/FlacPic.h +++ b/FlacPic.h @@ -19,7 +19,6 @@ ShadowPower 于2014/8/1 夜间 #include typedef unsigned char byte; -using namespace std; namespace spFLAC { //Flac元数据块头部结构体定义 diff --git a/ID3v2Pic.h b/ID3v2Pic.h index 59028c2..c86910c 100755 --- a/ID3v2Pic.h +++ b/ID3v2Pic.h @@ -19,7 +19,6 @@ ShadowPower 于2014/8/1 上午 #include typedef unsigned char byte; -using namespace std; namespace spID3 { //ID3v2标签头部结构体定义 diff --git a/appveyor.yml b/appveyor.yml index ad6149e..ad0bdc6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,16 +5,16 @@ environment: PACKAGE_INSTALL_ROOT: C:\projects\pir PKG_CONFIG_PATH: C:\projects\pir\lib\pkgconfig matrix: - - build_name: mingw81_64_qt5_15_2 - QTPATH: C:\Qt\5.15.2\mingw81_64 - MINGW64: C:\Qt\Tools\mingw810_64 + - build_name: mingw1120_64_qt6_4 + QTPATH: C:\Qt\6.4\mingw_64 + MINGW64: C:\Qt\Tools\mingw1120_64 install: - mkdir %CMAKE_INSTALL_ROOT% - mkdir %PACKAGE_INSTALL_ROOT% - cd %APPVEYOR_BUILD_FOLDER% - git submodule update --init --recursive - - set PATH=%PATH%;%CMAKE_INSTALL_ROOT%;%QTPATH%\bin;%MINGW32%\bin + - set PATH=%PATH%;%CMAKE_INSTALL_ROOT%;%QTPATH%\bin;%MINGW64%\bin - set CC=%MINGW64%\bin\gcc.exe - set CXX=%MINGW64%\bin\g++.exe @@ -26,7 +26,7 @@ build_script: # build taglib - cd 3rdparty - git clone -q https://github.com/taglib/taglib.git - - cd taglib + - cd taglib - cmake -G "Ninja" . -DCMAKE_INSTALL_PREFIX=%PACKAGE_INSTALL_ROOT% -DBUILD_SHARED_LIBS=ON - cmake --build . - cmake --build . --target install @@ -40,8 +40,11 @@ build_script: - cmake --build . --target install # fixme: I don't know how to NOT make the binary installed to the ./bin/ folder... - cd bin - - windeployqt --verbose=2 --no-quick-import --no-translations --no-opengl-sw --no-angle --no-system-d3d-compiler .\pmusic.exe + - windeployqt --verbose=2 --no-quick-import --no-translations --no-opengl-sw --compiler-runtime --no-system-d3d-compiler --multimedia .\pmusic.exe - xcopy /s %PACKAGE_INSTALL_ROOT%\bin %cd% +# don't know why windeployqt doesn't copy the multimedia plugin dir... + - mkdir multimedia + - copy %QTPATH%\plugins\multimedia\windowsmediaplugin.dll multimedia\windowsmediaplugin.dll # for debug.. - tree /f diff --git a/languages/pineapple-music.ts b/languages/pineapple-music.ts index e1754fa..647f4aa 100644 --- a/languages/pineapple-music.ts +++ b/languages/pineapple-music.ts @@ -4,61 +4,109 @@ MainWindow + Mono + Stereo + %1 Channels + Select songs to play + Audio Files + Pineapple Player + ^ + No song loaded... + Drag and drop file to load + + 0:00 + Sample Rate: %1 Hz + Bitrate: %1 Kbps + Channel Count: %1 + + QMediaPlaylist + + + The file could not be accessed. + + + + + %1 playlist type is unknown + + + + + invalid line in playlist file + + + + + Invalid stream + + + + + %1 does not exist + + + + + Empty file provided + + + main + File list. diff --git a/languages/pineapple-music_zh_CN.ts b/languages/pineapple-music_zh_CN.ts index 1351c62..83dbda1 100644 --- a/languages/pineapple-music_zh_CN.ts +++ b/languages/pineapple-music_zh_CN.ts @@ -4,61 +4,109 @@ MainWindow + Mono 单声道 + Stereo 立体声 + %1 Channels %1 声道 + Select songs to play 选择要播放的曲目 + Audio Files 音频文件 + Pineapple Player + ^ + No song loaded... 未加载曲目... + Drag and drop file to load 拖放文件来播放 + + 0:00 + Sample Rate: %1 Hz 采样率: %1 Hz + Bitrate: %1 Kbps 比特率: %1 Kbps + Channel Count: %1 声道数: %1 + + QMediaPlaylist + + + The file could not be accessed. + + + + + %1 playlist type is unknown + + + + + invalid line in playlist file + + + + + Invalid stream + + + + + %1 does not exist + + + + + Empty file provided + + + main + File list. 文件列表 diff --git a/mainwindow.cpp b/mainwindow.cpp index 4b97e85..cf4bebe 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -2,6 +2,7 @@ #include "./ui_mainwindow.h" #include "playlistmodel.h" +#include "qt/qmediaplaylist.h" #include "ID3v2Pic.h" #include "FlacPic.h" @@ -13,7 +14,7 @@ #include #include -#include +#include #include #include #include @@ -27,10 +28,13 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_mediaPlayer(new QMediaPlayer(this)) + , m_audioOutput(new QAudioOutput(this)) , m_playlistModel(new PlaylistModel(this)) { ui->setupUi(this); + m_mediaPlayer->setAudioOutput(m_audioOutput); + this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | Qt::WindowMinimizeButtonHint); this->setAttribute(Qt::WA_TranslucentBackground, true); @@ -88,8 +92,7 @@ void MainWindow::loadPlaylistBySingleLocalFile(const QString &path) currentFileIndex = 0; } - QMediaPlaylist * playlist = createPlaylist(urlList); - playlist->setCurrentIndex(currentFileIndex); + createPlaylist(urlList); } void MainWindow::setAudioPropertyInfoForDisplay(int sampleRate, int bitrate, int channelCount, QString audioExt) @@ -246,22 +249,11 @@ void MainWindow::loadFile() /* * The returned QMediaPlaylist* ownership belongs to the internal QMediaPlayer instance. */ -QMediaPlaylist *MainWindow::createPlaylist(QList urlList) +void MainWindow::createPlaylist(QList urlList) { - QMediaPlaylist * oldPlaylist = m_mediaPlayer->playlist(); - QMediaPlaylist * playlist = new QMediaPlaylist(m_mediaPlayer); - - if (oldPlaylist) { - oldPlaylist->disconnect(); - oldPlaylist->deleteLater(); - } - - for (const QUrl & url : urlList) { - bool succ = playlist->addMedia(QMediaContent(url)); - if (!succ) { - qDebug("!!!!!!!!! break point time !!!!!!!!!"); - } - } + QMediaPlaylist* playlist = m_playlistModel->playlist(); + playlist->clear(); + playlist->addMedia(urlList); connect(playlist, &QMediaPlaylist::playbackModeChanged, this, [=](QMediaPlaylist::PlaybackMode mode) { switch (mode) { @@ -274,19 +266,16 @@ QMediaPlaylist *MainWindow::createPlaylist(QList urlList) case QMediaPlaylist::Sequential: ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-normal.png")); break; - case QMediaPlaylist::Random: - ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-shuffle.png")); - break; +// case QMediaPlaylist::Random: +// ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-shuffle.png")); +// break; default: break; } }); playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop); - m_mediaPlayer->setPlaylist(playlist); - m_playlistModel->setPlaylist(playlist); - - return playlist; + playlist->setCurrentIndex(0); } void MainWindow::centerWindow() @@ -314,8 +303,8 @@ void MainWindow::on_playBtn_clicked() } else if (m_mediaPlayer->mediaStatus() == QMediaPlayer::InvalidMedia) { ui->propLabel->setText("Error: InvalidMedia"); } else { - if (QList {QMediaPlayer::PausedState, QMediaPlayer::StoppedState} - .contains(m_mediaPlayer->state())) { + if (QList {QMediaPlayer::PausedState, QMediaPlayer::StoppedState} + .contains(m_mediaPlayer->playbackState())) { m_mediaPlayer->play(); } else { m_mediaPlayer->pause(); @@ -348,10 +337,10 @@ QList MainWindow::strlst2urllst(QStringList strlst) void MainWindow::on_volumeSlider_valueChanged(int value) { - if (m_mediaPlayer->isMuted()) { - m_mediaPlayer->setMuted(false); + if (m_audioOutput->isMuted()) { + m_audioOutput->setMuted(false); } - m_mediaPlayer->setVolume(value); + m_audioOutput->setVolume(value); } void MainWindow::on_stopBtn_clicked() @@ -371,30 +360,30 @@ void MainWindow::on_prevBtn_clicked() { // QMediaPlaylist::previous() won't work when in CurrentItemInLoop playmode, // and also works not as intended when in other playmode, so do it manually... - QMediaPlaylist * playlist = m_mediaPlayer->playlist(); + QMediaPlaylist * playlist = m_playlistModel->playlist(); if (playlist) { int index = playlist->currentIndex(); int count = playlist->mediaCount(); - m_mediaPlayer->playlist()->setCurrentIndex(index == 0 ? count - 1 : index - 1); + playlist->setCurrentIndex(index == 0 ? count - 1 : index - 1); } } void MainWindow::on_nextBtn_clicked() { // see also: MainWindow::on_prevBtn_clicked() - QMediaPlaylist * playlist = m_mediaPlayer->playlist(); + QMediaPlaylist * playlist = m_playlistModel->playlist(); if (playlist) { int index = playlist->currentIndex(); int count = playlist->mediaCount(); - m_mediaPlayer->playlist()->setCurrentIndex(index == (count - 1) ? 0 : index + 1); + playlist->setCurrentIndex(index == (count - 1) ? 0 : index + 1); } } void MainWindow::on_volumeBtn_clicked() { - m_mediaPlayer->setMuted(!m_mediaPlayer->isMuted()); + m_audioOutput->setMuted(!m_audioOutput->isMuted()); } void MainWindow::on_minimumWindowBtn_clicked() @@ -424,13 +413,12 @@ void MainWindow::initUiAndAnimation() void MainWindow::initConnections() { - connect(m_mediaPlayer, &QMediaPlayer::currentMediaChanged, this, [=](const QMediaContent &media) { -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - QUrl fileUrl = media.canonicalUrl(); -#else - QUrl fileUrl = media.request().url(); -#endif // QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - + connect(m_playlistModel->playlist(), &QMediaPlaylist::currentIndexChanged, this, [=](int currentItem) { + bool isPlaying = m_mediaPlayer->playbackState() == QMediaPlayer::PlayingState; + m_mediaPlayer->setSource(m_playlistModel->playlist()->currentMedia()); + if (isPlaying) m_mediaPlayer->play(); + }); + connect(m_playlistModel->playlist(), &QMediaPlaylist::currentMediaChanged, this, [=](const QUrl &fileUrl) { ui->titleLabel->setText(fileUrl.fileName()); ui->titleLabel->setToolTip(fileUrl.fileName()); @@ -489,7 +477,7 @@ void MainWindow::initConnections() } }); - connect(m_mediaPlayer, &QMediaPlayer::mutedChanged, this, [=](bool muted) { + connect(m_audioOutput, &QAudioOutput::mutedChanged, this, [=](bool muted) { if (muted) { ui->volumeBtn->setIcon(QIcon(":/icons/icons/audio-volume-muted.png")); } else { @@ -501,7 +489,7 @@ void MainWindow::initConnections() ui->totalTimeLabel->setText(ms2str(dua)); }); - connect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, [=](QMediaPlayer::State newState) { + connect(m_mediaPlayer, &QMediaPlayer::playbackStateChanged, this, [=](QMediaPlayer::PlaybackState newState) { switch (newState) { case QMediaPlayer::PlayingState: ui->playBtn->setIcon(QIcon(":/icons/icons/media-playback-pause.png")); @@ -513,23 +501,23 @@ void MainWindow::initConnections() } }); - connect(m_mediaPlayer, &QMediaPlayer::volumeChanged, this, [=](int vol) { + connect(m_audioOutput, &QAudioOutput::volumeChanged, this, [=](int vol) { ui->volumeSlider->setValue(vol); }); - connect(m_mediaPlayer, static_cast(&QMediaPlayer::error), - this, [=](QMediaPlayer::Error error) { - switch (error) { - default: - break; - } - qDebug("%s aaaaaaaaaaaaa", m_mediaPlayer->errorString().toUtf8().data()); - }); +// connect(m_mediaPlayer, static_cast(&QMediaPlayer::error), +// this, [=](QMediaPlayer::Error error) { +// switch (error) { +// default: +// break; +// } +// qDebug("%s aaaaaaaaaaaaa", m_mediaPlayer->errorString().toUtf8().data()); +// }); } void MainWindow::on_playbackModeBtn_clicked() { - QMediaPlaylist * playlist = m_mediaPlayer->playlist(); + QMediaPlaylist * playlist = m_playlistModel->playlist(); if (!playlist) return; switch (playlist->playbackMode()) { @@ -540,11 +528,11 @@ void MainWindow::on_playbackModeBtn_clicked() playlist->setPlaybackMode(QMediaPlaylist::Sequential); break; case QMediaPlaylist::Sequential: - playlist->setPlaybackMode(QMediaPlaylist::Random); - break; - case QMediaPlaylist::Random: playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop); break; +// case QMediaPlaylist::Random: +// playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop); +// break; default: break; } diff --git a/mainwindow.h b/mainwindow.h index c48ba71..2998fbc 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -8,7 +8,7 @@ QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } class QMediaPlayer; -class QMediaPlaylist; +class QAudioOutput; class QPropertyAnimation; QT_END_NAMESPACE @@ -40,7 +40,7 @@ protected: void loadFile(); void centerWindow(); - QMediaPlaylist *createPlaylist(QList urlList); + void createPlaylist(QList urlList); private slots: void on_playbackModeBtn_clicked(); @@ -63,6 +63,7 @@ private: Ui::MainWindow *ui; QMediaPlayer *m_mediaPlayer; + QAudioOutput *m_audioOutput; QPropertyAnimation *m_fadeOutAnimation; PlaylistModel *m_playlistModel = nullptr; // TODO: move playback logic to player.cpp diff --git a/playlistmodel.cpp b/playlistmodel.cpp index 8540813..d05b10c 100644 --- a/playlistmodel.cpp +++ b/playlistmodel.cpp @@ -1,67 +1,24 @@ -/**************************************************************************** -** -** Copyright (C) 2017 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the examples of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:BSD$ -** 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. -** -** BSD License Usage -** Alternatively, you may use this file under the terms of the BSD license -** as follows: -** -** "Redistribution and use in source and binary forms, with or without -** modification, are permitted provided that the following conditions are -** met: -** * Redistributions of source code must retain the above copyright -** notice, this list of conditions and the following disclaimer. -** * Redistributions in binary form must reproduce the above copyright -** notice, this list of conditions and the following disclaimer in -** the documentation and/or other materials provided with the -** distribution. -** * Neither the name of The Qt Company Ltd nor the names of its -** contributors may be used to endorse or promote products derived -** from this software without specific prior written permission. -** -** -** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause #include "playlistmodel.h" +#include "qt/qmediaplaylist.h" #include #include -#include PlaylistModel::PlaylistModel(QObject *parent) : QAbstractItemModel(parent) { + m_playlist.reset(new QMediaPlaylist); + connect(m_playlist.data(), &QMediaPlaylist::mediaAboutToBeInserted, this, &PlaylistModel::beginInsertItems); + connect(m_playlist.data(), &QMediaPlaylist::mediaInserted, this, &PlaylistModel::endInsertItems); + connect(m_playlist.data(), &QMediaPlaylist::mediaAboutToBeRemoved, this, &PlaylistModel::beginRemoveItems); + connect(m_playlist.data(), &QMediaPlaylist::mediaRemoved, this, &PlaylistModel::endRemoveItems); + connect(m_playlist.data(), &QMediaPlaylist::mediaChanged, this, &PlaylistModel::changeItems); } -PlaylistModel::~PlaylistModel() -{ -} +PlaylistModel::~PlaylistModel() = default; int PlaylistModel::rowCount(const QModelIndex &parent) const { @@ -94,11 +51,7 @@ QVariant PlaylistModel::data(const QModelIndex &index, int role) const if (index.isValid() && role == Qt::DisplayRole) { QVariant value = m_data[index]; if (!value.isValid() && index.column() == Title) { -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - QUrl location = m_playlist->media(index.row()).canonicalUrl(); -#else - QUrl location = m_playlist->media(index.row()).request().url(); -#endif + QUrl location = m_playlist->media(index.row()); return QFileInfo(location.path()).fileName(); } @@ -109,31 +62,7 @@ QVariant PlaylistModel::data(const QModelIndex &index, int role) const QMediaPlaylist *PlaylistModel::playlist() const { - return m_playlist; -} - -void PlaylistModel::setPlaylist(QMediaPlaylist *playlist) -{ - if (m_playlist) { - disconnect(m_playlist, &QMediaPlaylist::mediaAboutToBeInserted, this, &PlaylistModel::beginInsertItems); - disconnect(m_playlist, &QMediaPlaylist::mediaInserted, this, &PlaylistModel::endInsertItems); - disconnect(m_playlist, &QMediaPlaylist::mediaAboutToBeRemoved, this, &PlaylistModel::beginRemoveItems); - disconnect(m_playlist, &QMediaPlaylist::mediaRemoved, this, &PlaylistModel::endRemoveItems); - disconnect(m_playlist, &QMediaPlaylist::mediaChanged, this, &PlaylistModel::changeItems); - } - - beginResetModel(); - m_playlist = playlist; - - if (m_playlist) { - connect(m_playlist, &QMediaPlaylist::mediaAboutToBeInserted, this, &PlaylistModel::beginInsertItems); - connect(m_playlist, &QMediaPlaylist::mediaInserted, this, &PlaylistModel::endInsertItems); - connect(m_playlist, &QMediaPlaylist::mediaAboutToBeRemoved, this, &PlaylistModel::beginRemoveItems); - connect(m_playlist, &QMediaPlaylist::mediaRemoved, this, &PlaylistModel::endRemoveItems); - connect(m_playlist, &QMediaPlaylist::mediaChanged, this, &PlaylistModel::changeItems); - } - - endResetModel(); + return m_playlist.data(); } bool PlaylistModel::setData(const QModelIndex &index, const QVariant &value, int role) diff --git a/playlistmodel.h b/playlistmodel.h index 7957a95..6c20cc1 100644 --- a/playlistmodel.h +++ b/playlistmodel.h @@ -1,59 +1,15 @@ -/**************************************************************************** -** -** Copyright (C) 2017 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the examples of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:BSD$ -** 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. -** -** BSD License Usage -** Alternatively, you may use this file under the terms of the BSD license -** as follows: -** -** "Redistribution and use in source and binary forms, with or without -** modification, are permitted provided that the following conditions are -** met: -** * Redistributions of source code must retain the above copyright -** notice, this list of conditions and the following disclaimer. -** * Redistributions in binary form must reproduce the above copyright -** notice, this list of conditions and the following disclaimer in -** the documentation and/or other materials provided with the -** distribution. -** * Neither the name of The Qt Company Ltd nor the names of its -** contributors may be used to endorse or promote products derived -** from this software without specific prior written permission. -** -** -** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause #ifndef PLAYLISTMODEL_H #define PLAYLISTMODEL_H #include +#include +QT_BEGIN_NAMESPACE class QMediaPlaylist; +QT_END_NAMESPACE class PlaylistModel : public QAbstractItemModel { @@ -67,7 +23,7 @@ public: }; explicit PlaylistModel(QObject *parent = nullptr); - ~PlaylistModel() override; + ~PlaylistModel(); int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; @@ -78,7 +34,6 @@ public: QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QMediaPlaylist *playlist() const; - void setPlaylist(QMediaPlaylist *playlist); bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::DisplayRole) override; @@ -90,7 +45,7 @@ private slots: void changeItems(int start, int end); private: - QMediaPlaylist * m_playlist = nullptr; + QScopedPointer m_playlist; QMap m_data; }; diff --git a/qt/qmediaplaylist.cpp b/qt/qmediaplaylist.cpp new file mode 100644 index 0000000..5297208 --- /dev/null +++ b/qt/qmediaplaylist.cpp @@ -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 +#include +#include +#include +#include +#include + +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 &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 &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 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" diff --git a/qt/qmediaplaylist.h b/qt/qmediaplaylist.h new file mode 100644 index 0000000..94846d9 --- /dev/null +++ b/qt/qmediaplaylist.h @@ -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 + +#include +#include + + +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 &items); + bool insertMedia(int index, const QUrl &content); + bool insertMedia(int index, const QList &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 diff --git a/qt/qmediaplaylist_p.h b/qt/qmediaplaylist_p.h new file mode 100644 index 0000000..b0a6609 --- /dev/null +++ b/qt/qmediaplaylist_p.h @@ -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 + +#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 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 diff --git a/qt/qplaylistfileparser.cpp b/qt/qplaylistfileparser.cpp new file mode 100644 index 0000000..698f81d --- /dev/null +++ b/qt/qplaylistfileparser.cpp @@ -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 +#include +#include +#include +#include +#include +#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 m_source; + QScopedPointer m_currentParser; + QByteArray m_buffer; + QUrl m_root; + QNetworkAccessManager m_mgr; + QString m_mimeType; + QPlaylistFileParser *q_ptr; + QPointer 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 diff --git a/qt/qplaylistfileparser_p.h b/qt/qplaylistfileparser_p.h new file mode 100644 index 0000000..3d20167 --- /dev/null +++ b/qt/qplaylistfileparser_p.h @@ -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 + +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 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 d_ptr; +}; + +QT_END_NAMESPACE + +#endif // PLAYLISTFILEPARSER_P_H diff --git a/singleapplicationmanager.cpp b/singleapplicationmanager.cpp index 1b597e6..4be5fd2 100644 --- a/singleapplicationmanager.cpp +++ b/singleapplicationmanager.cpp @@ -1,5 +1,6 @@ #include "singleapplicationmanager.h" +#include #include #include #include