diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b7a3ebd7b4..8ff4df5a1d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,6 +80,7 @@ jobs: qt6-linguist-devel gtest gmock + sparsehash-devel - name: Install kdsingleapplication-qt6-devel if: matrix.opensuse_version == 'tumbleweed' run: zypper -n --gpg-auto-import-keys in kdsingleapplication-qt6-devel @@ -193,6 +194,7 @@ jobs: kdsingleapplication-qt6-devel gtest-devel gmock-devel + sparsehash-devel - name: Checkout uses: actions/checkout@v4 with: @@ -379,6 +381,7 @@ jobs: lib64qt6dbus-devel lib64qt6help-devel lib64qt6test-devel + lib64sparsehash-devel desktop-file-utils appstream-util hicolor-icon-theme @@ -466,6 +469,7 @@ jobs: libmtp-dev libgpod-dev libxkbcommon-dev + libsparsehash-dev qt6-base-dev qt6-base-private-dev qt6-base-dev-tools @@ -549,6 +553,7 @@ jobs: libmtp-dev libgpod-dev libxkbcommon-dev + libsparsehash-dev qt6-base-dev qt6-base-private-dev qt6-base-dev-tools @@ -631,6 +636,7 @@ jobs: libmtp-dev libgpod-dev libxkbcommon-dev + libsparsehash-dev qt6-base-dev qt6-base-private-dev qt6-base-dev-tools @@ -687,7 +693,7 @@ jobs: with: usesh: true mem: 4096 - prepare: pkg install -y git cmake pkgconf boost-libs alsa-lib glib qt6-base qt6-tools sqlite gstreamer1 gstreamer1-plugins chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf2 libgpod fftw3 icu kdsingleapplication googletest pulseaudio + prepare: pkg install -y git cmake pkgconf boost-libs alsa-lib glib qt6-base qt6-tools sqlite gstreamer1 gstreamer1-plugins chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf2 libgpod fftw3 icu kdsingleapplication googletest pulseaudio sparsehash run: | set -e git config --global --add safe.directory ${GITHUB_WORKSPACE} @@ -712,7 +718,7 @@ jobs: with: usesh: true mem: 4096 - prepare: pkg_add git cmake pkgconf boost glib2 qt6-qtbase qt6-qttools sqlite gstreamer1 gstreamer1-plugins-base chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf libgpod fftw3 icu4c kdsingleapplication pulseaudio + prepare: pkg_add git cmake pkgconf boost glib2 qt6-qtbase qt6-qttools sqlite gstreamer1 gstreamer1-plugins-base chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf libgpod fftw3 icu4c kdsingleapplication pulseaudio sparsehash run: | set -e export LDFLAGS="-L/usr/local/lib" diff --git a/CMakeLists.txt b/CMakeLists.txt index 0611935e32..1ff86e0920 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -210,6 +210,8 @@ endif() find_package(GTest) +pkg_check_modules(LIBSPARSEHASH IMPORTED_TARGET libsparsehash) + set(QT_VERSION_MAJOR 6) set(QT_MIN_VERSION 6.4.0) set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION_MAJOR}) @@ -371,6 +373,10 @@ optional_component(QPA_QPLATFORMNATIVEINTERFACE ON "QPA Platform Native Interfac DEPENDS "Qt Gui Private" QT_GUI_PRIVATE_FOUND ) +optional_component(STREAMTAGREADER ON "Stream tagreader" + DEPENDS "sparsehash" LIBSPARSEHASH_FOUND +) + if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ) set(HAVE_CHROMAPRINT ON) endif() @@ -1233,6 +1239,11 @@ optional_source(WIN32 src/core/windows7thumbbar.h ) +optional_source(HAVE_STREAMTAGREADER + SOURCES src/tagreader/streamtagreader.cpp src/tagreader/tagreaderreadstreamrequest.cpp src/tagreader/tagreaderreadstreamreply.cpp + HEADERS src/tagreader/tagreaderreadstreamreply.h +) + if(HAVE_GLOBALSHORTCUTS) optional_source(HAVE_GLOBALSHORTCUTS @@ -1521,6 +1532,7 @@ target_link_libraries(strawberry_lib PUBLIC $<$:Qt${QT_VERSION_MAJOR}::GuiPrivate> ICU::uc ICU::i18n + $<$:PkgConfig::LIBSPARSEHASH> $<$:ALSA::ALSA> $<$:PkgConfig::LIBPULSE> $<$:PkgConfig::CHROMAPRINT> diff --git a/debian/control b/debian/control index 4803805f33..e6204a06e0 100644 --- a/debian/control +++ b/debian/control @@ -29,7 +29,8 @@ Build-Depends: debhelper-compat (= 12), libmtp-dev, libchromaprint-dev, libfftw3-dev, - libebur128-dev + libebur128-dev, + libsparsehash-dev Standards-Version: 4.7.0 Package: strawberry diff --git a/dist/unix/strawberry.spec.in b/dist/unix/strawberry.spec.in index 989faa8073..1f1e205049 100644 --- a/dist/unix/strawberry.spec.in +++ b/dist/unix/strawberry.spec.in @@ -63,6 +63,7 @@ BuildRequires: pkgconfig(libcdio) BuildRequires: pkgconfig(libebur128) BuildRequires: pkgconfig(libgpod-1.0) BuildRequires: pkgconfig(libmtp) +BuildRequires: pkgconfig(libsparsehash) BuildRequires: cmake(GTest) BuildRequires: pkgconfig(gmock) diff --git a/src/config.h.in b/src/config.h.in index e703dbe467..4e1fb1b56f 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -27,6 +27,7 @@ #cmakedefine HAVE_GLOBALSHORTCUTS #cmakedefine HAVE_X11_GLOBALSHORTCUTS #cmakedefine HAVE_KGLOBALACCEL_GLOBALSHORTCUTS +#cmakedefine HAVE_STREAMTAGREADER #cmakedefine HAVE_SUBSONIC #cmakedefine HAVE_TIDAL #cmakedefine HAVE_SPOTIFY diff --git a/src/tagreader/streamtagreader.cpp b/src/tagreader/streamtagreader.cpp new file mode 100644 index 0000000000..2b8f6cb2a8 --- /dev/null +++ b/src/tagreader/streamtagreader.cpp @@ -0,0 +1,211 @@ +/* + * Strawberry Music Player + * Copyright 2012, David Sansome + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "core/logging.h" +#include "core/networkaccessmanager.h" + +#include "streamtagreader.h" + +namespace { +constexpr TagLibLengthType kTagLibPrefixCacheBytes = 64UL * 1024UL; +constexpr TagLibLengthType kTagLibSuffixCacheBytes = 8UL * 1024UL; +} // namespace + +StreamTagReader::StreamTagReader(const QUrl &url, const QString &filename, const quint64 length, const QString &authorization_header) + : url_(url), + filename_(filename), + encoded_filename_(filename_.toUtf8()), + length_(static_cast(length)), + authorization_header_(authorization_header), + network_(new NetworkAccessManager), + cursor_(0), + cache_(length), + num_requests_(0) { + + network_->setAutoDeleteReplies(true); + +} + +TagLib::FileName StreamTagReader::name() const { return encoded_filename_.data(); } + +TagLib::ByteVector StreamTagReader::readBlock(const TagLibLengthType length) { + + const uint start = static_cast(cursor_); + const uint end = static_cast(std::min(cursor_ + length - 1, length_ - 1)); + + if (end < start) { + return TagLib::ByteVector(); + } + + if (CheckCache(start, end)) { + const TagLib::ByteVector cached = GetCached(start, end); + cursor_ += static_cast(cached.size()); + return cached; + } + + QNetworkRequest request(url_); + if (!authorization_header_.isEmpty()) { + request.setRawHeader("Authorization", authorization_header_.toUtf8()); + } + request.setRawHeader("Range", QStringLiteral("bytes=%1-%2").arg(start).arg(end).toUtf8()); + request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + + QNetworkReply *reply = network_->get(request); + ++num_requests_; + + QEventLoop event_loop; + QObject::connect(reply, &QNetworkReply::finished, &event_loop, &QEventLoop::quit); + event_loop.exec(); + + if (reply->error() != QNetworkReply::NoError) { + qLog(Error) << "Unable to get tags from stream for" << url_ << "got error:" << reply->errorString(); + return TagLib::ByteVector(); + } + + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) { + const int http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (http_status_code >= 400) { + qLog(Error) << "Unable to get tags from stream for" << url_ << "received HTTP code" << http_status_code; + return TagLib::ByteVector(); + } + } + + const QByteArray data = reply->readAll(); + const TagLib::ByteVector bytes(data.data(), static_cast(data.size())); + cursor_ += static_cast(data.size()); + + FillCache(start, bytes); + + return bytes; + +} + +void StreamTagReader::writeBlock(const TagLib::ByteVector &data) { + Q_UNUSED(data); +} + +void StreamTagReader::insert(const TagLib::ByteVector &data, const TagLibUOffsetType start, const TagLibLengthType replace) { + Q_UNUSED(data) + Q_UNUSED(start) + Q_UNUSED(replace) +} + +void StreamTagReader::removeBlock(const TagLibUOffsetType start, const TagLibLengthType length) { + Q_UNUSED(start) + Q_UNUSED(length) +} + +bool StreamTagReader::readOnly() const { return true; } + +bool StreamTagReader::isOpen() const { return true; } + +void StreamTagReader::seek(const TagLibOffsetType offset, const TagLib::IOStream::Position position) { + + switch (position) { + case TagLib::IOStream::Beginning: + cursor_ = offset; + break; + + case TagLib::IOStream::Current: + cursor_ = std::min(cursor_ + static_cast(offset), length_); + break; + + case TagLib::IOStream::End: + // This should really not have qAbs(), but OGG reading needs it. + cursor_ = std::max(static_cast(0), length_ - qAbs(static_cast(offset))); + break; + } + +} + +void StreamTagReader::clear() { cursor_ = 0; } + +TagLibOffsetType StreamTagReader::tell() const { return static_cast(cursor_); } + +TagLibOffsetType StreamTagReader::length() { return static_cast(length_); } + +void StreamTagReader::truncate(const TagLibOffsetType length) { + Q_UNUSED(length) +} + +bool StreamTagReader::CheckCache(const uint start, const uint end) { + + for (uint i = start; i <= end; ++i) { + if (!cache_.test(i)) { + return false; + } + } + + return true; + +} + +void StreamTagReader::FillCache(const uint start, const TagLib::ByteVector &data) { + + for (uint i = 0; i < data.size(); ++i) { + cache_.set(start + i, data[static_cast(i)]); + } + +} + +TagLib::ByteVector StreamTagReader::GetCached(const uint start, const uint end) { + + const uint size = end - start + 1U; + TagLib::ByteVector data(size); + for (uint i = 0; i < size; ++i) { + data[static_cast(i)] = cache_.get(start + i); + } + + return data; + +} + +void StreamTagReader::PreCache() { + + // For reading the tags of an MP3, TagLib tends to request: + // 1. The first 1024 bytes + // 2. Somewhere between the first 2KB and first 60KB + // 3. The last KB or two. + // 4. Somewhere in the first 64KB again + // + // OGG Vorbis may read the last 4KB. + // + // So, if we precache the first 64KB and the last 8KB we should be sorted :-) + // Ideally, we would use bytes=0-655364,-8096 but Google Drive does not seem + // to support multipart byte ranges yet so we have to make do with two requests. + + seek(0, TagLib::IOStream::Beginning); + readBlock(kTagLibPrefixCacheBytes); + seek(kTagLibSuffixCacheBytes, TagLib::IOStream::End); + readBlock(kTagLibSuffixCacheBytes); + clear(); + +} diff --git a/src/tagreader/streamtagreader.h b/src/tagreader/streamtagreader.h new file mode 100644 index 0000000000..b35f61d33f --- /dev/null +++ b/src/tagreader/streamtagreader.h @@ -0,0 +1,89 @@ +/* + * Strawberry Music Player + * Copyright 2012, David Sansome + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef STREAMTAGREADER_H +#define STREAMTAGREADER_H + +#include +#include + +#include +#include +#include + +#include "includes/scoped_ptr.h" +#include "core/networkaccessmanager.h" + +#if TAGLIB_MAJOR_VERSION >= 2 +using TagLibLengthType = size_t; +using TagLibUOffsetType = TagLib::offset_t; +using TagLibOffsetType = TagLib::offset_t; +#else +using TagLibLengthType = ulong; +using TagLibUOffsetType = ulong; +using TagLibOffsetType = long; +#endif + +class StreamTagReader : public TagLib::IOStream { + + public: + explicit StreamTagReader(const QUrl &url, const QString &filename, const quint64 length, const QString &authorization_header); + + virtual TagLib::FileName name() const override; + virtual TagLib::ByteVector readBlock(const TagLibLengthType length) override; + virtual void writeBlock(const TagLib::ByteVector &data) override; + virtual void insert(const TagLib::ByteVector &data, const TagLibUOffsetType start, const TagLibLengthType replace) override; + virtual void removeBlock(const TagLibUOffsetType start, const TagLibLengthType length) override; + virtual bool readOnly() const override; + virtual bool isOpen() const override; + virtual void seek(const TagLibOffsetType offset, const TagLib::IOStream::Position position) override; + virtual void clear() override; + virtual TagLibOffsetType tell() const override; + virtual TagLibOffsetType length() override; + virtual void truncate(const TagLibOffsetType length) override; + + google::sparsetable::size_type cached_bytes() const { + return cache_.num_nonempty(); + } + + int num_requests() const { return num_requests_; } + + void PreCache(); + + private: + bool CheckCache(const uint start, const uint end); + void FillCache(const uint start, const TagLib::ByteVector &data); + TagLib::ByteVector GetCached(const uint start, const uint end); + + private: + const QUrl url_; + const QString filename_; + const QByteArray encoded_filename_; + const TagLibLengthType length_; + const QString authorization_header_; + + ScopedPtr network_; + + TagLibLengthType cursor_; + google::sparsetable cache_; + int num_requests_; +}; + +#endif // STREAMTAGREADER_H diff --git a/src/tagreader/tagreaderbase.cpp b/src/tagreader/tagreaderbase.cpp index 90fb48aaea..e5730adf16 100644 --- a/src/tagreader/tagreaderbase.cpp +++ b/src/tagreader/tagreaderbase.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2024, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/tagreader/tagreaderbase.h b/src/tagreader/tagreaderbase.h index a88ae0199c..4022e5c1a0 100644 --- a/src/tagreader/tagreaderbase.h +++ b/src/tagreader/tagreaderbase.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2024, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -41,6 +41,10 @@ class TagReaderBase { virtual TagReaderResult IsMediaFile(const QString &filename) const = 0; virtual TagReaderResult ReadFile(const QString &filename, Song *song) const = 0; +#ifdef HAVE_STREAMTAGREADER + virtual TagReaderResult ReadStream(const QUrl &url, const QString &filename, const quint64 size, const quint64 mtime, const QString &authorization_header, Song *song) const = 0; +#endif + virtual TagReaderResult WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) const = 0; virtual TagReaderResult LoadEmbeddedCover(const QString &filename, QByteArray &data) const = 0; diff --git a/src/tagreader/tagreaderclient.cpp b/src/tagreader/tagreaderclient.cpp index 2156e4aacd..dd4b510bee 100644 --- a/src/tagreader/tagreaderclient.cpp +++ b/src/tagreader/tagreaderclient.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2024, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -37,6 +38,7 @@ #include "tagreaderrequest.h" #include "tagreaderismediafilerequest.h" #include "tagreaderreadfilerequest.h" +#include "tagreaderreadstreamrequest.h" #include "tagreaderwritefilerequest.h" #include "tagreaderloadcoverdatarequest.h" #include "tagreaderloadcoverimagerequest.h" @@ -45,6 +47,7 @@ #include "tagreadersaveratingrequest.h" #include "tagreaderreply.h" #include "tagreaderreadfilereply.h" +#include "tagreaderreadstreamreply.h" #include "tagreaderloadcoverdatareply.h" #include "tagreaderloadcoverimagereply.h" @@ -174,6 +177,17 @@ void TagReaderClient::ProcessRequest(TagReaderRequestPtr request) { } } } +#ifdef HAVE_STREAMTAGREADER + else if (TagReaderReadStreamRequestPtr read_stream_request = dynamic_pointer_cast(request)) { + Song song; + result = ReadStreamBlocking(read_stream_request->url, read_stream_request->filename, read_stream_request->size, read_stream_request->mtime, read_stream_request->authorization_header, &song); + if (result.success()) { + if (TagReaderReadStreamReplyPtr read_stream_reply = qSharedPointerDynamicCast(reply)) { + read_stream_reply->set_song(song); + } + } + } +#endif // HAVE_STREAMTAGREADER else if (TagReaderWriteFileRequestPtr write_file_request = dynamic_pointer_cast(request)) { result = WriteFileBlocking(write_file_request->filename, write_file_request->song, write_file_request->save_tags_options, write_file_request->save_tag_cover_data); } @@ -257,6 +271,32 @@ TagReaderReadFileReplyPtr TagReaderClient::ReadFileAsync(const QString &filename } +#ifdef HAVE_STREAMTAGREADER +TagReaderResult TagReaderClient::ReadStreamBlocking(const QUrl &url, const QString &filename, const quint64 size, const quint64 mtime, const QString &authorization_header, Song *song) { + + return tagreader_.ReadStream(url, filename, size, mtime, authorization_header, song); + +} + +TagReaderReadStreamReplyPtr TagReaderClient::ReadStreamAsync(const QUrl &url, const QString &filename, const quint64 size, const quint64 mtime, const QString &authorization_header) { + + Q_ASSERT(QThread::currentThread() != thread()); + + TagReaderReadStreamReplyPtr reply = TagReaderReply::Create(url, filename); + + TagReaderReadStreamRequestPtr request = TagReaderReadStreamRequest::Create(url, filename); + request->reply = reply; + request->size = size; + request->mtime = mtime; + request->authorization_header = authorization_header; + + EnqueueRequest(request); + + return reply; + +} +#endif // HAVE_STREAMTAGREADER + TagReaderResult TagReaderClient::WriteFileBlocking(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) { return tagreader_.WriteFile(filename, song, save_tags_options, save_tag_cover_data); diff --git a/src/tagreader/tagreaderclient.h b/src/tagreader/tagreaderclient.h index cf8814fc46..d13dba3e76 100644 --- a/src/tagreader/tagreaderclient.h +++ b/src/tagreader/tagreaderclient.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2024, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,7 +29,6 @@ #include #include -#include "includes/shared_ptr.h" #include "includes/mutex_protected.h" #include "core/song.h" @@ -39,6 +38,7 @@ #include "tagreaderresult.h" #include "tagreaderreply.h" #include "tagreaderreadfilereply.h" +#include "tagreaderreadstreamreply.h" #include "tagreaderloadcoverdatareply.h" #include "tagreaderloadcoverimagereply.h" #include "savetagsoptions.h" @@ -67,6 +67,11 @@ class TagReaderClient : public QObject { TagReaderResult ReadFileBlocking(const QString &filename, Song *song); [[nodiscard]] TagReaderReadFileReplyPtr ReadFileAsync(const QString &filename); +#ifdef HAVE_STREAMTAGREADER + TagReaderResult ReadStreamBlocking(const QUrl &url, const QString &filename, const quint64 size, const quint64 mtime, const QString &authorization_header, Song *song); + [[nodiscard]] TagReaderReadStreamReplyPtr ReadStreamAsync(const QUrl &url, const QString &filename, const quint64 size, const quint64 mtime, const QString &authorization_header); +#endif + TagReaderResult WriteFileBlocking(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options = SaveTagsOption::Tags, const SaveTagCoverData &save_tag_cover_data = SaveTagCoverData()); [[nodiscard]] TagReaderReplyPtr WriteFileAsync(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options = SaveTagsOption::Tags, const SaveTagCoverData &save_tag_cover_data = SaveTagCoverData()); diff --git a/src/tagreader/tagreadergme.cpp b/src/tagreader/tagreadergme.cpp index 06d7ef8503..2c4906bb83 100644 --- a/src/tagreader/tagreadergme.cpp +++ b/src/tagreader/tagreadergme.cpp @@ -301,6 +301,21 @@ TagReaderResult TagReaderGME::ReadFile(const QString &filename, Song *song) cons } +#ifdef HAVE_STREAMTAGREADER +TagReaderResult TagReaderGME::ReadStream(const QUrl &url, const QString &filename, const quint64 size, const quint64 mtime, const QString &authorization_header, Song *song) const { + + Q_UNUSED(url); + Q_UNUSED(filename); + Q_UNUSED(size); + Q_UNUSED(mtime); + Q_UNUSED(authorization_header); + Q_UNUSED(song); + + return TagReaderResult::ErrorCode::Unsupported; + +} +#endif + TagReaderResult TagReaderGME::WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) const { Q_UNUSED(filename); diff --git a/src/tagreader/tagreadergme.h b/src/tagreader/tagreadergme.h index 48cdc79ad0..c69fe18063 100644 --- a/src/tagreader/tagreadergme.h +++ b/src/tagreader/tagreadergme.h @@ -102,6 +102,11 @@ class TagReaderGME : public TagReaderBase { TagReaderResult IsMediaFile(const QString &filename) const override; TagReaderResult ReadFile(const QString &filename, Song *song) const override; + +#ifdef HAVE_STREAMTAGREADER + TagReaderResult ReadStream(const QUrl &url, const QString &filename, const quint64 size, const quint64 mtime, const QString &authorization_header, Song *song) const override; +#endif + TagReaderResult WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) const override; TagReaderResult LoadEmbeddedCover(const QString &filename, QByteArray &data) const override; diff --git a/src/tagreader/tagreaderreadstreamreply.cpp b/src/tagreader/tagreaderreadstreamreply.cpp new file mode 100644 index 0000000000..d14824d747 --- /dev/null +++ b/src/tagreader/tagreaderreadstreamreply.cpp @@ -0,0 +1,44 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include + +#include "core/logging.h" +#include "tagreaderreadstreamreply.h" + +TagReaderReadStreamReply::TagReaderReadStreamReply(const QUrl &_url, const QString &_filename, QObject *parent) + : TagReaderReply(_filename, parent), url_(_url) {} + +void TagReaderReadStreamReply::Finish() { + + qLog(Debug) << "Finishing tagreader reply for" << url_; + + finished_ = true; + + QMetaObject::invokeMethod(this, &TagReaderReadStreamReply::EmitFinished, Qt::QueuedConnection); + +} + +void TagReaderReadStreamReply::EmitFinished() { + + Q_EMIT TagReaderReply::Finished(filename_, result_); + Q_EMIT TagReaderReadStreamReply::Finished(url_, song_, result_); + +} diff --git a/src/tagreader/tagreaderreadstreamreply.h b/src/tagreader/tagreaderreadstreamreply.h new file mode 100644 index 0000000000..bc598fe1a8 --- /dev/null +++ b/src/tagreader/tagreaderreadstreamreply.h @@ -0,0 +1,55 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef TAGREADERREADSTREAMREPLY_H +#define TAGREADERREADSTREAMREPLY_H + +#include +#include +#include + +#include "core/song.h" +#include "tagreaderreply.h" +#include "tagreaderresult.h" + +class TagReaderReadStreamReply : public TagReaderReply { + Q_OBJECT + + public: + explicit TagReaderReadStreamReply(const QUrl &url, const QString &_filename, QObject *parent = nullptr); + + void Finish() override; + + Song song() const { return song_; } + void set_song(const Song &song) { song_ = song; } + + Q_SIGNALS: + void Finished(const QUrl &url, const Song &song, const TagReaderResult &result); + + private Q_SLOTS: + void EmitFinished() override; + + private: + Song song_; + QUrl url_; +}; + +using TagReaderReadStreamReplyPtr = QSharedPointer; + +#endif // TAGREADERREADSTREAMREPLY_H diff --git a/src/tagreader/tagreaderreadstreamrequest.cpp b/src/tagreader/tagreaderreadstreamrequest.cpp new file mode 100644 index 0000000000..9940232556 --- /dev/null +++ b/src/tagreader/tagreaderreadstreamrequest.cpp @@ -0,0 +1,25 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include + +#include "tagreaderreadstreamrequest.h" + +TagReaderReadStreamRequest::TagReaderReadStreamRequest(const QUrl &_url, const QString &_filename) : TagReaderRequest(_url, _filename), size(0), mtime(0) {} diff --git a/src/tagreader/tagreaderreadstreamrequest.h b/src/tagreader/tagreaderreadstreamrequest.h new file mode 100644 index 0000000000..07cffd1f7c --- /dev/null +++ b/src/tagreader/tagreaderreadstreamrequest.h @@ -0,0 +1,42 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef TAGREADERREADSTREAMREQUEST_H +#define TAGREADERREADSTREAMREQUEST_H + +#include +#include + +#include "includes/shared_ptr.h" +#include "tagreaderrequest.h" + +using std::make_shared; + +class TagReaderReadStreamRequest : public TagReaderRequest { + public: + explicit TagReaderReadStreamRequest(const QUrl &_url, const QString &_filename); + static SharedPtr Create(const QUrl &_url, const QString &_filename) { return make_shared(_url, _filename); } + quint64 size; + quint64 mtime; + QString authorization_header; +}; + +using TagReaderReadStreamRequestPtr = SharedPtr; + +#endif // TAGREADERREADSTREAMREQUEST_H diff --git a/src/tagreader/tagreaderreply.cpp b/src/tagreader/tagreaderreply.cpp index de0507a273..ffee8d0410 100644 --- a/src/tagreader/tagreaderreply.cpp +++ b/src/tagreader/tagreaderreply.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2024, Jonas Kvinge + * Copyright 2024-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/tagreader/tagreaderreply.h b/src/tagreader/tagreaderreply.h index e301e43937..1de1390c44 100644 --- a/src/tagreader/tagreaderreply.h +++ b/src/tagreader/tagreaderreply.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2024, Jonas Kvinge + * Copyright 2024-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,6 +22,7 @@ #include #include +#include #include #include "tagreaderresult.h" @@ -38,6 +39,11 @@ class TagReaderReply : public QObject { return QSharedPointer(new T(filename)); } + template + static QSharedPointer Create(const QUrl &url, const QString &filename) { + return QSharedPointer(new T(url, filename)); + } + QString filename() const { return filename_; } TagReaderResult result() const { return result_; } diff --git a/src/tagreader/tagreaderrequest.cpp b/src/tagreader/tagreaderrequest.cpp index 172ae1562b..4e353f1a74 100644 --- a/src/tagreader/tagreaderrequest.cpp +++ b/src/tagreader/tagreaderrequest.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2024, Jonas Kvinge + * Copyright 2024-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,8 +27,19 @@ TagReaderRequest::TagReaderRequest(const QString &_filename) : filename(_filenam } +TagReaderRequest::TagReaderRequest(const QUrl &_url, const QString &_filename) : filename(_filename), url(_url) { + + qLog(Debug) << "New tagreader request for" << filename << url; + +} + TagReaderRequest::~TagReaderRequest() { - qLog(Debug) << "Tagreader request for" << filename << "deleted"; + if (url.isValid()) { + qLog(Debug) << "Tagreader request for" << filename << url << "deleted"; + } + else { + qLog(Debug) << "Tagreader request for" << filename << "deleted"; + } } diff --git a/src/tagreader/tagreaderrequest.h b/src/tagreader/tagreaderrequest.h index fdc8e6380a..83cdfe055a 100644 --- a/src/tagreader/tagreaderrequest.h +++ b/src/tagreader/tagreaderrequest.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2024, Jonas Kvinge + * Copyright 2024-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,6 +21,7 @@ #define TAGREADERREQUEST_H #include +#include #include "includes/shared_ptr.h" #include "tagreaderreply.h" @@ -28,8 +29,10 @@ class TagReaderRequest { public: explicit TagReaderRequest(const QString &_filename); + explicit TagReaderRequest(const QUrl &_url, const QString &_filename); virtual ~TagReaderRequest(); QString filename; + QUrl url; TagReaderReplyPtr reply; }; diff --git a/src/tagreader/tagreadertaglib.cpp b/src/tagreader/tagreadertaglib.cpp index 6310b72f06..acec931ca0 100644 --- a/src/tagreader/tagreadertaglib.cpp +++ b/src/tagreader/tagreadertaglib.cpp @@ -1,7 +1,7 @@ /* * Strawberry Music Player * Copyright 2013, David Sansome - * Copyright 2018-2024, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,6 +21,7 @@ #include "config.h" #include "tagreadertaglib.h" +#include "streamtagreader.h" #include #include @@ -94,13 +95,14 @@ #include #include +#include "includes/scoped_ptr.h" #include "core/logging.h" #include "core/song.h" #include "constants/timeconstants.h" #include "albumcovertagdata.h" -using std::unique_ptr; +using std::make_unique; using namespace Qt::Literals::StringLiterals; #undef TStringToQString @@ -240,6 +242,7 @@ class FileRefFactory { FileRefFactory() = default; virtual ~FileRefFactory() = default; virtual TagLib::FileRef *GetFileRef(const QString &filename) = 0; + virtual TagLib::FileRef *GetFileRef(TagLib::IOStream *iostream) = 0; private: Q_DISABLE_COPY(FileRefFactory) @@ -256,6 +259,10 @@ class TagLibFileRefFactory : public FileRefFactory { #endif } + TagLib::FileRef *GetFileRef(TagLib::IOStream *iostream) override { + return new TagLib::FileRef(iostream); + } + private: Q_DISABLE_COPY(TagLibFileRefFactory) }; @@ -270,7 +277,7 @@ TagReaderResult TagReaderTagLib::IsMediaFile(const QString &filename) const { qLog(Debug) << "Checking for valid file" << filename; - unique_ptr fileref(factory_->GetFileRef(filename)); + ScopedPtr fileref(factory_->GetFileRef(filename)); return fileref && !fileref->isNull() && fileref->file() && @@ -309,41 +316,9 @@ Song::FileType TagReaderTagLib::GuessFileType(TagLib::FileRef *fileref) { } -TagReaderResult TagReaderTagLib::ReadFile(const QString &filename, Song *song) const { - - if (filename.isEmpty()) { - return TagReaderResult::ErrorCode::FilenameMissing; - } - - qLog(Debug) << "Reading tags from" << filename; - - const QFileInfo fileinfo(filename); - if (!fileinfo.exists()) { - qLog(Error) << "File" << filename << "does not exist"; - return TagReaderResult::ErrorCode::FileDoesNotExist; - } - - if (song->source() == Song::Source::Unknown) song->set_source(Song::Source::LocalFile); - - const QUrl url = QUrl::fromLocalFile(filename); - song->set_basefilename(fileinfo.fileName()); - song->set_url(url); - song->set_filesize(fileinfo.size()); - song->set_mtime(fileinfo.lastModified().isValid() ? std::max(fileinfo.lastModified().toSecsSinceEpoch(), 0LL) : 0LL); - song->set_ctime(fileinfo.birthTime().isValid() ? std::max(fileinfo.birthTime().toSecsSinceEpoch(), 0LL) : fileinfo.lastModified().isValid() ? std::max(fileinfo.lastModified().toSecsSinceEpoch(), 0LL) : 0LL); - if (song->ctime() <= 0) { - song->set_ctime(song->mtime()); - } - song->set_lastseen(QDateTime::currentSecsSinceEpoch()); - song->set_init_from_file(true); - - unique_ptr fileref(factory_->GetFileRef(filename)); - if (!fileref || fileref->isNull()) { - qLog(Error) << "TagLib could not open file" << filename; - return TagReaderResult::ErrorCode::FileOpenError; - } +TagReaderResult TagReaderTagLib::Read(SharedPtr fileref, Song *song) const { - song->set_filetype(GuessFileType(fileref.get())); + song->set_filetype(GuessFileType(&*fileref)); if (fileref->audioProperties()) { song->set_bitrate(fileref->audioProperties()->bitrate()); @@ -370,12 +345,14 @@ TagReaderResult TagReaderTagLib::ReadFile(const QString &filename, Song *song) c // apart, so we keep specific behavior for some formats by adding another "else if" block below. if (TagLib::Ogg::XiphComment *vorbis_comment = dynamic_cast(fileref->file()->tag())) { ParseVorbisComments(vorbis_comment->fieldListMap(), &disc, &compilation, song); - TagLib::List pictures = vorbis_comment->pictureList(); - if (!pictures.isEmpty()) { - for (TagLib::FLAC::Picture *picture : pictures) { - if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) { - song->set_art_embedded(true); - break; + if (song->url().isLocalFile()) { + TagLib::List pictures = vorbis_comment->pictureList(); + if (!pictures.isEmpty()) { + for (TagLib::FLAC::Picture *picture : pictures) { + if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) { + song->set_art_embedded(true); + break; + } } } } @@ -385,12 +362,14 @@ TagReaderResult TagReaderTagLib::ReadFile(const QString &filename, Song *song) c song->set_bitdepth(file_flac->audioProperties()->bitsPerSample()); if (file_flac->xiphComment()) { ParseVorbisComments(file_flac->xiphComment()->fieldListMap(), &disc, &compilation, song); - TagLib::List pictures = file_flac->pictureList(); - if (!pictures.isEmpty()) { - for (TagLib::FLAC::Picture *picture : pictures) { - if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) { - song->set_art_embedded(true); - break; + if (song->url().isLocalFile()) { + TagLib::List pictures = file_flac->pictureList(); + if (!pictures.isEmpty()) { + for (TagLib::FLAC::Picture *picture : pictures) { + if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) { + song->set_art_embedded(true); + break; + } } } } @@ -497,16 +476,103 @@ TagReaderResult TagReaderTagLib::ReadFile(const QString &filename, Song *song) c if (song->lastplayed() <= 0) { song->set_lastplayed(-1); } if (song->filetype() == Song::FileType::Unknown) { - qLog(Error) << "Unknown audio filetype reading" << filename; + return TagReaderResult::ErrorCode::Unsupported; + } + + return TagReaderResult::ErrorCode::Success; + +} + +TagReaderResult TagReaderTagLib::ReadFile(const QString &filename, Song *song) const { + + if (filename.isEmpty()) { + return TagReaderResult::ErrorCode::FilenameMissing; + } + + qLog(Debug) << "Reading tags from file" << filename; + + const QFileInfo fileinfo(filename); + if (!fileinfo.exists()) { + qLog(Error) << "File" << filename << "does not exist"; + return TagReaderResult::ErrorCode::FileDoesNotExist; + } + + if (song->source() == Song::Source::Unknown) song->set_source(Song::Source::LocalFile); + + const QUrl url = QUrl::fromLocalFile(filename); + song->set_basefilename(fileinfo.fileName()); + song->set_url(url); + song->set_filesize(fileinfo.size()); + song->set_mtime(fileinfo.lastModified().isValid() ? std::max(fileinfo.lastModified().toSecsSinceEpoch(), 0LL) : 0LL); + song->set_ctime(fileinfo.birthTime().isValid() ? std::max(fileinfo.birthTime().toSecsSinceEpoch(), 0LL) : fileinfo.lastModified().isValid() ? std::max(fileinfo.lastModified().toSecsSinceEpoch(), 0LL) : 0LL); + if (song->ctime() <= 0) { + song->set_ctime(song->mtime()); + } + song->set_lastseen(QDateTime::currentSecsSinceEpoch()); + song->set_init_from_file(true); + + SharedPtr fileref(factory_->GetFileRef(filename)); + if (!fileref || fileref->isNull()) { + qLog(Error) << "TagLib could not open file" << filename; + return TagReaderResult::ErrorCode::FileOpenError; + } + + const TagReaderResult result = Read(fileref, song); + if (result.error_code == TagReaderResult::ErrorCode::Unsupported) { + qLog(Error) << "Unknown audio filetype reading file" << filename; return TagReaderResult::ErrorCode::Unsupported; } qLog(Debug) << "Got tags for" << filename; - return TagReaderResult::ErrorCode::Success; + return result; } +#ifdef HAVE_STREAMTAGREADER + +TagReaderResult TagReaderTagLib::ReadStream(const QUrl &url, + const QString &filename, + const quint64 size, + const quint64 mtime, + const QString &authorization_header, + Song *song) const { + + qLog(Debug) << "Loading tags from stream" << url << filename; + + song->set_url(url); + song->set_basefilename(QFileInfo(filename).baseName()); + song->set_filesize(static_cast(size)); + song->set_ctime(static_cast(mtime)); + song->set_mtime(static_cast(mtime)); + + ScopedPtr stream = make_unique(url, filename, size, authorization_header); + stream->PreCache(); + + if (stream->num_requests() > 2) { + qLog(Warning) << "Total requests for file" << filename << stream->num_requests() << stream->cached_bytes(); + } + + SharedPtr fileref(factory_->GetFileRef(&*stream)); + if (!fileref || fileref->isNull()) { + qLog(Error) << "TagLib could not open stream" << filename << url; + return TagReaderResult::ErrorCode::FileOpenError; + } + + const TagReaderResult result = Read(fileref, song); + if (result.error_code == TagReaderResult::ErrorCode::Unsupported) { + qLog(Error) << "Unknown audio filetype reading stream" << filename << url; + return result; + } + + qLog(Debug) << "Got tags for stream" << filename << url; + + return result; + +} + +#endif // HAVE_STREAMTAGREADER + void TagReaderTagLib::ParseID3v2Tags(TagLib::ID3v2::Tag *tag, QString *disc, QString *compilation, Song *song) const { TagLib::ID3v2::FrameListMap map = tag->frameListMap(); @@ -538,7 +604,7 @@ void TagReaderTagLib::ParseID3v2Tags(TagLib::ID3v2::Tag *tag, QString *disc, QSt song->set_lyrics(map[kID3v2_UnsychronizedLyrics].front()->toString()); } - if (map.contains(kID3v2_CoverArt)) song->set_art_embedded(true); + if (map.contains(kID3v2_CoverArt) && song->url().isLocalFile()) song->set_art_embedded(true); // Find a suitable comment tag. For now we ignore iTunNORM comments. for (uint i = 0; i < map[kID3v2_CommercialFrame].size(); ++i) { @@ -652,7 +718,7 @@ void TagReaderTagLib::ParseVorbisComments(const TagLib::Ogg::FieldListMap &map, if (map.contains(kVorbisComment_Disc)) *disc = TagLibStringToQString(map[kVorbisComment_Disc].front()).trimmed(); if (map.contains(kVorbisComment_Compilation)) *compilation = TagLibStringToQString(map[kVorbisComment_Compilation].front()).trimmed(); - if (map.contains(kVorbisComment_CoverArt) || map.contains(kVorbisComment_MetadataBlockPicture)) song->set_art_embedded(true); + if ((map.contains(kVorbisComment_CoverArt) || map.contains(kVorbisComment_MetadataBlockPicture)) && song->url().isLocalFile()) song->set_art_embedded(true); if (map.contains(kVorbisComment_FMPS_Playcount) && song->playcount() <= 0) { const int playcount = TagLibStringToQString(map[kVorbisComment_FMPS_Playcount].front()).trimmed().toInt(); @@ -689,7 +755,7 @@ void TagReaderTagLib::ParseAPETags(const TagLib::APE::ItemListMap &map, QString } } - if (map.find(kAPE_CoverArt) != map.end()) song->set_art_embedded(true); + if (map.find(kAPE_CoverArt) != map.end() && song->url().isLocalFile()) song->set_art_embedded(true); if (map.contains(kAPE_Compilation)) { *compilation = TagLibStringToQString(TagLib::String::number(map[kAPE_Compilation].toString().toInt())); } @@ -757,7 +823,7 @@ void TagReaderTagLib::ParseMP4Tags(TagLib::MP4::Tag *tag, QString *disc, QString } // Find album cover art - if (tag->item(kMP4_CoverArt).isValid()) { + if (tag->item(kMP4_CoverArt).isValid() && song->url().isLocalFile()) { song->set_art_embedded(true); } @@ -959,7 +1025,7 @@ TagReaderResult TagReaderTagLib::WriteFile(const QString &filename, const Song & cover = LoadAlbumCoverTagData(filename, save_tag_cover_data); } - unique_ptr fileref(factory_->GetFileRef(filename)); + ScopedPtr fileref(factory_->GetFileRef(filename)); if (!fileref || fileref->isNull()) { qLog(Error) << "TagLib could not open file" << filename; return TagReaderResult::ErrorCode::FileOpenError; @@ -1310,7 +1376,7 @@ TagReaderResult TagReaderTagLib::LoadEmbeddedCover(const QString &filename, QByt qLog(Debug) << "Loading cover from" << filename; - unique_ptr fileref(factory_->GetFileRef(filename)); + ScopedPtr fileref(factory_->GetFileRef(filename)); if (!fileref || fileref->isNull()) { qLog(Error) << "TagLib could not open file" << filename; return TagReaderResult::ErrorCode::FileOpenError; @@ -1532,7 +1598,7 @@ TagReaderResult TagReaderTagLib::SaveEmbeddedCover(const QString &filename, cons return TagReaderResult::ErrorCode::FileDoesNotExist; } - unique_ptr fileref(factory_->GetFileRef(filename)); + ScopedPtr fileref(factory_->GetFileRef(filename)); if (!fileref || fileref->isNull()) { qLog(Error) << "TagLib could not open file" << filename; return TagReaderResult::ErrorCode::FileOpenError; @@ -1672,7 +1738,7 @@ TagReaderResult TagReaderTagLib::SaveSongPlaycount(const QString &filename, cons return TagReaderResult::ErrorCode::FileDoesNotExist; } - unique_ptr fileref(factory_->GetFileRef(filename)); + ScopedPtr fileref(factory_->GetFileRef(filename)); if (!fileref || fileref->isNull()) { qLog(Error) << "TagLib could not open file" << filename; return TagReaderResult::ErrorCode::FileOpenError; @@ -1802,7 +1868,7 @@ TagReaderResult TagReaderTagLib::SaveSongRating(const QString &filename, const f return TagReaderResult::ErrorCode::Success; } - unique_ptr fileref(factory_->GetFileRef(filename)); + ScopedPtr fileref(factory_->GetFileRef(filename)); if (!fileref || fileref->isNull()) { qLog(Error) << "TagLib could not open file" << filename; return TagReaderResult::ErrorCode::FileOpenError; diff --git a/src/tagreader/tagreadertaglib.h b/src/tagreader/tagreadertaglib.h index f395746022..7bc5c2332a 100644 --- a/src/tagreader/tagreadertaglib.h +++ b/src/tagreader/tagreadertaglib.h @@ -1,7 +1,7 @@ /* * Strawberry Music Player * Copyright 2013, David Sansome - * Copyright 2018-2024, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -40,6 +40,8 @@ #include #include +#include "includes/scoped_ptr.h" +#include "includes/shared_ptr.h" #include "core/song.h" #include "tagreaderbase.h" @@ -66,6 +68,10 @@ class TagReaderTagLib : public TagReaderBase { TagReaderResult IsMediaFile(const QString &filename) const override; TagReaderResult ReadFile(const QString &filename, Song *song) const override; +#ifdef HAVE_STREAMTAGREADER + TagReaderResult ReadStream(const QUrl &url, const QString &filename, const quint64 size, const quint64 mtime, const QString &authorization_header, Song *song) const override; +#endif + TagReaderResult WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) const override; TagReaderResult LoadEmbeddedCover(const QString &filename, QByteArray &data) const override; @@ -76,6 +82,7 @@ class TagReaderTagLib : public TagReaderBase { private: static Song::FileType GuessFileType(TagLib::FileRef *fileref); + TagReaderResult Read(SharedPtr fileref, Song *song) const; void ParseID3v2Tags(TagLib::ID3v2::Tag *tag, QString *disc, QString *compilation, Song *song) const; void ParseVorbisComments(const TagLib::Ogg::FieldListMap &map, QString *disc, QString *compilation, Song *song) const;