Skip to content

Commit

Permalink
Add stream tagreader
Browse files Browse the repository at this point in the history
  • Loading branch information
jonaski committed Feb 8, 2025
1 parent 215627b commit 615c779
Show file tree
Hide file tree
Showing 23 changed files with 723 additions and 74 deletions.
10 changes: 8 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -193,6 +194,7 @@ jobs:
kdsingleapplication-qt6-devel
gtest-devel
gmock-devel
sparsehash-devel
- name: Checkout
uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -379,6 +381,7 @@ jobs:
lib64qt6dbus-devel
lib64qt6help-devel
lib64qt6test-devel
lib64sparsehash-devel
desktop-file-utils
appstream-util
hicolor-icon-theme
Expand Down Expand Up @@ -466,6 +469,7 @@ jobs:
libmtp-dev
libgpod-dev
libxkbcommon-dev
libsparsehash-dev
qt6-base-dev
qt6-base-private-dev
qt6-base-dev-tools
Expand Down Expand Up @@ -549,6 +553,7 @@ jobs:
libmtp-dev
libgpod-dev
libxkbcommon-dev
libsparsehash-dev
qt6-base-dev
qt6-base-private-dev
qt6-base-dev-tools
Expand Down Expand Up @@ -631,6 +636,7 @@ jobs:
libmtp-dev
libgpod-dev
libxkbcommon-dev
libsparsehash-dev
qt6-base-dev
qt6-base-private-dev
qt6-base-dev-tools
Expand Down Expand Up @@ -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}
Expand All @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1521,6 +1532,7 @@ target_link_libraries(strawberry_lib PUBLIC
$<$<BOOL:${HAVE_QPA_QPLATFORMNATIVEINTERFACE}>:Qt${QT_VERSION_MAJOR}::GuiPrivate>
ICU::uc
ICU::i18n
$<$<BOOL:${HAVE_STREAMTAGREADER}>:PkgConfig::LIBSPARSEHASH>
$<$<BOOL:${HAVE_ALSA}>:ALSA::ALSA>
$<$<BOOL:${HAVE_PULSE}>:PkgConfig::LIBPULSE>
$<$<BOOL:${HAVE_CHROMAPRINT}>:PkgConfig::CHROMAPRINT>
Expand Down
3 changes: 2 additions & 1 deletion debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions dist/unix/strawberry.spec.in
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions src/config.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
211 changes: 211 additions & 0 deletions src/tagreader/streamtagreader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/*
* Strawberry Music Player
* Copyright 2012, David Sansome <[email protected]>
* Copyright 2025, Jonas Kvinge <[email protected]>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/

#include <algorithm>

#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QEventLoop>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSslError>

#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<TagLibLengthType>(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<uint>(cursor_);
const uint end = static_cast<uint>(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<TagLibLengthType>(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<uint>(data.size()));
cursor_ += static_cast<TagLibLengthType>(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<TagLibLengthType>(offset), length_);
break;

case TagLib::IOStream::End:
// This should really not have qAbs(), but OGG reading needs it.
cursor_ = std::max(static_cast<TagLibLengthType>(0), length_ - qAbs(static_cast<TagLibLengthType>(offset)));
break;
}

}

void StreamTagReader::clear() { cursor_ = 0; }

TagLibOffsetType StreamTagReader::tell() const { return static_cast<TagLibOffsetType>(cursor_); }

TagLibOffsetType StreamTagReader::length() { return static_cast<TagLibOffsetType>(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<int>(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<int>(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();

}
Loading

0 comments on commit 615c779

Please sign in to comment.