diff --git a/app/ui/tools/audio/AudioControlBar.qml b/app/ui/tools/audio/AudioControlBar.qml index 2b7085de..e96bd637 100644 --- a/app/ui/tools/audio/AudioControlBar.qml +++ b/app/ui/tools/audio/AudioControlBar.qml @@ -41,7 +41,15 @@ ToolBar { // Play Pause CustomToolBarButton { anchors.margins: 0 - iconText: AudioTool.isPaused ? FontAwesome.circlePlay : FontAwesome.circlePause + iconText: switch(AudioTool.playbackState){ + case AudioPlayer.Playing: + return FontAwesome.circlePause + case AudioPlayer.Loading: + return FontAwesome.spinner + default: + return FontAwesome.circlePlay + } + onClicked: AudioTool.playPause() pointSize: 24 } diff --git a/app/ui/tools/audio/MetaDataDisplay.qml b/app/ui/tools/audio/MetaDataDisplay.qml index 58724998..f7b8d7f4 100644 --- a/app/ui/tools/audio/MetaDataDisplay.qml +++ b/app/ui/tools/audio/MetaDataDisplay.qml @@ -10,7 +10,7 @@ Column { anchors.left: parent.left anchors.right: parent.right anchors.rightMargin: 5 - text: AudioTool.metaData.type + text: AudioTool.currentElementName font.bold: true } @@ -27,7 +27,7 @@ Column { anchors.left: parent.left anchors.right: parent.right anchors.rightMargin: 5 - text: AudioTool.metaData.artist + text: AudioTool.metaData.artist.join(", ") } CustomScrollLabel { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 19b77a07..c629d71a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -29,6 +29,8 @@ set(SRC_AUDIO tools/audio/editor/unsplash/unsplashparser.cpp tools/audio/players/audioplayer.h tools/audio/players/audioplayer.cpp + tools/audio/players/bufferedaudioplayer.h + tools/audio/players/bufferedaudioplayer.cpp tools/audio/players/musicplayer.h tools/audio/players/musicplayer.cpp tools/audio/players/soundplayer.h @@ -41,6 +43,10 @@ set(SRC_AUDIO tools/audio/players/spotifyplayer.cpp tools/audio/playlist/audioplaylist.h tools/audio/playlist/audioplaylist.cpp + tools/audio/playlist/audioplaylistfactory.h + tools/audio/playlist/audioplaylistfactory.cpp + tools/audio/playlist/resolvingaudioplaylist.h + tools/audio/playlist/resolvingaudioplaylist.cpp tools/audio/project/audioproject.h tools/audio/project/audioproject.cpp tools/audio/project/audiocategory.h diff --git a/src/common/utils/utils.h b/src/common/utils/utils.h index 980c8845..83693f94 100755 --- a/src/common/utils/utils.h +++ b/src/common/utils/utils.h @@ -24,7 +24,7 @@ class Utils return list; } - template static auto isInBounds(const QList &list, int index) -> bool + template static auto isInBounds(const QList &list, qsizetype index) -> bool { return index > -1 && index < list.length(); } diff --git a/src/services/spotify/data/spotifytrack.cpp b/src/services/spotify/data/spotifytrack.cpp index d118dac1..1a3b20ef 100644 --- a/src/services/spotify/data/spotifytrack.cpp +++ b/src/services/spotify/data/spotifytrack.cpp @@ -55,7 +55,7 @@ auto SpotifyTrack::fromJsonArray(const QByteArray &data) -> std::vector QString +auto SpotifyTrack::artistNames() const -> QStringList { QStringList names; names.reserve(artists.count()); @@ -64,8 +64,12 @@ auto SpotifyTrack::artistString() const -> QString { names << artist->name; } + return names; +} - return names.join(u", "_s); +auto SpotifyTrack::artistString() const -> QString +{ + return artistNames().join(u", "_s); } auto SpotifyTrack::image() const -> QSharedPointer diff --git a/src/services/spotify/data/spotifytrack.h b/src/services/spotify/data/spotifytrack.h index 2bab0d5b..8c9822f7 100644 --- a/src/services/spotify/data/spotifytrack.h +++ b/src/services/spotify/data/spotifytrack.h @@ -20,6 +20,7 @@ struct SpotifyTrack : public BaseSpotifyElement QSharedPointer album; QList> artists; + [[nodiscard]] auto artistNames() const -> QStringList; [[nodiscard]] auto artistString() const -> QString; [[nodiscard]] auto image() const -> QSharedPointer; diff --git a/src/tools/audio/audiotool.cpp b/src/tools/audio/audiotool.cpp index 5648b054..f999a265 100644 --- a/src/tools/audio/audiotool.cpp +++ b/src/tools/audio/audiotool.cpp @@ -14,7 +14,8 @@ using namespace Qt::Literals::StringLiterals; Q_LOGGING_CATEGORY(gmAudioTool, "gm.audio.tool") AudioTool::AudioTool(QQmlEngine *engine, QObject *parent) - : AbstractTool(parent), m_editor(engine), musicPlayer(metaDataReader), + : AbstractTool(parent), m_editor(engine), musicPlayer(*engine->networkAccessManager(), metaDataReader), + soundPlayerController(*engine->networkAccessManager()), radioPlayer(*engine->networkAccessManager(), metaDataReader) { qCDebug(gmAudioTool()) << "Loading ..."; @@ -23,35 +24,30 @@ AudioTool::AudioTool(QQmlEngine *engine, QObject *parent) connect(Spotify::instance(), &Spotify::authorized, this, &AudioTool::onSpotifyAuthorized); // Music Player - connect(&musicPlayer, &MusicPlayer::startedPlaying, this, &AudioTool::onStartedPlaying); - connect(&musicPlayer, &MusicPlayer::playlistChanged, this, [this](const QList &files) { - Q_UNUSED(files); - emit playlistChanged(); - }); - connect(&musicPlayer, &MusicPlayer::playlistChanged, this, - [](const QList &files) { qCDebug(gmAudioTool()) << "Playlist Changed!" << files.length(); }); - connect(&musicPlayer, &MusicPlayer::currentIndexChanged, this, &AudioTool::currentIndexChanged); + connect(&musicPlayer, &MusicPlayer::stateChanged, this, &AudioTool::onStateChanged); + connect(&musicPlayer, &MusicPlayer::playlistChanged, this, &AudioTool::playlistChanged); + connect(&musicPlayer, &MusicPlayer::playlistIndexChanged, this, &AudioTool::currentIndexChanged); // Radio Player - connect(&radioPlayer, &RadioPlayer::startedPlaying, this, &AudioTool::onStartedPlaying); - - // Meta Data - connect(&metaDataReader, &MetaDataReader::metaDataChanged, this, &AudioTool::onMetaDataUpdated); + connect(&radioPlayer, &RadioPlayer::stateChanged, this, &AudioTool::onStateChanged); +#ifndef NO_DBUS + mprisManager.setMetaDataReader(&metaDataReader); // Mpris connect(&mprisManager, &MprisManager::play, this, [this]() { - if (m_isPaused) playPause(); + if (playbackState() != AudioPlayer::State::Playing) playPause(); }); connect(&mprisManager, &MprisManager::playPause, this, [this]() { playPause(); }); connect(&mprisManager, &MprisManager::pause, this, [this]() { - if (!m_isPaused) playPause(); + if (playbackState() == AudioPlayer::State::Playing) playPause(); }); connect(&mprisManager, &MprisManager::stop, this, [this]() { - if (!m_isPaused) playPause(); + if (playbackState() == AudioPlayer::State::Playing) playPause(); }); connect(&mprisManager, &MprisManager::next, this, [this]() { next(); }); connect(&mprisManager, &MprisManager::previous, this, [this]() { again(); }); connect(&mprisManager, &MprisManager::changeVolume, this, [this](double volume) { setMusicVolume(volume); }); +#endif } auto AudioTool::create(QQmlEngine *qmlEngine, QJSEngine *jsEngine) -> AudioTool * @@ -145,36 +141,40 @@ void AudioTool::play(AudioElement *element) return; } + auto prepareMusic = [this](const AudioElement &element) { + metaDataReader.clearMetaData(); + m_musicElementType = element.type(); + setMusicVolume(m_musicVolume); + currentElementName(element.name()); + }; + switch (element->type()) { case AudioElement::Type::Music: - m_musicElementType = element->type(); + prepareMusic(*element); radioPlayer.stop(); musicPlayer.play(element); - setMusicVolume(m_musicVolume); - break; - - case AudioElement::Type::Sound: - soundPlayerController.play(element); - setSoundVolume(m_soundVolume); break; - case AudioElement::Type::Radio: - m_musicElementType = element->type(); + prepareMusic(*element); + emit playlistChanged(); musicPlayer.stop(); radioPlayer.play(element); - setMusicVolume(m_musicVolume); + break; + case AudioElement::Type::Sound: + soundPlayerController.play(element); + setSoundVolume(m_soundVolume); break; } - - metaDataReader.updateMetaData(QMediaMetaData::MediaType, element->name()); } -void AudioTool::onStartedPlaying() +void AudioTool::onStateChanged(AudioPlayer::State state) { - m_isPaused = false; - emit isPausedChanged(); - mprisManager.setPlaybackStatus(1); + playbackState(state); + +#ifndef NO_DBUS + mprisManager.setPlaybackStatus(state); +#endif } /** @@ -197,21 +197,7 @@ void AudioTool::next() */ void AudioTool::playPause() { - if (m_isPaused) - { - switch (m_musicElementType) - { - case AudioElement::Type::Music: - musicPlayer.play(); - break; - case AudioElement::Type::Radio: - radioPlayer.play(); - break; - default: - break; - } - } - else + if (playbackState() == AudioPlayer::State::Playing) { switch (m_musicElementType) { @@ -225,9 +211,19 @@ void AudioTool::playPause() break; } - m_isPaused = true; - emit isPausedChanged(); - mprisManager.setPlaybackStatus(2); + return; + } + + switch (m_musicElementType) + { + case AudioElement::Type::Music: + musicPlayer.play(); + break; + case AudioElement::Type::Radio: + radioPlayer.play(); + break; + default: + break; } } @@ -239,8 +235,7 @@ void AudioTool::stop() musicPlayer.stop(); radioPlayer.stop(); soundPlayerController.stop(); - m_isPaused = true; - emit isPausedChanged(); + playbackState(AudioPlayer::State::Stopped); } /** @@ -273,7 +268,10 @@ void AudioTool::setMusicVolume(qreal volume) musicPlayer.setVolume(linearVolume, logarithmicVolume); radioPlayer.setVolume(linearVolume, logarithmicVolume); + +#ifndef NO_DBUS mprisManager.setVolume(logarithmicVolume); +#endif } /** @@ -300,17 +298,6 @@ auto AudioTool::makeLogarithmicVolume(qreal linearVolume) -> int VOLUME_FACTOR); } -auto AudioTool::playlist() const -> QList -{ - switch (m_musicElementType) - { - case AudioElement::Type::Music: - return musicPlayer.playlist(); - default: - return {}; - } -} - auto AudioTool::playlistQml() -> QQmlListProperty { switch (m_musicElementType) @@ -331,6 +318,7 @@ auto AudioTool::index() const -> int switch (m_musicElementType) { case AudioElement::Type::Music: + qCDebug(gmAudioTool()) << musicPlayer.playlistIndex(); return musicPlayer.playlistIndex(); default: return 0; @@ -353,13 +341,6 @@ void AudioTool::setMusicIndex(int index) } } -void AudioTool::onMetaDataUpdated() -{ - mprisManager.updateMetaData(metaDataReader.metaData()); - emit metaDataChanged(); - emit currentIndexChanged(); -} - void AudioTool::findElement(const QString &term) const { AudioScenario::setFilterString(term); diff --git a/src/tools/audio/audiotool.h b/src/tools/audio/audiotool.h index 0b7707ac..3bf8cb5c 100644 --- a/src/tools/audio/audiotool.h +++ b/src/tools/audio/audiotool.h @@ -3,7 +3,6 @@ #include "common/abstracttool.h" #include "editor/audioeditor.h" #include "metadata/metadatareader.h" -#include "mpris/mprismanager.h" #include "players/musicplayer.h" #include "players/radioplayer.h" #include "players/soundplayercontroller.h" @@ -13,6 +12,10 @@ #include #include +#ifndef NO_DBUS +#include "mpris/mprismanager.h" +#endif + class AudioTool : public AbstractTool { Q_OBJECT @@ -21,10 +24,11 @@ class AudioTool : public AbstractTool READ_PROPERTY2(AudioProject *, currentProject, nullptr) READ_LIST_PROPERTY(AudioProject, projects) + AUTO_PROPERTY(QString, currentElementName) Q_PROPERTY(SoundPlayerController *soundController READ soundController CONSTANT) - Q_PROPERTY(bool isPaused READ isPaused NOTIFY isPausedChanged) + AUTO_PROPERTY_VAL2(AudioPlayer::State, playbackState, AudioPlayer::State::Initialized) Q_PROPERTY(qreal musicVolume READ musicVolume NOTIFY musicVolumeChanged) Q_PROPERTY(qreal soundVolume READ soundVolume NOTIFY soundVolumeChanged) @@ -76,28 +80,22 @@ class AudioTool : public AbstractTool { soundPlayerController.stop(sound); } - [[nodiscard]] auto isPaused() const -> bool - { - return m_isPaused; - } void stop(); Q_INVOKABLE void findElement(const QString &term) const; // Meta Data - [[nodiscard]] auto metaData() const -> AudioMetaData * + [[nodiscard]] auto metaData() -> AudioMetaData * { return metaDataReader.metaData(); } [[nodiscard]] auto index() const -> int; - [[nodiscard]] auto playlist() const -> QList; [[nodiscard]] auto playlistQml() -> QQmlListProperty; public slots: void loadData() override; signals: - void isPausedChanged(); void soundsChanged(); void metaDataChanged(); void currentIndexChanged(); @@ -109,8 +107,7 @@ public slots: private slots: void onProjectsChanged(const std::vector &projects); void onCurrentScenarioChanged() const; - void onStartedPlaying(); - void onMetaDataUpdated(); + void onStateChanged(AudioPlayer::State state); void onSpotifyAuthorized() { emit spotifyAuthorized(); @@ -119,7 +116,10 @@ private slots: private: AudioEditor m_editor; MetaDataReader metaDataReader; + +#ifndef NO_DBUS MprisManager mprisManager; +#endif // Players MusicPlayer musicPlayer; @@ -127,7 +127,6 @@ private slots: RadioPlayer radioPlayer; AudioElement::Type m_musicElementType = AudioElement::Type::Music; - bool m_isPaused = true; // Volume static constexpr qreal DEFAULT_MUSIC_VOLUME = 0.25; diff --git a/src/tools/audio/metadata/audiometadata.h b/src/tools/audio/metadata/audiometadata.h index 214cf213..3427cf9d 100644 --- a/src/tools/audio/metadata/audiometadata.h +++ b/src/tools/audio/metadata/audiometadata.h @@ -9,13 +9,30 @@ class AudioMetaData : public QObject Q_OBJECT QML_ELEMENT + AUTO_PROPERTY2(QString, title, QStringLiteral("-")) + AUTO_PROPERTY2(QStringList, artist, {QStringLiteral("-")}) + AUTO_PROPERTY2(QString, album, QStringLiteral("-")) + AUTO_PROPERTY(QString, cover) + AUTO_PROPERTY_VAL2(qint64, length, 0) + public: using QObject::QObject; - AUTO_PROPERTY(QString, title) - AUTO_PROPERTY(QString, artist) - AUTO_PROPERTY(QString, album) - AUTO_PROPERTY(QString, cover) - AUTO_PROPERTY(QString, type) - AUTO_PROPERTY_VAL2(qint64, length, 0) + void apply(const AudioMetaData &other) + { + title(other.title()); + artist(other.artist()); + album(other.album()); + cover(other.cover()); + length(other.length()); + } + + void clear() + { + title(QStringLiteral("-")); + artist({QStringLiteral("-")}); + album(QStringLiteral("-")); + cover(QStringLiteral()); + length(0); + } }; diff --git a/src/tools/audio/metadata/metadatareader.cpp b/src/tools/audio/metadata/metadatareader.cpp index 5d50b291..cf85230d 100644 --- a/src/tools/audio/metadata/metadatareader.cpp +++ b/src/tools/audio/metadata/metadatareader.cpp @@ -1,4 +1,6 @@ #include "metadatareader.h" +#include "../thumbnails/loaders/tagimageloader.h" +#include "utils/stringutils.h" #include #include #include @@ -10,74 +12,49 @@ using namespace Qt::Literals::StringLiterals; Q_LOGGING_CATEGORY(gmAudioMetaData, "gm.audio.metadata") -MetaDataReader::MetaDataReader(QObject *parent) : QObject(parent), m_metaData(new AudioMetaData(this)) +void MetaDataReader::setMetaData(const AudioMetaData &metaData) { + m_metaData.apply(metaData); } -void MetaDataReader::setMetaData(AudioMetaData *metaData) -{ - if (m_metaData) m_metaData->deleteLater(); - - m_metaData = metaData; - m_metaData->setParent(this); - emit metaDataChanged(); -} - -/** - * @brief Read MetaData from media - * @param mediaPlayer Pointer to QMediaPlayer with the media object - */ -void MetaDataReader::updateMetaData(QMediaPlayer *mediaPlayer) -{ - if (!m_metaData) m_metaData = new AudioMetaData(this); +// void MetaDataReader::updateMetaData(QMediaPlayer *mediaPlayer) +//{ +// if (!m_metaData) m_metaData = new AudioMetaData(this); - updateDuration(mediaPlayer->duration() * 1000); -} +// setDuration(mediaPlayer->duration() * 1000); +//} -void MetaDataReader::updateMetaData(const QMediaMetaData &metaData) -{ - if (!m_metaData) m_metaData = new AudioMetaData(this); - - foreach (auto key, metaData.keys()) - { - updateMetaData(key, metaData.value(key)); - } -} - -void MetaDataReader::updateMetaData(QMediaMetaData::Key key, const QVariant &value) +void MetaDataReader::setMetaData(QMediaMetaData::Key key, const QVariant &value) { if (!value.isValid() || value.isNull()) return; switch (key) { case QMediaMetaData::Title: - m_metaData->title(value.toString()); + m_metaData.title(value.toString()); break; case QMediaMetaData::Author: case QMediaMetaData::AlbumArtist: case QMediaMetaData::Composer: case QMediaMetaData::LeadPerformer: - m_metaData->artist(value.toString()); + m_metaData.artist(value.toStringList()); break; case QMediaMetaData::AlbumTitle: - m_metaData->album(value.toString()); - case QMediaMetaData::MediaType: - m_metaData->type(value.toString()); - break; + m_metaData.album(value.toString()); case QMediaMetaData::ThumbnailImage: case QMediaMetaData::CoverArtImage: { if (m_coverFile) m_coverFile->deleteLater(); - m_coverFile = new QTemporaryFile(this); + m_coverFile = std::make_unique(); if (m_coverFile->open()) { - if (!value.value().save(m_coverFile, "JPG")) + if (!value.value().save(m_coverFile.get(), "JPG")) { qWarning(gmAudioMetaData()) << "Could not save cover art in temp file."; } - m_metaData->cover(QUrl::fromLocalFile(m_coverFile->fileName()).toEncoded()); + m_metaData.cover(QUrl::fromLocalFile(m_coverFile->fileName()).toEncoded()); m_coverFile->close(); } break; @@ -87,75 +64,80 @@ void MetaDataReader::updateMetaData(QMediaMetaData::Key key, const QVariant &val } } -void MetaDataReader::updateMetaData(const QString &key, const QVariant &value) +void MetaDataReader::setMetaData(const QMediaMetaData &data) { - qCDebug(gmAudioMetaData()) << "Updating meta data:" << key; + setMetaData(QMediaMetaData::Title, data.value(QMediaMetaData::Title)); + setMetaData(QMediaMetaData::AlbumTitle, data.value(QMediaMetaData::AlbumTitle)); + + auto artist = data.value(QMediaMetaData::Author); + if (artist.isNull()) artist = data.value(QMediaMetaData::AlbumArtist); + if (artist.isNull()) artist = data.value(QMediaMetaData::Composer); + if (artist.isNull()) artist = data.value(QMediaMetaData::LeadPerformer); + setMetaData(QMediaMetaData::Author, artist); + + auto image = data.value(QMediaMetaData::CoverArtImage); + if (image.isNull()) image = data.value(QMediaMetaData::ThumbnailImage); + setMetaData(QMediaMetaData::CoverArtImage, image); +} - if (!m_metaData) m_metaData = new AudioMetaData(this); +void MetaDataReader::setMetaData(const QString &key, const QVariant &value) +{ + qCDebug(gmAudioMetaData()) << "Updating meta data:" << key; if (key == "Title"_L1) { - updateMetaData(QMediaMetaData::Title, value); + setMetaData(QMediaMetaData::Title, value); return; } if (key == "AlbumArtist"_L1) { - updateMetaData(QMediaMetaData::AlbumArtist, value); + setMetaData(QMediaMetaData::AlbumArtist, value); return; } if (key == "Artist"_L1) { - updateMetaData(QMediaMetaData::Author, value); + setMetaData(QMediaMetaData::Author, value); return; } if (key == "Composer"_L1) { - updateMetaData(QMediaMetaData::Composer, value); + setMetaData(QMediaMetaData::Composer, value); return; } if (key == "AlbumTitle"_L1) { - updateMetaData(QMediaMetaData::AlbumTitle, value); + setMetaData(QMediaMetaData::AlbumTitle, value); return; } if (key == "ThumbnailImage"_L1) { - updateMetaData(QMediaMetaData::ThumbnailImage, value); + setMetaData(QMediaMetaData::ThumbnailImage, value); return; } if (key == "CoverArtImage"_L1) { - updateMetaData(QMediaMetaData::CoverArtImage, value); + setMetaData(QMediaMetaData::CoverArtImage, value); return; } if (key == "Type"_L1) { - updateMetaData(QMediaMetaData::MediaType, value); + setMetaData(QMediaMetaData::MediaType, value); return; } } -/** - * @brief Use taglib to read basic meta data. Works only with taglib 1.12-beta1 or greater. - * This is intended to be a fallback implementation, as qmediaplayer sometimes does not recognize the tags. - * Also this unfortunately can not read the cover art, but it's better than nothing. - */ -void MetaDataReader::updateMetaData(const QByteArray &data) +void MetaDataReader::loadMetaData(const QString &path, const QByteArray &data) { -#if TAGLIB_MAJOR_VERSION >= 1 && TAGLIB_MINOR_VERSION >= 12 - - if (!m_metaData) m_metaData = new AudioMetaData(this); - const TagLib::ByteVector bvector(data.data(), data.length()); - TagLib::ByteVectorStream bvstream(bvector); - const TagLib::FileRef ref(&bvstream); + auto bvstream = std::make_unique(bvector); + const TagLib::FileRef ref(bvstream.get()); auto *tag = ref.tag(); if (!tag || tag->isEmpty()) return; @@ -163,37 +145,28 @@ void MetaDataReader::updateMetaData(const QByteArray &data) qCDebug(gmAudioMetaData()) << "Updating meta data from data ..."; auto title = tag->title(); - auto artist = tag->artist(); - auto album = tag->album(); + if (!title.isEmpty()) m_metaData.title(QString::fromStdString(title.to8Bit(true))); - if (!title.isEmpty()) m_metaData->title(QString::fromStdString(title.to8Bit(true))); - if (!artist.isEmpty()) m_metaData->artist(QString::fromStdString(artist.to8Bit(true))); - if (!album.isEmpty()) m_metaData->album(QString::fromStdString(album.to8Bit(true))); + auto artist = tag->artist(); + if (!artist.isEmpty()) m_metaData.artist(QString::fromStdString(artist.to8Bit(true)).split(", ")); - emit metaDataChanged(); + auto album = tag->album(); + if (!album.isEmpty()) m_metaData.album(QString::fromStdString(album.to8Bit(true))); -#endif + TagImageLoader::loadFromData(path, std::move(bvstream)).then(&m_metaData, [this](const QPixmap &pixmap) { + if (!pixmap.isNull()) + { + m_metaData.cover(StringUtils::stringFromImage(pixmap)); + } + }); } -void MetaDataReader::updateDuration(qint64 duration) +void MetaDataReader::setDuration(qint64 duration) { - if (!m_metaData) m_metaData = new AudioMetaData(this); - - m_metaData->length(duration); - emit metaDataChanged(); + m_metaData.length(duration); } void MetaDataReader::clearMetaData() { - qCDebug(gmAudioMetaData()) << "Clearing meta data ..."; - - if (!m_metaData) m_metaData = new AudioMetaData(this); - - m_metaData->artist(u"-"_s); - m_metaData->album(u"-"_s); - m_metaData->title(u"-"_s); - m_metaData->cover(u""_s); - m_metaData->length(0); - - emit metaDataChanged(); + m_metaData.clear(); } diff --git a/src/tools/audio/metadata/metadatareader.h b/src/tools/audio/metadata/metadatareader.h index 14499230..f1303993 100644 --- a/src/tools/audio/metadata/metadatareader.h +++ b/src/tools/audio/metadata/metadatareader.h @@ -1,37 +1,33 @@ #pragma once #include "audiometadata.h" +#include #include -#include -#include -#include #include +#include +#include -class MetaDataReader : public QObject +class MetaDataReader { - Q_OBJECT public: - explicit MetaDataReader(QObject *parent = nullptr); + MetaDataReader() = default; - [[nodiscard]] auto metaData() const -> AudioMetaData * + [[nodiscard]] auto metaData() -> AudioMetaData * { - return m_metaData; + return &m_metaData; } - void setMetaData(AudioMetaData *metaData); + void setMetaData(const AudioMetaData &metaData); -private: - AudioMetaData *m_metaData = nullptr; - QTemporaryFile *m_coverFile = nullptr; + void setMetaData(const QString &key, const QVariant &value); + void setMetaData(QMediaMetaData::Key key, const QVariant &value); + void setMetaData(const QMediaMetaData &data); + void setDuration(qint64 duration); -signals: - void metaDataChanged(); + void loadMetaData(const QString &path, const QByteArray &data); -public slots: - void updateMetaData(QMediaPlayer *mediaPlayer); - void updateMetaData(const QMediaMetaData &metaData); - void updateMetaData(const QString &key, const QVariant &value); - void updateMetaData(QMediaMetaData::Key key, const QVariant &value); - void updateMetaData(const QByteArray &data); - void updateDuration(qint64 duration); void clearMetaData(); + +private: + AudioMetaData m_metaData; + std::unique_ptr m_coverFile = nullptr; }; diff --git a/src/tools/audio/mpris/mprisadaptor.cpp b/src/tools/audio/mpris/mprisadaptor.cpp index ee1b3f05..2a4bf12d 100644 --- a/src/tools/audio/mpris/mprisadaptor.cpp +++ b/src/tools/audio/mpris/mprisadaptor.cpp @@ -1,3 +1,4 @@ +#ifndef NO_DBUS #include "mprisadaptor.h" void MprisAdaptor::Raise() const @@ -9,3 +10,5 @@ void MprisAdaptor::Quit() const { // Not implemented } + +#endif diff --git a/src/tools/audio/mpris/mprisadaptor.h b/src/tools/audio/mpris/mprisadaptor.h index c424250d..8724dae6 100644 --- a/src/tools/audio/mpris/mprisadaptor.h +++ b/src/tools/audio/mpris/mprisadaptor.h @@ -1,4 +1,5 @@ #pragma once +#ifndef NO_DBUS #include #include @@ -18,31 +19,31 @@ class MprisAdaptor : public QDBusAbstractAdaptor public: explicit MprisAdaptor(QObject *parent) : QDBusAbstractAdaptor(parent){}; - [[nodiscard]] bool canQuit() const + [[nodiscard]] auto canQuit() const -> bool { return false; } - [[nodiscard]] bool canRaise() const + [[nodiscard]] auto canRaise() const -> bool { return false; } - [[nodiscard]] bool hasTrackList() const + [[nodiscard]] auto hasTrackList() const -> bool { return false; } - [[nodiscard]] QString identity() const + [[nodiscard]] auto identity() const -> QString { return QStringLiteral("GM-Companion"); } - [[nodiscard]] QString desktopEntry() const + [[nodiscard]] auto desktopEntry() const -> QString { return QStringLiteral("gm-companion"); } - [[nodiscard]] QStringList supportedUriSchemes() const + [[nodiscard]] auto supportedUriSchemes() const -> QStringList { return {}; } - [[nodiscard]] QStringList supportedMimeTypes() const + [[nodiscard]] auto supportedMimeTypes() const -> QStringList { return {}; } @@ -51,3 +52,5 @@ public slots: Q_NOREPLY void Raise() const; Q_NOREPLY void Quit() const; }; + +#endif diff --git a/src/tools/audio/mpris/mprismanager.cpp b/src/tools/audio/mpris/mprismanager.cpp index 6b8f38f3..d9416db2 100644 --- a/src/tools/audio/mpris/mprismanager.cpp +++ b/src/tools/audio/mpris/mprismanager.cpp @@ -1,68 +1,71 @@ -#include "mprismanager.h" - #ifndef NO_DBUS +#include "mprismanager.h" #include #include #include #include #include #include -#endif using namespace Qt::Literals::StringLiterals; -MprisManager::MprisManager(QObject *parent) : QObject(parent) +MprisManager::MprisManager(QObject *parent) + : QObject(parent), m_mprisAdaptor(new MprisAdaptor(this)), m_mprisPlayerAdaptor(new MprisPlayerAdaptor(this)) { -#ifndef NO_DBUS - mprisAdaptor = new MprisAdaptor(this); - mprisPlayerAdaptor = new MprisPlayerAdaptor(this); - - connect(mprisPlayerAdaptor, &MprisPlayerAdaptor::next, this, &MprisManager::next); - connect(mprisPlayerAdaptor, &MprisPlayerAdaptor::changeVolume, this, &MprisManager::changeVolume); - connect(mprisPlayerAdaptor, &MprisPlayerAdaptor::pause, this, &MprisManager::pause); - connect(mprisPlayerAdaptor, &MprisPlayerAdaptor::previous, this, &MprisManager::previous); - connect(mprisPlayerAdaptor, &MprisPlayerAdaptor::playPause, this, &MprisManager::playPause); - connect(mprisPlayerAdaptor, &MprisPlayerAdaptor::stop, this, &MprisManager::stop); - connect(mprisPlayerAdaptor, &MprisPlayerAdaptor::play, this, &MprisManager::play); + connect(m_mprisPlayerAdaptor, &MprisPlayerAdaptor::next, this, &MprisManager::next); + connect(m_mprisPlayerAdaptor, &MprisPlayerAdaptor::changeVolume, this, &MprisManager::changeVolume); + connect(m_mprisPlayerAdaptor, &MprisPlayerAdaptor::pause, this, &MprisManager::pause); + connect(m_mprisPlayerAdaptor, &MprisPlayerAdaptor::previous, this, &MprisManager::previous); + connect(m_mprisPlayerAdaptor, &MprisPlayerAdaptor::playPause, this, &MprisManager::playPause); + connect(m_mprisPlayerAdaptor, &MprisPlayerAdaptor::stop, this, &MprisManager::stop); + connect(m_mprisPlayerAdaptor, &MprisPlayerAdaptor::play, this, &MprisManager::play); QDBusConnection::sessionBus().registerObject(u"/org/mpris/MediaPlayer2"_s, this); QDBusConnection::sessionBus().registerService(u"org.mpris.MediaPlayer2.gm_companion"_s); -#endif + + m_metaData[u"mpris:trackid"_s] = + QVariant::fromValue(QDBusObjectPath(u"/lol/rophil/gm_companion/audio/current_track"_s)); + sendUpdatedMetadata(); } -void MprisManager::setPlaybackStatus(int status) +void MprisManager::setPlaybackStatus(AudioPlayer::State status) { -#ifndef NO_DBUS - mprisPlayerAdaptor->setPlaybackStatus(status); - sendMprisUpdateSignal(u"PlaybackStatus"_s, mprisPlayerAdaptor->playbackStatus()); -#endif + m_mprisPlayerAdaptor->setPlaybackStatus(status); + sendMprisUpdateSignal(u"PlaybackStatus"_s, m_mprisPlayerAdaptor->playbackStatus()); } void MprisManager::setVolume(double volume) { -#ifndef NO_DBUS - mprisPlayerAdaptor->setVolume(volume); -#endif + m_mprisPlayerAdaptor->setVolume(volume); } -void MprisManager::updateMetaData(AudioMetaData *metaData) +void MprisManager::setMetaDataReader(MetaDataReader *reader) { -#ifndef NO_DBUS - QMap map; - map.insert(u"mpris:trackid"_s, - QVariant::fromValue(QDBusObjectPath(u"/lol/rophil/gm_companion/audio/current_track"_s))); - map.insert(u"mpris:length"_s, metaData->length()); - map.insert(u"mpris:artUrl"_s, metaData->cover()); - map.insert(u"xesam:album"_s, metaData->album().isEmpty() ? tr("Unknown Album") : metaData->album()); - map.insert(u"xesam:albumArtist"_s, - metaData->artist().isEmpty() ? QStringList({tr("Unknown Artist")}) : QStringList({metaData->artist()})); - map.insert(u"xesam:artist"_s, - metaData->artist().isEmpty() ? QStringList({tr("Unknown Artist")}) : QStringList({metaData->artist()})); - map.insert(u"xesam:title"_s, metaData->title().isEmpty() ? tr("Unknown Title") : metaData->title()); - - mprisPlayerAdaptor->setMetadata(map); - sendMprisUpdateSignal(u"Metadata"_s, map); -#endif + connect(reader->metaData(), &AudioMetaData::titleChanged, this, [this](const QString &title) { + m_metaData[u"xesam:title"_s] = title; + sendUpdatedMetadata(); + }); + + connect(reader->metaData(), &AudioMetaData::artistChanged, this, [this](const QStringList &artist) { + m_metaData[u"xesam:artist"_s] = artist; + m_metaData[u"xesam:albumArtist"_s] = artist; + sendUpdatedMetadata(); + }); + + connect(reader->metaData(), &AudioMetaData::albumChanged, this, [this](const QString &album) { + m_metaData[u"xesam:album"_s] = album; + sendUpdatedMetadata(); + }); + + connect(reader->metaData(), &AudioMetaData::coverChanged, this, [this](const QString &cover) { + m_metaData[u"mpris:artUrl"_s] = cover; + sendUpdatedMetadata(); + }); + + connect(reader->metaData(), &AudioMetaData::lengthChanged, this, [this](qint64 length) { + m_metaData[u"mpris:length"_s] = length; + sendUpdatedMetadata(); + }); } /** @@ -72,7 +75,6 @@ void MprisManager::updateMetaData(AudioMetaData *metaData) */ void MprisManager::sendMprisUpdateSignal(const QString &property, const QVariant &value) const { -#ifndef NO_DBUS QDBusMessage signal = QDBusMessage::createSignal(u"/org/mpris/MediaPlayer2"_s, u"org.freedesktop.DBus.Properties"_s, u"PropertiesChanged"_s); @@ -86,5 +88,12 @@ void MprisManager::sendMprisUpdateSignal(const QString &property, const QVariant signal << invalidatedProps; QDBusConnection::sessionBus().send(signal); -#endif } + +void MprisManager::sendUpdatedMetadata() +{ + m_mprisPlayerAdaptor->setMetadata(m_metaData); + sendMprisUpdateSignal(u"Metadata"_s, m_metaData); +} + +#endif diff --git a/src/tools/audio/mpris/mprismanager.h b/src/tools/audio/mpris/mprismanager.h index 923157a9..5438a6d4 100644 --- a/src/tools/audio/mpris/mprismanager.h +++ b/src/tools/audio/mpris/mprismanager.h @@ -1,9 +1,11 @@ #pragma once +#ifndef NO_DBUS #include "../metadata/metadatareader.h" #include "mprisadaptor.h" #include "mprisplayeradaptor.h" #include +#include class MprisManager : public QObject { @@ -11,15 +13,9 @@ class MprisManager : public QObject public: explicit MprisManager(QObject *parent = nullptr); - void setPlaybackStatus(int status); + void setPlaybackStatus(AudioPlayer::State status); void setVolume(double volume); - void updateMetaData(AudioMetaData *metaData); - -private: - MprisAdaptor *mprisAdaptor = nullptr; - MprisPlayerAdaptor *mprisPlayerAdaptor = nullptr; - - void sendMprisUpdateSignal(const QString &property, const QVariant &value) const; + void setMetaDataReader(MetaDataReader *reader); signals: void play(); @@ -29,4 +25,16 @@ class MprisManager : public QObject void next(); void previous(); void changeVolume(double volume); + +private: + void sendMprisUpdateSignal(const QString &property, const QVariant &value) const; + void sendUpdatedMetadata(); + + QHash m_metaData; + + // must be created on the heap using _new_ + MprisAdaptor *m_mprisAdaptor = nullptr; + MprisPlayerAdaptor *m_mprisPlayerAdaptor = nullptr; }; + +#endif diff --git a/src/tools/audio/mpris/mprisplayeradaptor.cpp b/src/tools/audio/mpris/mprisplayeradaptor.cpp index e2c18a40..27dbae84 100644 --- a/src/tools/audio/mpris/mprisplayeradaptor.cpp +++ b/src/tools/audio/mpris/mprisplayeradaptor.cpp @@ -1,3 +1,4 @@ +#ifndef NO_DBUS #include "mprisplayeradaptor.h" #include @@ -5,27 +6,27 @@ using namespace Qt::Literals::StringLiterals; Q_LOGGING_CATEGORY(gmMprisPlayer, "gm.mpris.player") -void MprisPlayerAdaptor::setPlaybackStatus(int status) +void MprisPlayerAdaptor::setPlaybackStatus(AudioPlayer::State status) { switch (status) { - case 0: - m_PlaybackStatus = u"Stopped"_s; - break; - - case 1: + case AudioPlayer::State::Playing: m_PlaybackStatus = u"Playing"_s; break; - case 2: + case AudioPlayer::State::Paused: m_PlaybackStatus = u"Paused"_s; break; + + default: + m_PlaybackStatus = u"Stopped"_s; + break; } emit playbackStatusChanged(m_PlaybackStatus); } -void MprisPlayerAdaptor::setMetadata(const QMap &data) +void MprisPlayerAdaptor::setMetadata(const QHash &data) { qCDebug(gmMprisPlayer()) << "Updating mpris metadata ..."; @@ -47,3 +48,5 @@ void MprisPlayerAdaptor::OpenUri(const QString & /*Uri*/) const { // Not implemented } + +#endif diff --git a/src/tools/audio/mpris/mprisplayeradaptor.h b/src/tools/audio/mpris/mprisplayeradaptor.h index 2b911676..7d0dd6e1 100644 --- a/src/tools/audio/mpris/mprisplayeradaptor.h +++ b/src/tools/audio/mpris/mprisplayeradaptor.h @@ -1,5 +1,7 @@ #pragma once +#ifndef NO_DBUS +#include "../players/audioplayer.h" #include #include #include @@ -14,7 +16,7 @@ class MprisPlayerAdaptor : public QDBusAbstractAdaptor Q_PROPERTY(QString LoopStatus READ loopStatus WRITE setLoopStatus) Q_PROPERTY(double Rate READ rate WRITE setRate) Q_PROPERTY(bool Shuffle READ shuffle WRITE setShuffle) - Q_PROPERTY(QMap Metadata READ metadata NOTIFY metadataChanged) + Q_PROPERTY(QHash Metadata READ metadata NOTIFY metadataChanged) Q_PROPERTY(double Volume READ volume WRITE setVolume) Q_PROPERTY(qlonglong Position READ position) Q_PROPERTY(double MinimumRate READ minimumRate) @@ -33,7 +35,7 @@ class MprisPlayerAdaptor : public QDBusAbstractAdaptor { return m_PlaybackStatus; } - void setPlaybackStatus(int status); + void setPlaybackStatus(AudioPlayer::State status); [[nodiscard]] auto loopStatus() const -> QString { @@ -59,11 +61,11 @@ class MprisPlayerAdaptor : public QDBusAbstractAdaptor { /* Not Implemented */ } - [[nodiscard]] auto metadata() const -> QMap + [[nodiscard]] auto metadata() const -> QHash { return m_Metadata; } - void setMetadata(const QMap &data); + void setMetadata(const QHash &data); [[nodiscard]] auto volume() const -> double { @@ -71,7 +73,7 @@ class MprisPlayerAdaptor : public QDBusAbstractAdaptor } void setVolume(double /*volume*/) const { - } // emit changeVolume(volume); + } [[nodiscard]] auto position() const -> qlonglong { @@ -116,7 +118,7 @@ class MprisPlayerAdaptor : public QDBusAbstractAdaptor QString m_LoopStatus; double m_Volume = 0; bool m_Shuffle = false; - QMap m_Metadata; + QHash m_Metadata; qlonglong m_Position = 0; QTemporaryFile m_tempArtFile; @@ -131,7 +133,7 @@ class MprisPlayerAdaptor : public QDBusAbstractAdaptor void play(); void playbackStatusChanged(QString); - void metadataChanged(QMap); + void metadataChanged(QHash); public slots: Q_NOREPLY void Next() @@ -164,3 +166,5 @@ public slots: Q_NOREPLY void SetPosition(const QDBusObjectPath &, qlonglong) const; Q_NOREPLY void OpenUri(const QString &) const; }; + +#endif diff --git a/src/tools/audio/players/audioplayer.h b/src/tools/audio/players/audioplayer.h index 92e09ee9..90fa6e0c 100644 --- a/src/tools/audio/players/audioplayer.h +++ b/src/tools/audio/players/audioplayer.h @@ -1,5 +1,6 @@ #pragma once +#include "thirdparty/propertyhelper/PropertyHelper.h" #include #include @@ -12,6 +13,18 @@ class AudioPlayer : public QObject public: using QObject::QObject; + enum class State + { + Initialized, + Playing, + Paused, + Stopped, + Loading + }; + Q_ENUM(State) + + AUTO_PROPERTY_VAL2(State, state, State::Initialized) + public slots: virtual void play() = 0; virtual void pause() = 0; diff --git a/src/tools/audio/players/bufferedaudioplayer.cpp b/src/tools/audio/players/bufferedaudioplayer.cpp new file mode 100644 index 00000000..018d9d21 --- /dev/null +++ b/src/tools/audio/players/bufferedaudioplayer.cpp @@ -0,0 +1,297 @@ +#include "bufferedaudioplayer.h" +#include "filesystem/file.h" +#include "filesystem/results/filedataresult.h" +#include "settings/settingsmanager.h" +#include "utils/fileutils.h" +#include "utils/utils.h" +#include + +using namespace Qt::Literals::StringLiterals; + +Q_LOGGING_CATEGORY(gmAudioBufferedPlayer, "gm.audio.buffered") + +BufferedAudioPlayer::BufferedAudioPlayer(const QString &settingsId, QNetworkAccessManager &networkManager, + QObject *parent) + : AudioPlayer(parent), m_settingsId(settingsId), + m_playlist(std::make_unique(settingsId, networkManager)) +{ + m_mediaPlayer.setAudioOutput(&m_audioOutput); + + connect(&m_mediaPlayer, &QMediaPlayer::playbackStateChanged, this, + &BufferedAudioPlayer::onMediaPlayerPlaybackStateChanged); + connect(&m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &BufferedAudioPlayer::onMediaStatusChanged); + connect(&m_mediaPlayer, &QMediaPlayer::errorOccurred, this, &BufferedAudioPlayer::onMediaPlayerErrorOccurred); + connect(&m_mediaPlayer, &QMediaPlayer::metaDataChanged, this, + [this]() { emit metaDataChanged(m_mediaPlayer.metaData()); }); + + connect(this, &BufferedAudioPlayer::playlistIndexChanged, this, + [](int index) { qCDebug(gmAudioBufferedPlayer()) << index; }); +} + +auto BufferedAudioPlayer::playlistQml() -> QQmlListProperty +{ + return m_playlist->filesQml(this); +} + +auto BufferedAudioPlayer::element() const -> QPointer +{ + return m_element; +} + +void BufferedAudioPlayer::setIndex(int index) +{ + playlistIndex(index); + + if (Utils::isInBounds(m_playlist->files(), index)) + { + const auto *file = m_playlist->at(index); + + if (file) + { + loadMedia(*file); + } + else + { + next(); + } + } +} + +void BufferedAudioPlayer::play(AudioElement *element) +{ + m_mediaPlayer.stop(); + playlistIndex(0); + + m_element = element; + + if (!m_element) + { + qCWarning(gmAudioBufferedPlayer()) << "Error: Could not play element, it is null"; + return; + } + + loadPlaylist().then(this, [this]() { startPlaying(); }); +} + +void BufferedAudioPlayer::play() +{ + m_mediaPlayer.play(); +} + +void BufferedAudioPlayer::play(const QByteArray &data) +{ + m_mediaPlayer.stop(); + + if (m_mediaBuffer) + { + m_mediaBuffer->close(); + } + + m_mediaBuffer = std::make_unique(); + m_mediaBuffer->setData(data); + m_mediaBuffer->open(QIODevice::ReadOnly); + m_mediaPlayer.setSourceDevice(m_mediaBuffer.get()); + + state(AudioPlayer::State::Playing); + + m_audioOutput.setMuted(false); + m_mediaPlayer.play(); + + emit currentFileChanged(m_playlist->at(playlistIndex())->url(), data); +} + +auto BufferedAudioPlayer::loadPlaylist() -> QFuture +{ + m_playlist->setFiles(m_element->files()); + return m_playlist->resolve().then([this]() { applyShuffleMode(); }); +} + +void BufferedAudioPlayer::handleUnsupportedMediaSource(const AudioFile &file) +{ + qCWarning(gmAudioBufferedPlayer()) << "Media type" << file.source() << "is currently not supported."; + next(); +} + +void BufferedAudioPlayer::onFileReceived(std::shared_ptr result) +{ + if (!result) return; + + if (result->data().isEmpty()) + { + next(); + return; + } + + play(result->data()); +} + +auto BufferedAudioPlayer::fileSource() const -> AudioFile::Source +{ + return m_currentFileSource; +} + +void BufferedAudioPlayer::setPlaylist(std::unique_ptr playlist) +{ + m_playlist = std::move(playlist); + emit playlistChanged(); +} + +void BufferedAudioPlayer::pause() +{ + if (state() == AudioPlayer::State::Paused) return; + + m_mediaPlayer.pause(); + state(AudioPlayer::State::Paused); +} + +void BufferedAudioPlayer::stop() +{ + if (state() == AudioPlayer::State::Stopped) return; + + m_mediaPlayer.stop(); + state(AudioPlayer::State::Stopped); + + m_playlist->clear(); +} + +void BufferedAudioPlayer::setVolume(int linear, int logarithmic) +{ + Q_UNUSED(linear) + m_audioOutput.setVolume(normalizeVolume(logarithmic)); +} + +void BufferedAudioPlayer::again() +{ + m_mediaPlayer.setPosition(0); +} + +void BufferedAudioPlayer::next() +{ + if (!m_element || m_playlist->isEmpty()) + { + stop(); + return; + } + + // Complete random + if (m_element->mode() == AudioElement::Mode::Random) + { + auto index = QRandomGenerator::system()->bounded(m_playlist->length()); + setIndex(index); + return; + } + + // choose next in line (mode RandomList, ListLoop, ListOnce) + if (playlistIndex() + 1 < m_playlist->length()) + { + setIndex(playlistIndex() + 1); + return; + } + + // loop around (mode RandomList or ListLoop) + if (m_element->mode() != AudioElement::Mode::ListOnce) + { + setIndex(0); + return; + } + + // reached end of playlist, stop + stop(); +} + +void BufferedAudioPlayer::onMediaPlayerPlaybackStateChanged(QMediaPlayer::PlaybackState newState) +{ + qCDebug(gmAudioBufferedPlayer) << "Media player playback state changed:" << newState; + if (newState == QMediaPlayer::PlayingState) state(State::Playing); +} + +void BufferedAudioPlayer::onMediaStatusChanged(QMediaPlayer::MediaStatus status) +{ + qCDebug(gmAudioBufferedPlayer()) << "Status changed:" << status; + + switch (status) + { + case QMediaPlayer::EndOfMedia: + qCDebug(gmAudioBufferedPlayer()) << "End of media was reached, playing next file ..."; + next(); + break; + case QMediaPlayer::BufferingMedia: + state(State::Loading); + break; + default: + break; + } +} + +void BufferedAudioPlayer::onMediaPlayerErrorOccurred(QMediaPlayer::Error error, const QString &errorString) +{ + qCWarning(gmAudioBufferedPlayer()) << error << errorString; + + if (error != QMediaPlayer::NoError) + { + next(); + } +} + +void BufferedAudioPlayer::startPlaying() +{ + if (!m_element || m_playlist->isEmpty()) return; + + if (m_element->mode() == AudioElement::Mode::Random) + { + next(); + return; + } + + setIndex(0); +} + +void BufferedAudioPlayer::loadMedia(const AudioFile &file) +{ + qCDebug(gmAudioBufferedPlayer()) << "Loading media (" << file.url() << ") ..."; + + m_mediaPlayer.stop(); + state(AudioPlayer::State::Loading); + + m_currentFileSource = file.source(); + + switch (m_currentFileSource) + { + case AudioFile::Source::File: + loadLocalFile(file); + break; + case AudioFile::Source::Web: + loadWebFile(file); + break; + default: + handleUnsupportedMediaSource(file); + break; + } +} + +void BufferedAudioPlayer::loadLocalFile(const AudioFile &file) +{ + m_fileRequestContext = std::make_unique(); + + const auto path = FileUtils::fileInDir(file.url(), SettingsManager::getPath(m_settingsId)); + const auto callback = [this](std::shared_ptr result) { onFileReceived(result); }; + + Files::File::getDataAsync(path).then(m_fileRequestContext.get(), callback); +} + +void BufferedAudioPlayer::loadWebFile(const AudioFile &file) +{ + m_mediaPlayer.setSource(QUrl(file.url())); + m_mediaPlayer.play(); + m_audioOutput.setMuted(false); +} + +void BufferedAudioPlayer::applyShuffleMode() +{ + qCDebug(gmAudioBufferedPlayer()) << "Applying shuffle mode" << m_element->mode(); + + if (m_element->mode() != AudioElement::Mode::RandomList) return; + + m_playlist->shuffle(); + emit playlistChanged(); +} diff --git a/src/tools/audio/players/bufferedaudioplayer.h b/src/tools/audio/players/bufferedaudioplayer.h new file mode 100644 index 00000000..2c266a70 --- /dev/null +++ b/src/tools/audio/players/bufferedaudioplayer.h @@ -0,0 +1,84 @@ +#pragma once + +#include "../playlist/resolvingaudioplaylist.h" +#include "../project/audioelement.h" +#include "audioplayer.h" +#include "thirdparty/propertyhelper/PropertyHelper.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Files +{ +class FileDataResult; +} + +class BufferedAudioPlayer : public AudioPlayer +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + + Q_PROPERTY(QQmlListProperty playlist READ playlistQml NOTIFY playlistChanged FINAL) + AUTO_PROPERTY_VAL2(int, playlistIndex, 0); + +public: + explicit BufferedAudioPlayer(const QString &settingsId, QNetworkAccessManager &networkManager, QObject *parent); + + [[nodiscard]] auto playlistQml() -> QQmlListProperty; + [[nodiscard]] auto element() const -> QPointer; + + void setIndex(int index); + +public slots: + virtual void play(AudioElement *element); + void play() override; + void pause() override; + void stop() override; + void setVolume(int linear, int logarithmic) override; + void again() override; + void next() override; + +signals: + void playlistChanged(); + void metaDataChanged(const QMediaMetaData &data); + void currentFileChanged(const QString &path, const QByteArray &data); + +protected: + void play(const QByteArray &data); + auto loadPlaylist() -> QFuture; + virtual void handleUnsupportedMediaSource(const AudioFile &file); + void onFileReceived(std::shared_ptr result); + + [[nodiscard]] auto fileSource() const -> AudioFile::Source; + void setPlaylist(std::unique_ptr playlist); + + void loadMedia(const AudioFile &file); + void loadLocalFile(const AudioFile &file); + void loadWebFile(const AudioFile &file); + +private slots: + void onMediaPlayerPlaybackStateChanged(QMediaPlayer::PlaybackState newState); + void onMediaStatusChanged(QMediaPlayer::MediaStatus status); + void onMediaPlayerErrorOccurred(QMediaPlayer::Error error, const QString &errorString); + +private: + void startPlaying(); + void applyShuffleMode(); + + QString m_settingsId; + QMediaPlayer m_mediaPlayer; + QAudioOutput m_audioOutput; + std::unique_ptr m_mediaBuffer = nullptr; + std::unique_ptr m_playlist = nullptr; + + QPointer m_element = nullptr; + AudioFile::Source m_currentFileSource = AudioFile::Source::Unknown; + + std::unique_ptr m_fileRequestContext = nullptr; +}; diff --git a/src/tools/audio/players/musicplayer.cpp b/src/tools/audio/players/musicplayer.cpp index 3f68b4db..954c61cd 100644 --- a/src/tools/audio/players/musicplayer.cpp +++ b/src/tools/audio/players/musicplayer.cpp @@ -1,527 +1,109 @@ #include "musicplayer.h" -#include "filesystem/file.h" -#include "filesystem/results/filedataresult.h" -#include "services/spotify/spotify.h" -#include "services/spotify/spotifyutils.h" -#include "settings/settingsmanager.h" -#include "utils/fileutils.h" #include -#include -#include using namespace Qt::Literals::StringLiterals; Q_LOGGING_CATEGORY(gmAudioMusic, "gm.audio.music") -MusicPlayer::MusicPlayer(MetaDataReader &metaDataReader, QObject *parent) - : AudioPlayer(parent), m_spotifyPlayer(metaDataReader) +MusicPlayer::MusicPlayer(QNetworkAccessManager &networkManager, MetaDataReader &metaDataReader, QObject *parent) + : BufferedAudioPlayer(u"music"_s, networkManager, parent), m_spotifyPlayer(metaDataReader) { - m_mediaPlayer.setObjectName(tr("Music")); - m_mediaPlayer.setAudioOutput(&m_audioOutput); - connect(&m_spotifyPlayer, &SpotifyPlayer::songEnded, this, &MusicPlayer::onSpotifySongEnded); - - connectPlaybackStateSignals(); - connectMetaDataSignals(metaDataReader); + connect(&m_spotifyPlayer, &SpotifyPlayer::stateChanged, this, &MusicPlayer::onSpotifyStateChanged); + connect(this, &MusicPlayer::stateChanged, this, &MusicPlayer::onStateChanged); + connect( + this, &MusicPlayer::currentFileChanged, this, + [&metaDataReader](const QString &path, const QByteArray &data) { metaDataReader.loadMetaData(path, data); }); } -/** - * @brief Play music list - * @param name Name of the element - */ void MusicPlayer::play(AudioElement *element) { - if (!element) return; - - qCDebug(gmAudioMusic) << "Playing music list:" << element->name(); - - m_mediaPlayer.stop(); - m_mediaPlayer.setObjectName(tr("Music") + ": " + element->name()); - - playlistIndex(0); - - m_currentElement = element; - a_playlist = element->files(); - - emit metaDataChanged(&m_mediaPlayer); + m_spotifyPlayer.stop(); - loadPlaylist().then(this, [this]() { startPlaying(); }); + BufferedAudioPlayer::play(element); } -/** - * @brief Start playing music - */ void MusicPlayer::play() { - qCDebug(gmAudioMusic) << "Continue play of MusicPlayer ..."; - - switch (m_currentFileSource) + if (fileSource() == AudioFile::Source::Spotify) { - case AudioFile::Source::Spotify: m_spotifyPlayer.play(); - emit startedPlaying(); - break; - - default: - m_mediaPlayer.play(); - break; - } -} - -void MusicPlayer::startPlaying() -{ - qCDebug(gmAudioMusic) << "startPlaying()"; - - if (m_currentElement && !playlist().isEmpty()) - { - if (m_currentElement->mode() == AudioElement::Mode::Random) - { - next(); - return; - } - - const auto &playlist = this->playlist(); - loadMedia(playlist.first()); - } -} - -void MusicPlayer::printPlaylist() const -{ - const auto &playlist = this->playlist(); - for (const auto *audioFile : playlist) - { - qCDebug(gmAudioMusic) << audioFile->url(); - } -} - -auto MusicPlayer::loadPlaylist() -> QFuture -{ - qCDebug(gmAudioMusic()) << "loadPlaylist()"; - - clearPlaylist(); - - if (m_playlistLoadingContext) m_playlistLoadingContext->deleteLater(); - m_playlistLoadingContext = new QObject(this); - - return loadPlaylistRecursive(); -} - -void MusicPlayer::clearPlaylist() -{ - const auto playlist = this->playlist(); - - for (auto *entry : playlist) - { - if (entry->parent() == this) entry->deleteLater(); - } - - a_playlist.clear(); - emit playlistChanged(a_playlist); -} - -void MusicPlayer::loadTrackNamesAsync() const -{ - QList spotifyTracks; - - const auto &playlist = this->playlist(); - for (auto *track : playlist) - { - if (track->title().isEmpty()) - { - switch (track->source()) - { - case AudioFile::Source::Spotify: - if (SpotifyUtils::getUriType(track->url()) == SpotifyUtils::SpotifyType::Track) spotifyTracks << track; - break; - case AudioFile::Source::Youtube: - track->title(tr("[BROKEN] %1").arg(track->url())); - qCWarning(gmAudioMusic()) << "Youtube integration is currently broken!"; - break; - default: - break; - } - } - } - - loadSpotifyTrackNamesAsync(spotifyTracks); -} - -void MusicPlayer::loadSpotifyTrackNamesAsync(const QList &files) -{ - if (files.isEmpty()) return; - - QStringList trackIds; - trackIds.reserve(files.length()); - - for (const auto *track : files) - { - trackIds << SpotifyUtils::getIdFromUri(track->url()); - } - - const auto callback = [files](const std::vector> &tracks) { - for (size_t i = 0; i < tracks.size(); i++) - { - files[i]->title(tracks[i]->name); - } - }; - - Spotify::instance()->tracks->getTracks(trackIds).then(Spotify::instance(), callback); -} - -/// Load all entries of a playlist by "expanding" entries like spotify playlists and albums -auto MusicPlayer::loadPlaylistRecursive(int index) -> QFuture -{ - if (index >= m_currentElement->files().length()) - { - applyShuffleMode(); - loadTrackNamesAsync(); - emit playlistChanged(a_playlist); - return QtFuture::makeReadyFuture(); - } - - const auto &files = m_currentElement->files(); - auto *audioFile = files[index]; - - switch (audioFile->source()) - { - case AudioFile::Source::Spotify: - return loadPlaylistRecursiveSpotify(index, audioFile); - case AudioFile::Source::Youtube: - qCWarning(gmAudioMusic()) << "Youtube integration is currently broken"; - break; - default: - a_playlist.append(audioFile); - break; - } - - return loadPlaylistRecursive(index + 1); -} - -auto MusicPlayer::loadPlaylistRecursiveSpotify(int index, AudioFile *audioFile) -> QFuture -{ - const auto uri = SpotifyUtils::makeUri(audioFile->url()); - const auto type = SpotifyUtils::getUriType(uri); - const auto id = SpotifyUtils::getIdFromUri(uri); - - if (SpotifyUtils::isContainerType(type)) - { - const auto callback = [this, index](const QSharedPointer &tracks) { - for (const auto &track : qAsConst(tracks->tracks)) - { - if (!track->isPlayable) - { - qCDebug(gmAudioMusic()) << "Spotify track" << track->name << track->album->name << track->uri - << "is not playable -> ignoring it"; - continue; - } - - switch (SpotifyUtils::getUriType(track->uri)) - { - case SpotifyUtils::SpotifyType::Track: - case SpotifyUtils::SpotifyType::Episode: - a_playlist << new AudioFile(track->uri, AudioFile::Source::Spotify, track->name, this); - default: - break; - } - } - - return loadPlaylistRecursive(index + 1); - }; - - switch (type) - { - case SpotifyUtils::SpotifyType::Playlist: - return Spotify::instance()->playlists->getPlaylistTracks(id).then(this, callback).unwrap(); - case SpotifyUtils::SpotifyType::Album: - return Spotify::instance()->albums->getAlbumTracks(id).then(this, callback).unwrap(); - default: - qCCritical(gmAudioMusic()) << "loadPlaylistRecursiveSpotify(): not implemented for container type" - << (int)type; - } - } - - a_playlist << audioFile; - return loadPlaylistRecursive(index + 1); -} - -/// Shuffle playlist randomly if required -void MusicPlayer::applyShuffleMode() -{ - qCDebug(gmAudioMusic) << "Applying shuffle mode" << m_currentElement->mode(); - if (m_currentElement->mode() != AudioElement::Mode::RandomList) return; - - qCDebug(gmAudioMusic) << "Unshuffled playlist:"; - printPlaylist(); - - QList temp; - - while (!a_playlist.isEmpty()) - { - temp.append(a_playlist.takeAt(QRandomGenerator::global()->bounded(a_playlist.size()))); - } - a_playlist = temp; - - qCDebug(gmAudioMusic) << "Shuffled playlist:"; - printPlaylist(); -} - -void MusicPlayer::loadMedia(AudioFile *file) -{ - qCDebug(gmAudioMusic) << "Loading media (" << file->url() << ") ..."; - - m_mediaPlayer.stop(); - m_spotifyPlayer.stop(); - m_currentFileSource = file->source(); - emit currentIndexChanged(); - emit clearMetaData(); - - switch (m_currentFileSource) - { - case AudioFile::Source::File: - loadLocalFile(file); - break; - case AudioFile::Source::Web: - loadWebFile(file); - break; - case AudioFile::Source::Spotify: - loadSpotifyFile(file); - break; - case AudioFile::Source::Youtube: - loadYoutubeFile(file); - break; - default: - qCCritical(gmAudioMusic()) << "loadMedia() not implemented for type" << m_currentFileSource; - next(); - break; + state(State::Playing); + return; } -} - -void MusicPlayer::loadLocalFile(AudioFile *file) -{ - if (m_fileRequestContext) m_fileRequestContext->deleteLater(); - m_fileRequestContext = new QObject(this); - - m_fileName = file->url(); - - const auto path = FileUtils::fileInDir(file->url(), SettingsManager::getPath(u"music"_s)); - const auto callback = [this](std::shared_ptr result) { onFileReceived(result); }; - - Files::File::getDataAsync(path).then(m_fileRequestContext, callback); -} - -void MusicPlayer::loadWebFile(AudioFile *file) -{ - m_mediaPlayer.setSource(QUrl(file->url())); - m_mediaPlayer.play(); - m_audioOutput.setMuted(false); -} -void MusicPlayer::loadSpotifyFile(AudioFile *file) -{ - m_spotifyPlayer.play(file->url()); - emit startedPlaying(); -} - -void MusicPlayer::loadYoutubeFile(AudioFile *file) const -{ - Q_UNUSED(file) - qCDebug(gmAudioMusic) << "Media is a youtube video ..."; - qCCritical(gmAudioMusic()) << "Youtube integration is currently broken"; + BufferedAudioPlayer::play(); } -/** - * @brief Pause the music playback - */ void MusicPlayer::pause() { - qCDebug(gmAudioMusic) << "Pausing MusicPlayer ..."; - - switch (m_currentFileSource) + switch (fileSource()) { case AudioFile::Source::Spotify: m_spotifyPlayer.pause(); break; default: - m_mediaPlayer.pause(); + BufferedAudioPlayer::pause(); break; } } -/** - * @brief Stop playing music - */ void MusicPlayer::stop() { - qCDebug(gmAudioMusic) << "Stopping MusicPlayer ..."; - m_spotifyPlayer.stop(); - m_mediaPlayer.stop(); - - clearPlaylist(); -} - -void MusicPlayer::next() -{ - emit clearMetaData(); - - if (!m_currentElement || a_playlist.isEmpty()) return; - - if (m_currentElement->mode() == AudioElement::Mode::Random) - { - playlistIndex(QRandomGenerator::system()->bounded(a_playlist.length())); - } - else - { - // choose next in line (mode 0, 2, 3) - if (playlistIndex() < a_playlist.length() - 1) - { - playlistIndex(playlistIndex() + 1); - } - // loop around (Mode 0 or 2) - else if (m_currentElement->mode() != AudioElement::Mode::ListOnce) - { - playlistIndex(0); - } - } - - loadMedia(a_playlist.at(playlistIndex())); + BufferedAudioPlayer::stop(); } void MusicPlayer::again() { - switch (m_currentFileSource) + switch (fileSource()) { case AudioFile::Source::Spotify: m_spotifyPlayer.again(); break; default: - m_mediaPlayer.setPosition(0); + BufferedAudioPlayer::again(); break; } } -void MusicPlayer::setIndex(int index) -{ - playlistIndex(index); - - const auto &playlist = this->playlist(); - loadMedia(playlist[index]); -} - -auto MusicPlayer::playlistQml() -> QQmlListProperty -{ - return QQmlListProperty(this, &a_playlist); -} - void MusicPlayer::setVolume(int linear, int logarithmic) { - m_audioOutput.setVolume(normalizeVolume(logarithmic)); + BufferedAudioPlayer::setVolume(linear, logarithmic); m_spotifyPlayer.setVolume(linear, logarithmic); } -void MusicPlayer::onMediaPlayerPlaybackStateChanged(QMediaPlayer::PlaybackState newState) -{ - qCDebug(gmAudioMusic) << "Media player playback state changed:" << newState; - if (newState == QMediaPlayer::PlayingState) emit startedPlaying(); -} - -void MusicPlayer::onMediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus status) +void MusicPlayer::handleUnsupportedMediaSource(const AudioFile &file) { - switch (status) + switch (file.source()) { - case QMediaPlayer::EndOfMedia: - qCDebug(gmAudioMusic()) << "End of media was reached, starting next song ..."; - next(); + case AudioFile::Source::Spotify: + loadSpotifyFile(file); break; - case QMediaPlayer::BufferedMedia: - emit metaDataChanged(&m_mediaPlayer); + case AudioFile::Source::Youtube: + loadYoutubeFile(file); break; default: + qCCritical(gmAudioMusic()) << "loadMedia() is not implemented for type" << file.source(); + next(); break; } } -void MusicPlayer::onMediaPlayerErrorOccurred(QMediaPlayer::Error error, const QString &errorString) +void MusicPlayer::loadSpotifyFile(const AudioFile &file) { - qCWarning(gmAudioMusic()) << error << errorString; - - if (error != QMediaPlayer::NoError) - { - next(); - } + m_spotifyPlayer.play(file.url()); + state(State::Playing); } -void MusicPlayer::onFileReceived(std::shared_ptr result) +void MusicPlayer::loadYoutubeFile(const AudioFile &file) const { - if (!result) return; - - qCDebug(gmAudioMusic()) << "Received file ..."; - - // On Windows a there seems to be a weird issue with - // a strange clicking noise at the beginning of files. - // Muting the mediaPlayer and unmuting about 100ms into - // the song seems to be a workaround. - auto clickingWorkaround = false; - -#ifdef Q_OS_WIN - clickingWorkaround = true; -#endif - - if (clickingWorkaround) m_audioOutput.setMuted(true); - m_mediaPlayer.stop(); - - if (result->data().isEmpty()) - { - qCWarning(gmAudioMusic()) << "File is empty, skipping ..."; - next(); - return; - } - -#ifdef Q_OS_WIN - QFile file(m_tempDir.path() + "/" + FileUtils::fileName(m_fileName)); - - if (!file.open(QIODevice::WriteOnly)) - { - if (file.error() == QFileDevice::OpenError) - { - file.setFileName(FileUtils::incrementFileName(file.fileName())); - if (!file.open(QIODevice::WriteOnly)) - { - qCWarning(gmAudioMusic()) << "Error: Could not open temporary file even after incrementing the filename" - << file.fileName() << file.errorString(); - return; - } - } - else - { - qCWarning(gmAudioMusic()) << "Error: Could not open temporary file:" << file.fileName() - << file.errorString(); - return; - } - } - - qCDebug(gmAudioMusic()) << file.fileName(); - file.write(result->data()); - file.close(); - - m_mediaPlayer.setSource(QUrl::fromLocalFile(file.fileName())); -#else - m_mediaBuffer.close(); - m_mediaBuffer.setData(result->data()); - m_mediaBuffer.open(QIODevice::ReadOnly); - m_mediaPlayer.setSourceDevice(&m_mediaBuffer); -#endif - - m_mediaPlayer.play(); - - if (clickingWorkaround) QTimer::singleShot(100, this, [this]() { m_audioOutput.setMuted(false); }); - - qCDebug(gmAudioMusic()) << "Sending file data to metadatareader ..."; - - emit metaDataChanged(result->data()); + Q_UNUSED(file) + qCDebug(gmAudioMusic) << "Media is a youtube video ..."; + qCCritical(gmAudioMusic()) << "Youtube integration is currently broken"; } void MusicPlayer::onSpotifySongEnded() @@ -530,26 +112,24 @@ void MusicPlayer::onSpotifySongEnded() next(); } -void MusicPlayer::connectPlaybackStateSignals() const +void MusicPlayer::onSpotifyStateChanged(State state) { - connect(&m_mediaPlayer, &QMediaPlayer::playbackStateChanged, this, &MusicPlayer::onMediaPlayerPlaybackStateChanged); - connect(&m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &MusicPlayer::onMediaPlayerMediaStatusChanged); - connect(&m_mediaPlayer, &QMediaPlayer::errorOccurred, this, &MusicPlayer::onMediaPlayerErrorOccurred); + if (fileSource() != AudioFile::Source::Spotify) return; + + this->state(state); } -void MusicPlayer::connectMetaDataSignals(MetaDataReader &metaDataReader) const +void MusicPlayer::onStateChanged(State state) { - connect(&m_mediaPlayer, &QMediaPlayer::metaDataChanged, this, - [this, &metaDataReader]() { metaDataReader.updateMetaData(m_mediaPlayer.metaData()); }); - - connect(this, QOverload::of(&MusicPlayer::metaDataChanged), &metaDataReader, - QOverload::of(&MetaDataReader::updateMetaData)); - - connect(this, QOverload::of(&MusicPlayer::metaDataChanged), &metaDataReader, - QOverload::of(&MetaDataReader::updateMetaData)); - - connect(this, QOverload::of(&MusicPlayer::metaDataChanged), &metaDataReader, - QOverload::of(&MetaDataReader::updateMetaData)); + if (fileSource() == AudioFile::Source::Spotify) return; - connect(this, &MusicPlayer::clearMetaData, &metaDataReader, &MetaDataReader::clearMetaData); + switch (state) + { + case AudioPlayer::State::Playing: + case AudioPlayer::State::Loading: + m_spotifyPlayer.stop(); + break; + default: + break; + } } diff --git a/src/tools/audio/players/musicplayer.h b/src/tools/audio/players/musicplayer.h index c0b2d165..388cb335 100644 --- a/src/tools/audio/players/musicplayer.h +++ b/src/tools/audio/players/musicplayer.h @@ -1,98 +1,40 @@ #pragma once #include "../project/audioelement.h" -#include "audioplayer.h" +#include "bufferedaudioplayer.h" #include "spotifyplayer.h" -#include "thirdparty/propertyhelper/PropertyHelper.h" -#include -#include #include -#include -#include - -#ifdef Q_OS_WIN -#include -#endif +#include namespace Files { class FileDataResult; } -class MusicPlayer : public AudioPlayer +class MusicPlayer : public BufferedAudioPlayer { Q_OBJECT public: - explicit MusicPlayer(MetaDataReader &metaDataReader, QObject *parent = nullptr); - - void play(AudioElement *element); - void setIndex(int index); - - [[nodiscard]] auto playlistQml() -> QQmlListProperty; - - AUTO_PROPERTY_VAL2(int, playlistIndex, 0); - READ_PROPERTY(QList, playlist) + explicit MusicPlayer(QNetworkAccessManager &networkManager, MetaDataReader &metaDataReader, + QObject *parent = nullptr); public slots: + void play(AudioElement *element) override; void play() override; void pause() override; void stop() override; void setVolume(int linear, int logarithmic) override; - void next() override; void again() override; private: - SpotifyPlayer m_spotifyPlayer; - QMediaPlayer m_mediaPlayer; - QAudioOutput m_audioOutput; - AudioElement *m_currentElement = nullptr; - - /// Context object to easily stop file data requests by deleting the object - QObject *m_fileRequestContext = nullptr; - QObject *m_playlistLoadingContext = nullptr; - - AudioFile::Source m_currentFileSource = AudioFile::Source::Unknown; - - QBuffer m_mediaBuffer; - QString m_fileName; + void handleUnsupportedMediaSource(const AudioFile &file) override; + void loadSpotifyFile(const AudioFile &file); + void loadYoutubeFile(const AudioFile &file) const; -#ifdef Q_OS_WIN - QTemporaryDir m_tempDir; -#endif - - void loadMedia(AudioFile *file); - void loadLocalFile(AudioFile *file); - void loadWebFile(AudioFile *file); - void loadSpotifyFile(AudioFile *file); - void loadYoutubeFile(AudioFile *file) const; - - auto loadPlaylist() -> QFuture; - void clearPlaylist(); - void loadTrackNamesAsync() const; - static void loadSpotifyTrackNamesAsync(const QList &tracks); - - auto loadPlaylistRecursive(int index = 0) -> QFuture; - auto loadPlaylistRecursiveSpotify(int index, AudioFile *audioFile) -> QFuture; - void applyShuffleMode(); - void startPlaying(); - - void connectPlaybackStateSignals() const; - void connectMetaDataSignals(MetaDataReader &metaDataReader) const; - - void printPlaylist() const; + SpotifyPlayer m_spotifyPlayer; private slots: - void onMediaPlayerPlaybackStateChanged(QMediaPlayer::PlaybackState newState); - void onMediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus status); - void onMediaPlayerErrorOccurred(QMediaPlayer::Error error, const QString &errorString); - void onFileReceived(std::shared_ptr result); void onSpotifySongEnded(); - -signals: - void startedPlaying(); - void clearMetaData(); - void metaDataChanged(QMediaPlayer *mediaPlayer); - void metaDataChanged(const QString &key, const QVariant &value); - void metaDataChanged(const QByteArray &data); - void currentIndexChanged(); + void onSpotifyStateChanged(AudioPlayer::State state); + void onStateChanged(AudioPlayer::State state); }; diff --git a/src/tools/audio/players/radioplayer.cpp b/src/tools/audio/players/radioplayer.cpp index 9f8dadef..a548d171 100644 --- a/src/tools/audio/players/radioplayer.cpp +++ b/src/tools/audio/players/radioplayer.cpp @@ -1,234 +1,17 @@ #include "radioplayer.h" -#include "../playlist/audioplaylist.h" -#include "filesystem/file.h" -#include "filesystem/results/filedataresult.h" -#include "settings/settingsmanager.h" -#include "utils/fileutils.h" #include -#include using namespace Qt::Literals::StringLiterals; -static constexpr auto BUFFER_FULL = 100; - Q_LOGGING_CATEGORY(gmAudioRadio, "gm.audio.radio") RadioPlayer::RadioPlayer(QNetworkAccessManager &networkManager, MetaDataReader &metaDataReader, QObject *parent) - : AudioPlayer(parent), m_networkManager(networkManager) -{ - qCDebug(gmAudioRadio()) << "Loading RadioPlayer ..."; - - m_mediaPlayer.setObjectName(tr("Radio")); - m_mediaPlayer.setAudioOutput(&m_audioOutput); - - connect(&m_mediaPlayer, &QMediaPlayer::playbackStateChanged, this, &RadioPlayer::onMediaPlayerPlaybackStateChanged); - connect(&m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &RadioPlayer::onMediaPlayerMediaStatusChanged); - connect(&m_mediaPlayer, &QMediaPlayer::errorOccurred, this, &RadioPlayer::onMediaPlayerErrorOccurred); - - // MetaData - connect(&m_mediaPlayer, &QMediaPlayer::metaDataChanged, this, &RadioPlayer::onMetaDataChanged); - connect(this, &RadioPlayer::metaDataChanged, &metaDataReader, - QOverload::of(&MetaDataReader::updateMetaData)); -} - -/** - * @brief Play a radio element - * @param element RadioElement to be played - */ -void RadioPlayer::play(AudioElement *element) -{ - if (!element) return; - - qCDebug(gmAudioRadio()) << "Playing radio element:" << element->name(); - - m_mediaPlayer.stop(); - m_mediaPlayer.setObjectName(tr("Radio") + ": " + element->name()); - m_currentElement = element; - - if (element->files().empty()) return; - - const auto *audioFile = element->files().constFirst(); - play(audioFile); -} - -void RadioPlayer::play(const AudioFile *audioFile) -{ - m_fileName = audioFile->url(); - - if (audioFile->source() == AudioFile::Source::File) - { - qCDebug(gmAudioRadio()) << "Playing radio from local playlist:" << audioFile->url() << "..."; - - if (m_fileRequestContext) m_fileRequestContext->deleteLater(); - m_fileRequestContext = new QObject(this); - - m_fileName = audioFile->url(); - Files::File::getDataAsync(FileUtils::fileInDir(audioFile->url(), SettingsManager::getPath(u"radio"_s))) - .then(m_fileRequestContext, - [this](std::shared_ptr result) { onFileReceived(result); }); - return; - } - - qCDebug(gmAudioRadio()) << "Playing radio from url:" << audioFile->url(); - - if (isPlaylist(audioFile->url())) - { - auto *reply = m_networkManager.get(QNetworkRequest(QUrl(audioFile->url()))); - QtFuture::connect(reply, &QNetworkReply::finished).then(this, [this, reply]() { - onFileReceived(std::make_shared(reply->readAll())); - reply->deleteLater(); - }); - return; - } - - m_mediaPlayer.setSource(QUrl(audioFile->url())); - m_audioOutput.setMuted(false); - m_mediaPlayer.play(); -} - -/** - * @brief Start playing the radio element - */ -void RadioPlayer::play() -{ - qCDebug(gmAudioRadio()) << "Continue play of RadioPlayer ..."; - m_mediaPlayer.play(); -} - -/** - * @brief Pause the playback of the radio element - */ -void RadioPlayer::pause() -{ - qCDebug(gmAudioRadio()) << "Pausing RadioPlayer ..."; - m_mediaPlayer.pause(); -} - -/** - * @brief Stop radio playback - */ -void RadioPlayer::stop() -{ - qCDebug(gmAudioRadio()) << "Stopping RadioPlayer ..."; - - m_mediaPlayer.stop(); -} - -void RadioPlayer::setVolume(int linear, int logarithmic) -{ - Q_UNUSED(linear) - m_audioOutput.setVolume(normalizeVolume(logarithmic)); -} - -auto RadioPlayer::isPlaylist(const QString &file) -> bool -{ - return file.endsWith(".m3u"_L1) || file.endsWith(".m3u8"_L1) || file.endsWith(".pls"_L1); -} - -void RadioPlayer::playAudio(const QByteArray &data) -{ -#ifdef Q_OS_WIN - QFile file(m_tempDir.path() + "/" + FileUtils::fileName(m_fileName)); - - if (!file.open(QIODevice::WriteOnly)) - { - if (file.error() == QFileDevice::OpenError) - { - file.setFileName(FileUtils::incrementFileName(file.fileName())); - if (!file.open(QIODevice::WriteOnly)) - { - qCWarning(gmAudioRadio()) << "Error: Could not open temporary file even after incrementing the filename" - << file.fileName() << file.errorString(); - return; - } - } - else - { - qCWarning(gmAudioRadio()) << "Error: Could not open temporary file:" << file.fileName() - << file.errorString(); - return; - } - } - - qCWarning(gmAudioRadio()) << file.fileName(); - file.write(data); - file.close(); - - m_mediaPlayer.setSource(QUrl::fromLocalFile(file.fileName())); -#else - m_mediaBuffer.close(); - m_mediaBuffer.setData(data); - m_mediaBuffer.open(QIODevice::ReadOnly); - m_mediaPlayer.setSourceDevice(&m_mediaBuffer); -#endif - - m_audioOutput.setMuted(false); - m_mediaPlayer.play(); -} - -void RadioPlayer::playPlaylist(const QByteArray &data) -{ - const AudioPlaylist playlist(data, this); - if (playlist.isEmpty()) - { - qCWarning(gmAudioRadio()) << "Playlist is empty"; - return; - } - - play(playlist.files().constFirst()); -} - -/** - * @brief Tell MetaDataReader that there is new MetaData available - */ -void RadioPlayer::onMetaDataChanged() -{ - qCDebug(gmAudioRadio()) << "MetaData changed!"; - - if (m_mediaPlayer.bufferProgress() >= BUFFER_FULL || (m_mediaPlayer.mediaStatus() == QMediaPlayer::BufferedMedia)) - { - emit metaDataChanged(m_mediaPlayer.metaData()); - } -} - -void RadioPlayer::onFileReceived(std::shared_ptr result) -{ - if (!result) return; - - qCDebug(gmAudioRadio()) << "Received file ..."; - - m_mediaPlayer.stop(); - const auto data = result->data(); - - if (data.isEmpty()) - { - qCWarning(gmAudioRadio()) << "Error: File is empty!"; - return; - } - - if (data.isValidUtf8()) - { - qCDebug(gmAudioRadio()) << "Received file is utf-8, so probably a playlist file"; - playPlaylist(data); - return; - } - - playAudio(data); -} - -void RadioPlayer::onMediaPlayerPlaybackStateChanged(QMediaPlayer::PlaybackState newState) -{ - qCDebug(gmAudioRadio()) << "Playback state changed:" << newState; - - if (newState == QMediaPlayer::PlayingState) emit startedPlaying(); -} - -void RadioPlayer::onMediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus status) + : BufferedAudioPlayer(u"radio"_s, networkManager, parent), m_metaDataReader(metaDataReader) { - qCDebug(gmAudioRadio()) << "Media status changed:" << status; + connect(this, &RadioPlayer::metaDataChanged, this, &RadioPlayer::onMetaDataChanged); } -void RadioPlayer::onMediaPlayerErrorOccurred(QMediaPlayer::Error error, const QString &errorString) +void RadioPlayer::onMetaDataChanged(const QMediaMetaData &data) { - qCDebug(gmAudioRadio()) << "Error:" << error << errorString; + m_metaDataReader.setMetaData(data); } diff --git a/src/tools/audio/players/radioplayer.h b/src/tools/audio/players/radioplayer.h index 9a101f7e..17c62614 100644 --- a/src/tools/audio/players/radioplayer.h +++ b/src/tools/audio/players/radioplayer.h @@ -1,65 +1,22 @@ #pragma once #include "../metadata/metadatareader.h" -#include "../project/audioelement.h" -#include "audioplayer.h" -#include -#include -#include +#include "bufferedaudioplayer.h" #include -#include -namespace Files -{ -class FileDataResult; -} - -class RadioPlayer : public AudioPlayer +class RadioPlayer : public BufferedAudioPlayer { Q_OBJECT public: - RadioPlayer(QNetworkAccessManager &networkManager, MetaDataReader &metaDataReader, QObject *parent = nullptr); - -public slots: - void play(AudioElement *element); - void play() override; - void pause() override; - void stop() override; - void setVolume(int linear, int logarithmic) override; - void again() override - { - } - void next() override - { - } - -private: - QNetworkAccessManager &m_networkManager; - QMediaPlayer m_mediaPlayer; - QAudioOutput m_audioOutput; - QObject *m_fileRequestContext = nullptr; - - AudioElement *m_currentElement = nullptr; - - QBuffer m_mediaBuffer; - QTemporaryDir m_tempDir; - QString m_fileName; - - void play(const AudioFile *audioFile); - - static auto isPlaylist(const QString &file) -> bool; - - void playAudio(const QByteArray &data); - void playPlaylist(const QByteArray &data); + explicit RadioPlayer(QNetworkAccessManager &networkManager, MetaDataReader &metaDataReader, + QObject *parent = nullptr); signals: - void startedPlaying(); void metaDataChanged(const QMediaMetaData &metaData); private slots: - void onMetaDataChanged(); - void onFileReceived(std::shared_ptr result); - void onMediaPlayerPlaybackStateChanged(QMediaPlayer::PlaybackState newState); - void onMediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus status); - void onMediaPlayerErrorOccurred(QMediaPlayer::Error error, const QString &errorString); + void onMetaDataChanged(const QMediaMetaData &data); + +private: + MetaDataReader &m_metaDataReader; }; diff --git a/src/tools/audio/players/soundplayer.cpp b/src/tools/audio/players/soundplayer.cpp index cc158b54..c107c32f 100644 --- a/src/tools/audio/players/soundplayer.cpp +++ b/src/tools/audio/players/soundplayer.cpp @@ -1,229 +1,20 @@ #include "soundplayer.h" #include "../project/audioelement.h" -#include "filesystem/file.h" -#include "settings/settingsmanager.h" -#include "utils/fileutils.h" #include -#include -#include -#include using namespace Qt::Literals::StringLiterals; Q_LOGGING_CATEGORY(gmAudioSounds, "gm.audio.sounds") -SoundPlayer::SoundPlayer(AudioElement *element, int volume, QObject *parent) : AudioPlayer(parent), m_element(element) +SoundPlayer::SoundPlayer(QNetworkAccessManager &networkManager, QObject *parent) + : BufferedAudioPlayer(u"sounds"_s, networkManager, parent) { - if (!element) - { - qCWarning(gmAudioSounds()) << "Error: could not initialize soundplayer, element is null"; - return; - } - - m_mediaPlayer.setObjectName(element->name()); - m_mediaPlayer.setAudioOutput(&m_audioOutput); - m_audioOutput.setVolume(normalizeVolume(volume)); - - connect(&m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &SoundPlayer::onMediaStatusChanged); - connect(&m_mediaPlayer, &QMediaPlayer::errorOccurred, this, &SoundPlayer::onMediaPlayerErrorOccurred); - - m_playlist = element->files(); - applyShuffleMode(); -} - -void SoundPlayer::loadMedia(const AudioFile *file) -{ - qCDebug(gmAudioSounds()) << "Loading media" << file->url(); - - switch (file->source()) - { - case AudioFile::Source::File: { - m_fileRequestContext = std::make_unique(); - - m_fileName = file->url(); - Files::File::getDataAsync(FileUtils::fileInDir(file->url(), SettingsManager::getPath(u"sounds"_s))) - .then(m_fileRequestContext.get(), - [this](std::shared_ptr result) { onFileReceived(result); }); - break; - } - - case AudioFile::Source::Web: { - m_mediaPlayer.setSource(QUrl(file->url())); - m_mediaPlayer.play(); - m_audioOutput.setMuted(false); - m_fileName = file->url(); - break; - } - - case AudioFile::Source::Youtube: - // FIXME - default: - qCWarning(gmAudioSounds()) << "Media type" << file->source() << "is currently not supported."; - } -} - -void SoundPlayer::play() -{ - if (m_element->mode() == AudioElement::Mode::Random) - { - next(); - } - else - { - loadMedia(m_playlist.constFirst()); - } -} - -void SoundPlayer::pause() -{ - m_mediaPlayer.pause(); -} - -void SoundPlayer::stop() -{ - m_mediaPlayer.stop(); - emit playerStopped(this); -} - -void SoundPlayer::stopElement(const QString &element) -{ - if (m_element && (m_element->name() == element)) - { - qCDebug(gmAudioSounds()) << "Stopping" << element; - m_mediaPlayer.stop(); - emit playerStopped(this); - } -} - -void SoundPlayer::setVolume(int linear, int logarithmic) -{ - Q_UNUSED(linear) - m_audioOutput.setVolume(normalizeVolume(logarithmic)); -} - -void SoundPlayer::next() -{ - if (!m_element || (m_playlist.isEmpty())) - { - emit playerStopped(this); - return; - } - - // Complete random - if (m_element->mode() == AudioElement::Mode::Random) - { - m_playlistIndex = QRandomGenerator::system()->bounded(m_playlist.length()); - loadMedia(m_playlist.at(m_playlistIndex)); - return; - } - - // choose next in line (mode 0, 2, 3) - if (m_playlistIndex + 1 < m_playlist.length()) - { - loadMedia(m_playlist.at(++m_playlistIndex)); - } - - // loop around (Mode 0 or 2) - else if (m_element->mode() != AudioElement::Mode::ListOnce) - { - m_playlistIndex = 0; - loadMedia(m_playlist.constFirst()); - } - - // reached end of playlist, stop - else - { - emit playerStopped(this); - } } -void SoundPlayer::applyShuffleMode() +void SoundPlayer::stopElement(const QString &name) { - if (m_element->mode() != AudioElement::Mode::RandomList) return; - - QList temp; - - while (!m_playlist.isEmpty()) + if (element() && (element()->name() == name)) { - temp.append(m_playlist.takeAt(QRandomGenerator::global()->bounded(m_playlist.size()))); + stop(); } - m_playlist = temp; -} - -void SoundPlayer::onMediaStatusChanged(QMediaPlayer::MediaStatus status) -{ - qCDebug(gmAudioSounds()) << "Status changed:" << status; - - if (status == QMediaPlayer::EndOfMedia) - { - next(); - } -} - -void SoundPlayer::onMediaPlayerErrorOccurred(QMediaPlayer::Error error, const QString &errorString) -{ - qCWarning(gmAudioSounds()) << error << errorString; - - if (error == QMediaPlayer::FormatError) - { - next(); - } -} - -void SoundPlayer::onFileReceived(std::shared_ptr result) -{ - if (!result) return; - - m_mediaPlayer.stop(); - - if (result->data().isEmpty()) - { - next(); - return; - } - -#ifdef Q_OS_WIN - QFile file(m_tempDir.path() + "/" + FileUtils::fileName(m_fileName)); - - if (!file.open(QIODevice::WriteOnly)) - { - if (file.error() == QFileDevice::OpenError) - { - file.setFileName(FileUtils::incrementFileName(file.fileName())); - if (!file.open(QIODevice::WriteOnly)) - { - qCWarning(gmAudioSounds()) - << "Error: Could not open temporary file even after incrementing the filename" << file.fileName() - << file.errorString(); - return; - } - } - else - { - qCWarning(gmAudioSounds()) << "Error: Could not open temporary file:" << file.fileName() - << file.errorString(); - return; - } - } - - qCDebug(gmAudioSounds()) << file.fileName(); - file.write(result->data()); - file.close(); - - m_mediaPlayer.setSource(QUrl::fromLocalFile(file.fileName())); -#else - if (m_mediaBuffer) - { - m_mediaBuffer->close(); - } - - m_mediaBuffer = std::make_unique(); - m_mediaBuffer->setData(result->data()); - m_mediaBuffer->open(QIODevice::ReadOnly); - m_mediaPlayer.setSourceDevice(m_mediaBuffer.get()); -#endif - - m_audioOutput.setMuted(false); - - m_mediaPlayer.play(); } diff --git a/src/tools/audio/players/soundplayer.h b/src/tools/audio/players/soundplayer.h index e0626117..415fa3ad 100644 --- a/src/tools/audio/players/soundplayer.h +++ b/src/tools/audio/players/soundplayer.h @@ -1,69 +1,20 @@ #pragma once -#include "audioplayer.h" -#include "filesystem/results/filedataresult.h" -#include -#include -#include -#include -#include +#include "bufferedaudioplayer.h" #include -#include class AudioElement; class AudioFile; -class SoundPlayer : public AudioPlayer +class SoundPlayer : public BufferedAudioPlayer { Q_OBJECT QML_ELEMENT QML_UNCREATABLE("") public: - SoundPlayer(AudioElement *element, int volume, QObject *parent = nullptr); - - [[nodiscard]] auto element() const -> AudioElement * - { - return m_element; - } - [[nodiscard]] auto fileName() const -> QString - { - return m_fileName; - } - void loadMedia(const AudioFile *file); + explicit SoundPlayer(QNetworkAccessManager &networkManager, QObject *parent = nullptr); public slots: - void play() override; - void pause() override; - void stop() override; - void stopElement(const QString &element); - void setVolume(int linear, int logarithmic) override; - void again() override - { - } - void next() override; - -private: - QPointer m_element = nullptr; - QMediaPlayer m_mediaPlayer; - QAudioOutput m_audioOutput; - std::unique_ptr m_fileRequestContext = nullptr; - - QList m_playlist; - int m_playlistIndex = 0; - int m_youtubeRequestId = -1; - - std::unique_ptr m_mediaBuffer = nullptr; - QTemporaryDir m_tempDir; - QString m_fileName; - - void applyShuffleMode(); - -private slots: - void onMediaStatusChanged(QMediaPlayer::MediaStatus status); - void onMediaPlayerErrorOccurred(QMediaPlayer::Error error, const QString &errorString); - void onFileReceived(std::shared_ptr result); - -signals: - void playerStopped(SoundPlayer *player); + void stopElement(const QString &name); }; diff --git a/src/tools/audio/players/soundplayercontroller.cpp b/src/tools/audio/players/soundplayercontroller.cpp index d1703108..b7cec5ba 100644 --- a/src/tools/audio/players/soundplayercontroller.cpp +++ b/src/tools/audio/players/soundplayercontroller.cpp @@ -5,7 +5,8 @@ Q_LOGGING_CATEGORY(gmAudioSoundController, "gm.audio.sounds.controller") -SoundPlayerController::SoundPlayerController(QObject *parent) : AudioPlayer(parent) +SoundPlayerController::SoundPlayerController(QNetworkAccessManager &networkManager, QObject *parent) + : AudioPlayer(parent), m_networkManager(networkManager) { connect(this, &SoundPlayerController::soundsChanged, this, &SoundPlayerController::onSoundsChanged); } @@ -22,15 +23,16 @@ void SoundPlayerController::play(AudioElement *element) if (!isSoundPlaying(element)) { - auto *player = new SoundPlayer(element, m_volume, this); + auto *player = new SoundPlayer(m_networkManager, this); + player->setVolume(m_linearVolume, m_logarithmicVolume); - connect(player, &SoundPlayer::playerStopped, this, &SoundPlayerController::onPlayerStopped); + connect(player, &SoundPlayer::stateChanged, this, &SoundPlayerController::onPlayerStateChanged); connect(this, &SoundPlayerController::setPlayerVolume, player, &SoundPlayer::setVolume); connect(this, &SoundPlayerController::stopAll, player, &SoundPlayer::stop); connect(this, &SoundPlayerController::stopElement, player, &SoundPlayer::stopElement); m_players.append(player); - player->play(); + player->play(element); emit soundsChanged(elements()); } @@ -83,7 +85,8 @@ void SoundPlayerController::updateActiveElements() */ void SoundPlayerController::setVolume(int linear, int logarithmic) { - m_volume = logarithmic; + m_linearVolume = linear; + m_logarithmicVolume = logarithmic; emit setPlayerVolume(linear, logarithmic); } @@ -102,15 +105,19 @@ auto SoundPlayerController::elements() const -> QList return elements; } -/** - * @brief A sound player stopped. Remove it from list and delete it. - */ -void SoundPlayerController::onPlayerStopped(SoundPlayer *player) +void SoundPlayerController::onPlayerStateChanged(State state) { - m_players.removeOne(player); - player->deleteLater(); + // A sound player stopped. Remove it from list and delete it. + if (state == State::Stopped) + { + auto *player = qobject_cast(sender()); + if (!player) return; - emit soundsChanged(elements()); + m_players.removeOne(player); + player->deleteLater(); + + emit soundsChanged(elements()); + } } void SoundPlayerController::onSoundsChanged(const QList &sounds) diff --git a/src/tools/audio/players/soundplayercontroller.h b/src/tools/audio/players/soundplayercontroller.h index 35c07286..e6004ac1 100644 --- a/src/tools/audio/players/soundplayercontroller.h +++ b/src/tools/audio/players/soundplayercontroller.h @@ -2,6 +2,7 @@ #include "audioplayer.h" #include "thirdparty/propertyhelper/PropertyHelper.h" +#include class SoundPlayer; class AudioElement; @@ -15,7 +16,7 @@ class SoundPlayerController : public AudioPlayer READ_LIST_PROPERTY(AudioElement, activeElements) public: - explicit SoundPlayerController(QObject *parent = nullptr); + explicit SoundPlayerController(QNetworkAccessManager &networkManager, QObject *parent = nullptr); void play(AudioElement *elements); void stop(const QString &element); @@ -40,15 +41,17 @@ public slots: } private: + QNetworkAccessManager &m_networkManager; QList m_players; - int m_volume = 0; + int m_linearVolume = 0; + int m_logarithmicVolume = 0; [[nodiscard]] auto elements() const -> QList; [[nodiscard]] auto isSoundPlaying(AudioElement *elements) const -> bool; void updateActiveElements(); private slots: - void onPlayerStopped(SoundPlayer *player); + void onPlayerStateChanged(State state); void onSoundsChanged(const QList &sounds); signals: diff --git a/src/tools/audio/players/spotifyplayer.cpp b/src/tools/audio/players/spotifyplayer.cpp index 9771e56a..5e64c072 100644 --- a/src/tools/audio/players/spotifyplayer.cpp +++ b/src/tools/audio/players/spotifyplayer.cpp @@ -1,8 +1,5 @@ #include "spotifyplayer.h" #include "services/spotify/spotify.h" -#include -#include -#include #include Q_LOGGING_CATEGORY(gmAudioSpotify, "gm.audio.spotify") @@ -13,12 +10,10 @@ SpotifyPlayer::SpotifyPlayer(MetaDataReader &mDReader, QObject *parent) qCDebug(gmAudioSpotify()) << "Loading ..."; // Timer for "current song" updates - m_songDurationTimer = new QTimer(this); - m_metaDataTimer = new QTimer(this); - m_metaDataTimer->setSingleShot(true); + m_metaDataTimer.setSingleShot(true); - connect(m_songDurationTimer, &QTimer::timeout, this, &SpotifyPlayer::onDurationTimerTimeout); - connect(m_metaDataTimer, &QTimer::timeout, this, &SpotifyPlayer::onMetaDataTimerTimeout); + connect(&m_songDurationTimer, &QTimer::timeout, this, &SpotifyPlayer::onDurationTimerTimeout); + connect(&m_metaDataTimer, &QTimer::timeout, this, &SpotifyPlayer::onMetaDataTimerTimeout); } /// The current song has ended, stop any spotify activity and notify music player @@ -76,20 +71,21 @@ void SpotifyPlayer::play(const QString &uri) m_metaDataReader.clearMetaData(); startMetaDataTimer(); + + state(AudioPlayer::State::Playing); }; Spotify::instance()->player->play(uri).then(this, callback); - m_isPlaying = true; - emit startedPlaying(); + state(AudioPlayer::State::Loading); } /** - * @brief Conitinue playback + * @brief Continue playback */ void SpotifyPlayer::play() { - if (!isSpotifyAvailable()) return; + if (!isSpotifyAvailable() || state() == AudioPlayer::State::Playing) return; qCDebug(gmAudioSpotify) << "Continuing playback ..."; @@ -99,40 +95,42 @@ void SpotifyPlayer::play() startMetaDataTimer(); }); - m_isPlaying = true; - emit startedPlaying(); + state(AudioPlayer::State::Playing); } void SpotifyPlayer::startDurationTimer(std::chrono::milliseconds interval) { - m_songDurationTimer->stop(); + m_songDurationTimer.stop(); qCDebug(gmAudioSpotify) << "Resuming timer with remaining time:" << interval.count() << "ms"; - m_songDurationTimer->start(interval); + m_songDurationTimer.start(interval); } void SpotifyPlayer::startMetaDataTimer() { - m_metaDataTimer->start(std::chrono::seconds(1)); + m_metaDataTimer.start(std::chrono::seconds(1)); } -/** - * @brief Stop playback - */ -void SpotifyPlayer::stop() +void SpotifyPlayer::pause() { - qCDebug(gmAudioSpotify) << "Stopping playback ..."; + if (state() != AudioPlayer::State::Playing) return; - if (!m_isPlaying) return; + qCDebug(gmAudioSpotify) << "Pausing playback ..."; - m_songDurationTimer->stop(); - m_metaDataTimer->stop(); + m_songDurationTimer.stop(); + m_metaDataTimer.stop(); Spotify::instance()->player->pause().then(this, [](RestNetworkReply *reply) { if (reply) reply->deleteLater(); }); - m_isPlaying = false; + state(AudioPlayer::State::Paused); +} + +void SpotifyPlayer::stop() +{ + pause(); + state(AudioPlayer::State::Stopped); } /** @@ -140,9 +138,9 @@ void SpotifyPlayer::stop() */ void SpotifyPlayer::pausePlay() { - if (m_isPlaying) + if (state() == AudioPlayer::State::Playing) { - stop(); + pause(); return; } @@ -203,11 +201,11 @@ void SpotifyPlayer::getCurrentSong() return; } - auto *metadata = new AudioMetaData(&m_metaDataReader); - metadata->title(track->track->name); - metadata->album(track->track->album->name); - metadata->artist(track->track->artistString()); - metadata->cover(track->track->image()->url); + AudioMetaData metadata; + metadata.title(track->track->name); + metadata.album(track->track->album->name); + metadata.artist(track->track->artistNames()); + metadata.cover(track->track->image()->url); using namespace std::chrono; auto duration = milliseconds(track->track->durationMs); diff --git a/src/tools/audio/players/spotifyplayer.h b/src/tools/audio/players/spotifyplayer.h index 9377853c..ac380119 100644 --- a/src/tools/audio/players/spotifyplayer.h +++ b/src/tools/audio/players/spotifyplayer.h @@ -1,18 +1,10 @@ -#ifndef SPOTIFYPLAYER_H -#define SPOTIFYPLAYER_H - -#include -#include -#include -#include -#include -#include -#include -#include +#pragma once #include "../metadata/metadatareader.h" #include "../project/audioelement.h" #include "audioplayer.h" +#include +#include class SpotifyPlayer : public AudioPlayer { @@ -24,10 +16,7 @@ class SpotifyPlayer : public AudioPlayer public slots: void play(const QString &uri); void play() override; - void pause() override - { - stop(); - } + void pause() override; void stop() override; void pausePlay(); void next() override; @@ -36,10 +25,9 @@ public slots: private: MetaDataReader &m_metaDataReader; - gsl::owner m_songDurationTimer = nullptr; - gsl::owner m_metaDataTimer = nullptr; + QTimer m_songDurationTimer; + QTimer m_metaDataTimer; - bool m_isPlaying = false; int m_volume = 0; QString m_currentUri; @@ -55,9 +43,6 @@ private slots: signals: void songNamesChanged(); - void startedPlaying(); void songEnded(); void receivedElementIcon(AudioElement *element); }; - -#endif // SPOTIFYPLAYER_H diff --git a/src/tools/audio/playlist/audioplaylist.cpp b/src/tools/audio/playlist/audioplaylist.cpp index f6167c12..449489bf 100644 --- a/src/tools/audio/playlist/audioplaylist.cpp +++ b/src/tools/audio/playlist/audioplaylist.cpp @@ -1,23 +1,11 @@ #include "audioplaylist.h" -#include "utils/networkutils.h" -#include -#include +#include "utils/utils.h" +#include using namespace Qt::Literals::StringLiterals; -AudioPlaylist::AudioPlaylist(const QByteArray &data, QObject *parent) : m_type(getType(data)) +AudioPlaylist::AudioPlaylist(Type type) : m_type(type) { - switch (m_type) - { - case Type::m3u: - parseM3u(data, parent); - break; - case Type::pls: - parsePls(data, parent); - break; - default: - break; - } } auto AudioPlaylist::isEmpty() const -> bool @@ -35,57 +23,67 @@ auto AudioPlaylist::files() const -> QList return m_files; } +auto AudioPlaylist::filesQml(QObject *parent) -> QQmlListProperty +{ + return QQmlListProperty(parent, &m_files); +} + auto AudioPlaylist::type() const -> AudioPlaylist::Type { return m_type; } -auto AudioPlaylist::getType(const QByteArray &data) -> AudioPlaylist::Type +auto AudioPlaylist::at(qsizetype i) const -> AudioFile *const & { - QTextStream stream(data, QIODeviceBase::ReadOnly); - const auto line = stream.readLine(); + return m_files.at(i); +} - if (line.isNull() || line.isEmpty()) return Type::Undefined; +auto AudioPlaylist::constFirst() const -> AudioFile *const & +{ + return m_files.constFirst(); +} + +void AudioPlaylist::setFiles(const QList &files) +{ + m_files = files; +} - if (line.trimmed() == "[playlist]"_L1) return Type::pls; +void AudioPlaylist::append(AudioFile *file) +{ + if (!file) return; + m_files.append(file); +} - return Type::m3u; +void AudioPlaylist::insert(qsizetype index, AudioFile *file) +{ + if (!file) return; + m_files.insert(index, file); } -void AudioPlaylist::parseM3u(const QByteArray &data, QObject *parent) +void AudioPlaylist::replace(qsizetype index, const AudioPlaylist &other) { - QTextStream stream(data, QIODeviceBase::ReadOnly); + replace(index, other.m_files); +} - QString line; - while (!(line = stream.readLine().trimmed()).isNull()) - { - // header type line, ignore for now - if (line.startsWith("#"_L1)) continue; +void AudioPlaylist::replace(qsizetype index, const QList &files) +{ + if (!Utils::isInBounds(m_files, index)) return; - const auto isFromWeb = NetworkUtils::isHttpUrl(line); + m_files.removeAt(index); - m_files << new AudioFile(line, isFromWeb ? AudioFile::Source::Web : AudioFile::Source::File, u""_s, parent); + foreach (auto *file, files) + { + m_files.insert(index, file); + index++; } } -void AudioPlaylist::parsePls(const QByteArray &data, QObject *parent) +void AudioPlaylist::shuffle() { - QTextStream stream(data, QIODeviceBase::ReadOnly); + std::shuffle(m_files.begin(), m_files.end(), *QRandomGenerator::system()); +} - QString line; - while (!(line = stream.readLine().trimmed()).isNull()) - { - static QRegularExpression const regex(uR"([fF]ile(?\d+)=(?.+))"_s); - auto match = regex.match(line); - - if (match.isValid() && match.hasMatch()) - { - const auto index = match.captured("index"_L1).toInt() - 1; - const auto url = match.captured("url"_L1); - const auto isFromWeb = NetworkUtils::isHttpUrl(url); - - m_files.insert( - index, new AudioFile(url, isFromWeb ? AudioFile::Source::Web : AudioFile::Source::File, u""_s, parent)); - } - } +void AudioPlaylist::clear() +{ + m_files.clear(); } diff --git a/src/tools/audio/playlist/audioplaylist.h b/src/tools/audio/playlist/audioplaylist.h index fba4d186..e7b95e3c 100644 --- a/src/tools/audio/playlist/audioplaylist.h +++ b/src/tools/audio/playlist/audioplaylist.h @@ -4,6 +4,7 @@ #include #include #include +#include class AudioPlaylist { @@ -15,19 +16,28 @@ class AudioPlaylist pls }; - AudioPlaylist(const QByteArray &data, QObject *parent = nullptr); + AudioPlaylist() = default; + explicit AudioPlaylist(Type type); [[nodiscard]] auto isEmpty() const -> bool; [[nodiscard]] auto length() const -> qsizetype; - [[nodiscard]] auto files() const -> QList; [[nodiscard]] auto type() const -> AudioPlaylist::Type; + [[nodiscard]] auto at(qsizetype i) const -> AudioFile *const &; + [[nodiscard]] auto constFirst() const -> AudioFile *const &; + + [[nodiscard]] auto files() const -> QList; + [[nodiscard]] auto filesQml(QObject *parent) -> QQmlListProperty; + void setFiles(const QList &files); + void append(AudioFile *file); + void insert(qsizetype index, AudioFile *file); + + void replace(qsizetype index, const AudioPlaylist &other); + void replace(qsizetype index, const QList &files); + + void shuffle(); + void clear(); private: QList m_files; Type m_type = Type::Undefined; - - [[nodiscard]] static auto getType(const QByteArray &data) -> Type; - - void parseM3u(const QByteArray &data, QObject *parent); - void parsePls(const QByteArray &data, QObject *parent); }; diff --git a/src/tools/audio/playlist/audioplaylistfactory.cpp b/src/tools/audio/playlist/audioplaylistfactory.cpp new file mode 100644 index 00000000..e1c58bfe --- /dev/null +++ b/src/tools/audio/playlist/audioplaylistfactory.cpp @@ -0,0 +1,75 @@ +#include "audioplaylistfactory.h" +#include "utils/networkutils.h" +#include +#include + +using namespace Qt::Literals::StringLiterals; + +auto AudioPlaylistFactory::build(const QByteArray &data, QObject *parent) -> std::unique_ptr +{ + switch (getType(data)) + { + case AudioPlaylist::Type::m3u: + return m3u(data, parent); + case AudioPlaylist::Type::pls: + return pls(data, parent); + default: + return std::unique_ptr(); + } +} + +auto AudioPlaylistFactory::m3u(const QByteArray &data, QObject *parent) -> std::unique_ptr +{ + auto playlist = std::make_unique(AudioPlaylist::Type::m3u); + QTextStream stream(data, QIODeviceBase::ReadOnly); + + QString line; + while (!(line = stream.readLine().trimmed()).isNull()) + { + // header type line, ignore for now + if (line.startsWith("#"_L1)) continue; + + const auto isFromWeb = NetworkUtils::isHttpUrl(line); + + playlist->append( + new AudioFile(line, isFromWeb ? AudioFile::Source::Web : AudioFile::Source::File, u""_s, parent)); + } + + return playlist; +} + +auto AudioPlaylistFactory::pls(const QByteArray &data, QObject *parent) -> std::unique_ptr +{ + auto playlist = std::make_unique(AudioPlaylist::Type::pls); + QTextStream stream(data, QIODeviceBase::ReadOnly); + + QString line; + while (!(line = stream.readLine().trimmed()).isNull()) + { + static QRegularExpression const regex(uR"([fF]ile(?\d+)=(?.+))"_s); + auto match = regex.match(line); + + if (match.isValid() && match.hasMatch()) + { + const auto index = match.captured("index"_L1).toInt() - 1; + const auto url = match.captured("url"_L1); + const auto isFromWeb = NetworkUtils::isHttpUrl(url); + + playlist->insert( + index, new AudioFile(url, isFromWeb ? AudioFile::Source::Web : AudioFile::Source::File, u""_s, parent)); + } + } + return playlist; +} + +auto AudioPlaylistFactory::getType(const QByteArray &data) -> AudioPlaylist::Type +{ + QTextStream stream(data, QIODeviceBase::ReadOnly); + const auto line = stream.readLine(); + + if (line.isNull() || line.isEmpty()) return AudioPlaylist::Type::Undefined; + + if (line.trimmed() == "[playlist]"_L1) return AudioPlaylist::Type::pls; + + return AudioPlaylist::Type::m3u; +} diff --git a/src/tools/audio/playlist/audioplaylistfactory.h b/src/tools/audio/playlist/audioplaylistfactory.h new file mode 100644 index 00000000..6d1f6ff1 --- /dev/null +++ b/src/tools/audio/playlist/audioplaylistfactory.h @@ -0,0 +1,17 @@ +#pragma once + +#include "audioplaylist.h" +#include + +class AudioPlaylistFactory +{ +public: + AudioPlaylistFactory() = delete; + + static auto build(const QByteArray &data, QObject *parent) -> std::unique_ptr; + static auto m3u(const QByteArray &data, QObject *parent) -> std::unique_ptr; + static auto pls(const QByteArray &data, QObject *parent) -> std::unique_ptr; + +private: + [[nodiscard]] static auto getType(const QByteArray &data) -> AudioPlaylist::Type; +}; diff --git a/src/tools/audio/playlist/resolvingaudioplaylist.cpp b/src/tools/audio/playlist/resolvingaudioplaylist.cpp new file mode 100644 index 00000000..057732e9 --- /dev/null +++ b/src/tools/audio/playlist/resolvingaudioplaylist.cpp @@ -0,0 +1,200 @@ +#include "resolvingaudioplaylist.h" +#include "audioplaylistfactory.h" +#include "filesystem/file.h" +#include "filesystem/results/filedataresult.h" +#include "services/spotify/spotify.h" +#include "services/spotify/spotifyutils.h" +#include "settings/settingsmanager.h" +#include "utils/fileutils.h" +#include + +using namespace Qt::Literals::StringLiterals; + +Q_LOGGING_CATEGORY(gmAudioPlaylistResolving, "gm.audio.playlist.resolving") + +ResolvingAudioPlaylist::ResolvingAudioPlaylist(const QString &settingsId, QNetworkAccessManager &networkManager, + Type type) + : AudioPlaylist(type), m_networkManager(networkManager), m_settingsId(settingsId) +{ +} + +auto ResolvingAudioPlaylist::resolve() -> QFuture +{ + if (m_isResolving) return {}; + + m_isResolving = true; + + return unwrapEntries().then([this]() { + m_isResolving = false; + loadTitles(); + }); +} + +auto ResolvingAudioPlaylist::unwrapEntries() -> QFuture +{ + QList> futures; + AudioFile *audioFile = nullptr; + + for (qsizetype i = 0; i < length(); i++) + { + audioFile = at(i); + if (!audioFile) continue; + + switch (audioFile->source()) + { + case AudioFile::Source::Spotify: + futures << unwrapSpotify(i, *audioFile); + break; + case AudioFile::Source::File: + case AudioFile::Source::Web: + if (isPlaylist(audioFile->url())) + { + futures << unwrapPlaylistFile(i, *audioFile); + } + break; + default: + break; + } + } + + return QtFuture::whenAll(futures.begin(), futures.end()).then([](const QList> &) {}); +} + +auto ResolvingAudioPlaylist::unwrapPlaylistFile(qsizetype index, const AudioFile &file) -> QFuture +{ + auto callback = [this, index](const QByteArray &data) { + if (data.isEmpty()) + { + qCWarning(gmAudioPlaylistResolving()) << "Error: File is empty!"; + return; + } + + auto playlist = AudioPlaylistFactory::build(data, &m_fileParent); + if (playlist->isEmpty()) + { + qCWarning(gmAudioPlaylistResolving()) << "Playlist is empty"; + return; + } + + replace(index, *playlist); + }; + + switch (file.source()) + { + case AudioFile::Source::File: { + const auto path = FileUtils::fileInDir(file.url(), SettingsManager::getPath(m_settingsId)); + return Files::File::getDataAsync(path).then( + &m_fileParent, [callback](std::shared_ptr result) { callback(result->data()); }); + break; + } + case AudioFile::Source::Web: { + auto *reply = m_networkManager.get(QNetworkRequest(QUrl(file.url()))); + return QtFuture::connect(reply, &QNetworkReply::finished).then(&m_fileParent, [reply, callback]() { + const auto &data = reply->readAll(); + reply->deleteLater(); + callback(data); + }); + break; + } + default: + qCWarning(gmAudioPlaylistResolving()) + << "Could not expand playlist file" << file.url() << "with source" << file.source(); + return QtFuture::makeReadyFuture(); + } +} + +auto ResolvingAudioPlaylist::unwrapSpotify(qsizetype index, const AudioFile &file) -> QFuture +{ + const auto uri = SpotifyUtils::makeUri(file.url()); + const auto type = SpotifyUtils::getUriType(uri); + const auto id = SpotifyUtils::getIdFromUri(uri); + + if (!SpotifyUtils::isContainerType(type)) return QtFuture::makeReadyFuture(); + + const auto callback = [this, index](const QSharedPointer &tracks) { + QList files; + foreach (const auto &track, tracks->tracks) + { + if (!track->isPlayable) + { + qCDebug(gmAudioPlaylistResolving()) << "Spotify track" << track->name << track->album->name + << track->uri << "is not playable -> ignoring it"; + continue; + } + + switch (SpotifyUtils::getUriType(track->uri)) + { + case SpotifyUtils::SpotifyType::Track: + case SpotifyUtils::SpotifyType::Episode: + files << new AudioFile(track->uri, AudioFile::Source::Spotify, track->name, &m_fileParent); + default: + break; + } + } + replace(index, files); + }; + + switch (type) + { + case SpotifyUtils::SpotifyType::Playlist: + return Spotify::instance()->playlists->getPlaylistTracks(id).then(&m_fileParent, callback); + case SpotifyUtils::SpotifyType::Album: + return Spotify::instance()->albums->getAlbumTracks(id).then(&m_fileParent, callback); + default: + qCCritical(gmAudioPlaylistResolving()) + << "loadPlaylistRecursiveSpotify(): not implemented for container type" << (int)type; + return QtFuture::makeReadyFuture(); + } +} + +void ResolvingAudioPlaylist::loadTitles() +{ + QList spotifyTracks; + + foreach (auto *audioFile, files()) + { + if (!audioFile || !audioFile->title().isEmpty()) continue; + + switch (audioFile->source()) + { + case AudioFile::Source::Spotify: + if (SpotifyUtils::getUriType(audioFile->url()) == SpotifyUtils::SpotifyType::Track) + spotifyTracks.append(audioFile); + break; + case AudioFile::Source::Youtube: + audioFile->title(QObject::tr("[BROKEN] %1").arg(audioFile->url())); + break; + default: + break; + } + } + + loadSpotifyTitles(spotifyTracks); +} + +void ResolvingAudioPlaylist::loadSpotifyTitles(const QList &tracks) +{ + if (tracks.isEmpty()) return; + + QStringList trackIds; + trackIds.reserve(tracks.length()); + + foreach (const auto *track, tracks) + { + trackIds << SpotifyUtils::getIdFromUri(track->url()); + } + + const auto callback = [tracks](const std::vector> &results) { + for (size_t i = 0; i < results.size(); i++) + { + tracks.at(i)->title(results.at(i)->name); + } + }; + + Spotify::instance()->tracks->getTracks(trackIds).then(Spotify::instance(), callback); +} + +auto ResolvingAudioPlaylist::isPlaylist(const QString &file) -> bool +{ + return file.endsWith(".m3u"_L1) || file.endsWith(".m3u8"_L1) || file.endsWith(".pls"_L1); +} diff --git a/src/tools/audio/playlist/resolvingaudioplaylist.h b/src/tools/audio/playlist/resolvingaudioplaylist.h new file mode 100644 index 00000000..026bcd93 --- /dev/null +++ b/src/tools/audio/playlist/resolvingaudioplaylist.h @@ -0,0 +1,29 @@ +#pragma once + +#include "audioplaylist.h" +#include +#include + +class ResolvingAudioPlaylist : public AudioPlaylist +{ +public: + explicit ResolvingAudioPlaylist(const QString &settingsId, QNetworkAccessManager &networkManager, + Type type = Type::Undefined); + + auto resolve() -> QFuture; + +private: + auto unwrapEntries() -> QFuture; + auto unwrapPlaylistFile(qsizetype index, const AudioFile &file) -> QFuture; + auto unwrapSpotify(qsizetype index, const AudioFile &file) -> QFuture; + + void loadTitles(); + void loadSpotifyTitles(const QList &tracks); + + static auto isPlaylist(const QString &file) -> bool; + + QNetworkAccessManager &m_networkManager; + QObject m_fileParent; + QString m_settingsId; + bool m_isResolving = false; +}; diff --git a/src/tools/audio/thumbnails/loaders/tagimageloader.cpp b/src/tools/audio/thumbnails/loaders/tagimageloader.cpp index 64e8dd0f..2c77e499 100755 --- a/src/tools/audio/thumbnails/loaders/tagimageloader.cpp +++ b/src/tools/audio/thumbnails/loaders/tagimageloader.cpp @@ -9,13 +9,7 @@ #include #include #include -#include -#include -#include -#include #include -#include -#include using namespace Qt::Literals::StringLiterals; @@ -57,6 +51,55 @@ auto TagImageLoader::loadFromFile(const QString &path, bool isLocalFile) -> QFut return loadFromLocalFile(path); } +auto TagImageLoader::loadFromData(const QString &path, const QByteArray &data) -> QFuture +{ + const ByteVector bvector(data.data(), data.length()); + auto bvstream = std::make_unique(bvector); + + return loadFromData(path, std::move(bvstream)); +} + +auto TagImageLoader::loadFromData(const QString &path, std::unique_ptr data) -> QFuture +{ + const auto mimeType = FileUtils::getMimeType(path); + + switch (mimeType) + { + case FileUtils::MimeType::MPEG: { + auto mpeg = std::make_unique(data.get(), ID3v2::FrameFactory::instance()); + return loadFromMpeg(std::move(mpeg), path); + } + case FileUtils::MimeType::OGA: { + Ogg::FLAC::File flac(data.get()); + if (flac.isValid()) + { + return loadFromFlac(flac, path); + } + + Ogg::Vorbis::File vorbis(data.get()); + return loadFromVorbis(vorbis, path); + } + case FileUtils::MimeType::Vorbis: { + Ogg::Vorbis::File vorbis(data.get()); + return loadFromVorbis(vorbis, path); + } + case FileUtils::MimeType::FLAC: { + FLAC::File flac(data.get(), ID3v2::FrameFactory::instance()); + if (flac.isValid()) return loadFromFlac(flac, path); + + Ogg::FLAC::File oggflac(data.get()); + return loadFromFlac(oggflac, path); + } + case FileUtils::MimeType::WAV: { + RIFF::WAV::File wav(data.get()); + return loadFromWav(wav, path); + } + default: + qCDebug(gmAudioTagImageLoader()) << "Could not load image from" << path << "mime type is not supported yet"; + return QtFuture::makeReadyFuture(QPixmap()); + } +} + auto TagImageLoader::loadViaTempFile(const QString &path) -> QFuture { auto future = Files::File::getDataAsync(path); @@ -89,7 +132,7 @@ auto TagImageLoader::loadFromLocalFile(const QString &path) -> QFuture switch (mimeType) { case FileUtils::MimeType::MPEG: - return loadFromLocalMpeg(path); + return loadFromMpeg(path); case FileUtils::MimeType::OGA: return loadFromOga(path); case FileUtils::MimeType::Vorbis: @@ -104,26 +147,30 @@ auto TagImageLoader::loadFromLocalFile(const QString &path) -> QFuture } } -auto TagImageLoader::loadFromLocalMpeg(const QString &path) -> QFuture +auto TagImageLoader::loadFromMpeg(const QString &path) -> QFuture { - auto mpeg = TagLib::MPEG::File(QFile::encodeName(path).constData()); + auto mpeg = std::make_unique(QFile::encodeName(path).constData()); + return loadFromMpeg(std::move(mpeg), path); +} - if (!mpeg.isValid()) +auto TagImageLoader::loadFromMpeg(std::unique_ptr mpeg, const QString &path) -> QFuture +{ + if (!mpeg->isValid()) { qCCritical(gmAudioTagImageLoader) << "File could not be opened by TagLib!" << path; return QtFuture::makeReadyFuture(QPixmap()); } - if (mpeg.hasID3v2Tag()) + if (mpeg->hasID3v2Tag()) { - return loadFromId3v2(mpeg.ID3v2Tag(), path); + return loadFromId3v2(mpeg->ID3v2Tag(), path); } qCDebug(gmAudioTagImageLoader) << "File does not contain supported tags" << path; return QtFuture::makeReadyFuture(QPixmap()); } -auto TagImageLoader::loadFromId3v2(const TagLib::ID3v2::Tag *tag, const QString &path) -> QFuture +auto TagImageLoader::loadFromId3v2(const ID3v2::Tag *tag, const QString &path) -> QFuture { if (!tag) { @@ -155,7 +202,7 @@ auto TagImageLoader::loadFromId3v2(const TagLib::ID3v2::Tag *tag, const QString return QtFuture::makeReadyFuture(image); } -auto TagImageLoader::pixmapFromId3v2Frames(const TagLib::ID3v2::FrameList &frames) -> QPixmap +auto TagImageLoader::pixmapFromId3v2Frames(const ID3v2::FrameList &frames) -> QPixmap { QPixmap result; @@ -187,7 +234,7 @@ auto TagImageLoader::pixmapFromId3v2Frames(const TagLib::ID3v2::FrameList &frame auto TagImageLoader::loadFromOga(const QString &path) -> QFuture { // Can be of type FLAC or Vorbis, check FLAC first - auto flac = TagLib::Ogg::FLAC::File(QFile::encodeName(path).constData()); + auto flac = Ogg::FLAC::File(QFile::encodeName(path).constData()); if (flac.isValid()) { return loadFromFlac(flac, path); @@ -199,8 +246,12 @@ auto TagImageLoader::loadFromOga(const QString &path) -> QFuture auto TagImageLoader::loadFromVorbis(const QString &path) -> QFuture { - const auto file = TagLib::Ogg::Vorbis::File(QFile::encodeName(path).constData()); + const auto file = Ogg::Vorbis::File(QFile::encodeName(path).constData()); + return loadFromVorbis(file, path); +} +auto TagImageLoader::loadFromVorbis(const Ogg::Vorbis::File &file, const QString &path) -> QFuture +{ if (!file.isValid()) { qCWarning(gmAudioTagImageLoader) << "Could not read tags from vorbis (ogg) file" << path; @@ -212,13 +263,13 @@ auto TagImageLoader::loadFromVorbis(const QString &path) -> QFuture auto TagImageLoader::loadFromFlac(const QString &path) -> QFuture { - auto flac = TagLib::FLAC::File(QFile::encodeName(path).constData()); + auto flac = FLAC::File(QFile::encodeName(path).constData()); if (flac.isValid()) { return loadFromFlac(flac, path); } - auto oggflac = TagLib::Ogg::FLAC::File(QFile::encodeName(path).constData()); + auto oggflac = Ogg::FLAC::File(QFile::encodeName(path).constData()); if (oggflac.isValid()) { return loadFromFlac(oggflac, path); @@ -227,7 +278,7 @@ auto TagImageLoader::loadFromFlac(const QString &path) -> QFuture return QtFuture::makeReadyFuture(QPixmap()); } -auto TagImageLoader::loadFromFlac(const TagLib::Ogg::FLAC::File &file, const QString &path) -> QFuture +auto TagImageLoader::loadFromFlac(const Ogg::FLAC::File &file, const QString &path) -> QFuture { if (file.hasXiphComment()) { @@ -238,7 +289,7 @@ auto TagImageLoader::loadFromFlac(const TagLib::Ogg::FLAC::File &file, const QSt return QtFuture::makeReadyFuture(QPixmap()); } -auto TagImageLoader::loadFromFlac(TagLib::FLAC::File &file, const QString &path) -> QFuture +auto TagImageLoader::loadFromFlac(FLAC::File &file, const QString &path) -> QFuture { if (file.hasID3v2Tag()) { @@ -254,7 +305,7 @@ auto TagImageLoader::loadFromFlac(TagLib::FLAC::File &file, const QString &path) return QtFuture::makeReadyFuture(QPixmap()); } -auto TagImageLoader::loadFromXiphComment(TagLib::Ogg::XiphComment *tag, const QString &path) -> QFuture +auto TagImageLoader::loadFromXiphComment(Ogg::XiphComment *tag, const QString &path) -> QFuture { if (!tag) { @@ -292,8 +343,12 @@ auto TagImageLoader::loadFromXiphComment(TagLib::Ogg::XiphComment *tag, const QS auto TagImageLoader::loadFromWav(const QString &path) -> QFuture { - const auto file = TagLib::RIFF::WAV::File(QFile::encodeName(path).constData()); + const auto file = RIFF::WAV::File(QFile::encodeName(path).constData()); + return loadFromWav(file, path); +} +auto TagImageLoader::loadFromWav(const TagLib::RIFF::WAV::File &file, const QString &path) -> QFuture +{ if (!file.isValid()) { qCWarning(gmAudioTagImageLoader) << "Could not read tags from wav file" << path; diff --git a/src/tools/audio/thumbnails/loaders/tagimageloader.h b/src/tools/audio/thumbnails/loaders/tagimageloader.h index 05037453..458953a8 100755 --- a/src/tools/audio/thumbnails/loaders/tagimageloader.h +++ b/src/tools/audio/thumbnails/loaders/tagimageloader.h @@ -1,35 +1,43 @@ #pragma once -#include -#include #include "../../project/audioelement.h" #include "../../project/audiofile.h" - -#include -#include +#include +#include +#include #include +#include +#include #include +#include +#include +#include +#include class TagImageLoader { public: static auto loadImageAsync(AudioElement *element, AudioFile *audioFile) -> QFuture; static auto loadFromFile(const QString &path, bool isLocalFile) -> QFuture; + static auto loadFromData(const QString &path, const QByteArray &data) -> QFuture; + static auto loadFromData(const QString &path, std::unique_ptr data) -> QFuture; private: static auto loadFromLocalFile(const QString &path) -> QFuture; static auto loadViaTempFile(const QString &path) -> QFuture; - static auto loadFromLocalMpeg(const QString &path) -> QFuture; + static auto loadFromMpeg(const QString &path) -> QFuture; + static auto loadFromMpeg(std::unique_ptr mpeg, const QString &path) -> QFuture; static auto loadFromId3v2(const TagLib::ID3v2::Tag *tag, const QString &path) -> QFuture; static auto pixmapFromId3v2Frames(const TagLib::ID3v2::FrameList &frames) -> QPixmap; static auto loadFromOga(const QString &path) -> QFuture; static auto loadFromVorbis(const QString &path) -> QFuture; + static auto loadFromVorbis(const TagLib::Ogg::Vorbis::File &file, const QString &path) -> QFuture; static auto loadFromFlac(const QString &path) -> QFuture; static auto loadFromFlac(const TagLib::Ogg::FLAC::File &file, const QString &path) -> QFuture; static auto loadFromFlac(TagLib::FLAC::File &file, const QString &path) -> QFuture; static auto loadFromXiphComment(TagLib::Ogg::XiphComment *tag, const QString &path) -> QFuture; - static auto loadFromWav(const QString &path) -> QFuture; + static auto loadFromWav(const TagLib::RIFF::WAV::File &file, const QString &path) -> QFuture; }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8efcadbe..0c2dfd90 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -36,6 +36,7 @@ set(SOURCES tools/audio/testaudiosaveload.cpp tools/audio/testaudioaddons.cpp tools/audio/testaudioplaylist.cpp + tools/audio/testresolvingaudioplaylist.cpp tools/converter/testconverterprojectupgrader.cpp tools/converter/testconvertertool.cpp tools/converter/testconvertereditor.cpp diff --git a/tests/tools/audio/testaudioplaylist.cpp b/tests/tools/audio/testaudioplaylist.cpp index f8d7acf5..136b6818 100644 --- a/tests/tools/audio/testaudioplaylist.cpp +++ b/tests/tools/audio/testaudioplaylist.cpp @@ -1,35 +1,140 @@ +#include "abstracttest.h" #include "src/tools/audio/playlist/audioplaylist.h" -#include "tests/testhelper/abstracttest.h" +#include "src/tools/audio/playlist/audioplaylistfactory.h" +#include + +using namespace Qt::Literals::StringLiterals; class AudioPlaylistTest : public AbstractTest { protected: - static void parsePlaylist(const QString &url, AudioPlaylist::Type expectedType); + void parsePlaylist(const QString &url, AudioPlaylist::Type expectedType) + { + { + QFile projectFile(url); + EXPECT_TRUE(projectFile.open(QIODevice::ReadOnly)) << "Could not open test playlist file"; + auto data = projectFile.readAll(); + projectFile.close(); + + EXPECT_FALSE(data.isEmpty()) << "Playlist file does not contain data"; + auto playlist = AudioPlaylistFactory::build(data, nullptr); + + EXPECT_EQ(playlist->type(), expectedType); + EXPECT_EQ(playlist->length(), 3); + EXPECT_FALSE(playlist->isEmpty()); + + foreach (const auto *file, playlist->files()) + { + EXPECT_TRUE(file); + EXPECT_FALSE(file->url().isEmpty()); + } + } + } }; -void AudioPlaylistTest::parsePlaylist(const QString &url, AudioPlaylist::Type expectedType) +TEST_F(AudioPlaylistTest, ParsePlaylists) { - QFile projectFile(url); - EXPECT_TRUE(projectFile.open(QIODevice::ReadOnly)) << "Could not open test playlist file"; - auto data = projectFile.readAll(); - projectFile.close(); + parsePlaylist(u":/resources/audioplaylist/test.m3u"_s, AudioPlaylist::Type::m3u); + parsePlaylist(u":/resources/audioplaylist/test.pls"_s, AudioPlaylist::Type::pls); +} - EXPECT_FALSE(data.isEmpty()) << "Playlist file does not contain data"; - const AudioPlaylist playlist(data, nullptr); +TEST_F(AudioPlaylistTest, CanModify) +{ + AudioPlaylist playlist; - EXPECT_EQ(playlist.type(), expectedType); - EXPECT_EQ(playlist.length(), 3); + EXPECT_TRUE(playlist.isEmpty()); + EXPECT_EQ(playlist.length(), 0); + + AudioFile file0(u"/test.mp3"_s, AudioFile::Source::File, u"Test"_s, nullptr); + playlist.append(&file0); EXPECT_FALSE(playlist.isEmpty()); + EXPECT_EQ(playlist.length(), 1); + EXPECT_EQ(playlist.at(0), &file0); + EXPECT_EQ(playlist.constFirst(), &file0); - foreach (const auto *file, playlist.files()) - { - EXPECT_TRUE(file); - EXPECT_FALSE(file->url().isEmpty()); - } + AudioFile file1(u"/test1.mp3"_s, AudioFile::Source::File, u"Test1"_s, nullptr); + AudioFile file2(u"/test2.mp3"_s, AudioFile::Source::File, u"Test2"_s, nullptr); + playlist.setFiles({&file1, &file2}); + EXPECT_EQ(playlist.length(), 2); + EXPECT_EQ(playlist.at(0), &file1); + EXPECT_EQ(playlist.at(1), &file2); + + playlist.insert(1, &file0); + EXPECT_EQ(playlist.length(), 3); + EXPECT_EQ(playlist.at(1), &file0); + EXPECT_EQ(playlist.at(2), &file2); + + playlist.clear(); + EXPECT_TRUE(playlist.isEmpty()); + EXPECT_EQ(playlist.length(), 0); } -TEST_F(AudioPlaylistTest, ParsePlaylists) +TEST_F(AudioPlaylistTest, CanReplace) { - parsePlaylist(u":/resources/audioplaylist/test.m3u"_s, AudioPlaylist::Type::m3u); - parsePlaylist(u":/resources/audioplaylist/test.pls"_s, AudioPlaylist::Type::pls); + AudioPlaylist playlist; + AudioFile file0(u"/test.mp3"_s, AudioFile::Source::File, u"Test"_s, nullptr); + AudioFile file1(u"/test1.mp3"_s, AudioFile::Source::File, u"Test1"_s, nullptr); + AudioFile file2(u"/test2.mp3"_s, AudioFile::Source::File, u"Test2"_s, nullptr); + AudioFile file3(u"/test3.mp3"_s, AudioFile::Source::File, u"Test3"_s, nullptr); + + playlist.setFiles({&file0, &file1}); + playlist.replace(0, {&file2, &file3}); + EXPECT_EQ(playlist.length(), 3); + EXPECT_EQ(playlist.at(0), &file2); + EXPECT_EQ(playlist.at(1), &file3); + + playlist.setFiles({&file0, &file1}); + playlist.replace(1, {&file2, &file3}); + EXPECT_EQ(playlist.length(), 3); + EXPECT_EQ(playlist.at(1), &file2); + EXPECT_EQ(playlist.at(2), &file3); +} + +TEST_F(AudioPlaylistTest, CanReplaceWithPlaylist) +{ + AudioPlaylist playlist; + AudioFile file0(u"/test.mp3"_s, AudioFile::Source::File, u"Test"_s, nullptr); + AudioFile file1(u"/test1.mp3"_s, AudioFile::Source::File, u"Test1"_s, nullptr); + playlist.setFiles({&file0, &file1}); + + AudioPlaylist playlist2; + AudioFile file2(u"/test2.mp3"_s, AudioFile::Source::File, u"Test2"_s, nullptr); + AudioFile file3(u"/test3.mp3"_s, AudioFile::Source::File, u"Test3"_s, nullptr); + playlist2.setFiles({&file2, &file3}); + + playlist.replace(0, playlist2); + EXPECT_EQ(playlist.length(), 3); + EXPECT_EQ(playlist.at(0), &file2); + EXPECT_EQ(playlist.at(1), &file3); + + playlist.setFiles({&file0, &file1}); + playlist.replace(1, playlist2); + EXPECT_EQ(playlist.length(), 3); + EXPECT_EQ(playlist.at(1), &file2); + EXPECT_EQ(playlist.at(2), &file3); +} + +TEST_F(AudioPlaylistTest, CanShuffle) +{ + AudioPlaylist playlist; + AudioFile file0(u"/test.mp3"_s, AudioFile::Source::File, u"Test"_s, nullptr); + AudioFile file1(u"/test1.mp3"_s, AudioFile::Source::File, u"Test1"_s, nullptr); + AudioFile file2(u"/test2.mp3"_s, AudioFile::Source::File, u"Test2"_s, nullptr); + AudioFile file3(u"/test3.mp3"_s, AudioFile::Source::File, u"Test3"_s, nullptr); + + playlist.setFiles({&file0, &file1, &file2, &file3}); + + for (int i = 0; i < 5; i++) + { + playlist.shuffle(); + + if (playlist.at(0) != &file0 || playlist.at(1) != &file1 || playlist.at(2) != &file2 || + playlist.at(3) != &file3) + { + SUCCEED(); + return; + } + } + + FAIL() << "Attempted to shuffle the playlist 5 times, the order was always the same"; } diff --git a/tests/tools/audio/testresolvingaudioplaylist.cpp b/tests/tools/audio/testresolvingaudioplaylist.cpp new file mode 100644 index 00000000..2253de3b --- /dev/null +++ b/tests/tools/audio/testresolvingaudioplaylist.cpp @@ -0,0 +1,112 @@ +#include "settings/settingsmanager.h" +#include "src/tools/audio/playlist/resolvingaudioplaylist.h" +#include "testhelper/abstractmocknetworkmanager.h" +#include "testhelper/abstracttest.h" +#include "testhelper/mocknetworkreply.h" +#include "utils/fileutils.h" +#include +#include + +#ifndef QT_GUI_LIB +#define QT_GUI_LIB +#endif + +using namespace Qt::Literals::StringLiterals; + +class PlaylistMockNetworkManager : public AbstractMockNetworkManager +{ + Q_OBJECT +public: + explicit PlaylistMockNetworkManager(const QString &host, QObject *parent = nullptr) + : AbstractMockNetworkManager({host}, parent) + { + } + +protected: + auto sendMockReply(Operation, const QNetworkRequest &req, const QByteArray &) -> QNetworkReply * override + { + QByteArray data; + QFile file(u":"_s + req.url().path()); + if (file.open(QIODevice::ReadOnly)) + { + data = file.readAll(); + file.close(); + emit replySent(); + return MockNetworkReply::successGeneric(data, this); + } + + emit replySent(); + return MockNetworkReply::notFound(this); + } +}; + +class ResolvingAudioPlaylistTest : public AbstractTest +{ +public: + ResolvingAudioPlaylistTest() + { + networkManager = std::make_unique(u"fileserver.mock"_s); + QDesktopServices::setUrlHandler(u"http"_s, networkManager.get(), "simulateBrowser"); + QDesktopServices::setUrlHandler(u"https"_s, networkManager.get(), "simulateBrowser"); + + m_playlist = std::make_unique(u"testing"_s, *networkManager.get()); + + auto testingDir = SettingsManager::getPath(u"testing"_s); + backupDir = backupUserFolder(testingDir); + + copyResourceToFile(u":/resources/audioplaylist/test.m3u"_s, FileUtils::fileInDir(u"test.m3u"_s, testingDir)); + copyResourceToFile(u":/resources/audioplaylist/test.pls"_s, FileUtils::fileInDir(u"test.pls"_s, testingDir)); + } + + ~ResolvingAudioPlaylistTest() + { + restoreUserFolder(backupDir, SettingsManager::getPath(u"testing"_s)); + } + +protected: + std::unique_ptr m_playlist = nullptr; + +private: + QString backupDir; +}; + +TEST_F(ResolvingAudioPlaylistTest, CanResolveEmptyPlaylist) +{ + auto future = m_playlist->resolve(); + testFuture(future, "resolve()", [this, future]() { + EXPECT_FALSE(future.isCanceled()); + EXPECT_TRUE(m_playlist->isEmpty()); + }); +} + +TEST_F(ResolvingAudioPlaylistTest, CanResolveLocalPlaylists) +{ + auto *m3u = new AudioFile(u"/test.m3u"_s, AudioFile::Source::File, u""_s, nullptr); + auto *pls = new AudioFile(u"/test.pls"_s, AudioFile::Source::File, u""_s, nullptr); + m_playlist->setFiles({m3u, pls}); + + auto future = m_playlist->resolve(); + testFuture(future, "resolve()", [this, future]() { + EXPECT_FALSE(future.isCanceled()); + EXPECT_FALSE(m_playlist->isEmpty()); + EXPECT_EQ(m_playlist->length(), 6); + }); +} + +TEST_F(ResolvingAudioPlaylistTest, CanResolveWebPlaylists) +{ + auto *m3u = new AudioFile(u"http://fileserver.mock/resources/audioplaylist/test.m3u"_s, AudioFile::Source::Web, + u""_s, nullptr); + auto *pls = new AudioFile(u"http://fileserver.mock/resources/audioplaylist/test.pls"_s, AudioFile::Source::Web, + u""_s, nullptr); + m_playlist->setFiles({m3u, pls}); + + auto future = m_playlist->resolve(); + testFuture(future, "resolve()", [this, future]() { + EXPECT_FALSE(future.isCanceled()); + EXPECT_FALSE(m_playlist->isEmpty()); + EXPECT_EQ(m_playlist->length(), 6); + }); +} + +#include "testresolvingaudioplaylist.moc"