Add stream tagreader

This commit is contained in:
Jonas Kvinge
2025-01-26 11:05:07 +01:00
parent 215627b0e4
commit 2b52553864
23 changed files with 738 additions and 75 deletions

View File

@@ -0,0 +1,216 @@
/*
* Strawberry Music Player
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 &token_type,
const QString &access_token)
: url_(url),
filename_(filename),
encoded_filename_(filename_.toUtf8()),
length_(static_cast<TagLibLengthType>(length)),
token_type_(token_type),
access_token_(access_token),
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 = GetCache(start, end);
cursor_ += static_cast<TagLibLengthType>(cached.size());
return cached;
}
QNetworkRequest request(url_);
if (!token_type_.isEmpty() && !access_token_.isEmpty()) {
request.setRawHeader("Authorization", token_type_.toUtf8() + " " + access_token_.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::GetCache(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();
}

View File

@@ -0,0 +1,94 @@
/*
* Strawberry Music Player
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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/>.
*
*/
#ifndef STREAMTAGREADER_H
#define STREAMTAGREADER_H
#include <taglib/tiostream.h>
#include <google/sparsetable>
#include <QByteArray>
#include <QString>
#include <QUrl>
#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 &token_type,
const QString &access_token);
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<char>::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 GetCache(const uint start, const uint end);
private:
const QUrl url_;
const QString filename_;
const QByteArray encoded_filename_;
const TagLibLengthType length_;
const QString token_type_;
const QString access_token_;
ScopedPtr<NetworkAccessManager> network_;
TagLibLengthType cursor_;
google::sparsetable<char> cache_;
int num_requests_;
};
#endif // STREAMTAGREADER_H

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 &token_type, const QString &access_token, 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;

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2019-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2019-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <QMutex>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QImage>
#include <QScopeGuard>
@@ -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<TagReaderReadStreamRequest>(request)) {
Song song;
result = ReadStreamBlocking(read_stream_request->url, read_stream_request->filename, read_stream_request->size, read_stream_request->mtime, read_stream_request->token_type, read_stream_request->access_token, &song);
if (result.success()) {
if (TagReaderReadStreamReplyPtr read_stream_reply = qSharedPointerDynamicCast<TagReaderReadStreamReply>(reply)) {
read_stream_reply->set_song(song);
}
}
}
#endif // HAVE_STREAMTAGREADER
else if (TagReaderWriteFileRequestPtr write_file_request = dynamic_pointer_cast<TagReaderWriteFileRequest>(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,33 @@ 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 &token_type, const QString &access_token, Song *song) {
return tagreader_.ReadStream(url, filename, size, mtime, token_type, access_token, song);
}
TagReaderReadStreamReplyPtr TagReaderClient::ReadStreamAsync(const QUrl &url, const QString &filename, const quint64 size, const quint64 mtime, const QString &token_type, const QString &access_token) {
Q_ASSERT(QThread::currentThread() != thread());
TagReaderReadStreamReplyPtr reply = TagReaderReply::Create<TagReaderReadStreamReply>(url, filename);
TagReaderReadStreamRequestPtr request = TagReaderReadStreamRequest::Create(url, filename);
request->reply = reply;
request->size = size;
request->mtime = mtime;
request->token_type = token_type;
request->access_token = access_token;
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);

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2019-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2019-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <QImage>
#include <QMutex>
#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 &token_type, const QString &access_token, Song *song);
[[nodiscard]] TagReaderReadStreamReplyPtr ReadStreamAsync(const QUrl &url, const QString &filename, const quint64 size, const quint64 mtime, const QString &token_type, const QString &access_token);
#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());

View File

@@ -301,6 +301,22 @@ 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 &token_type, const QString &access_token, Song *song) const {
Q_UNUSED(url);
Q_UNUSED(filename);
Q_UNUSED(size);
Q_UNUSED(mtime);
Q_UNUSED(token_type);
Q_UNUSED(access_token);
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);

View File

@@ -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 &token_type, const QString &access_token, 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;

View File

@@ -0,0 +1,44 @@
/*
* Strawberry Music Player
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <QString>
#include <QUrl>
#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_);
}

View File

@@ -0,0 +1,55 @@
/*
* Strawberry Music Player
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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/>.
*
*/
#ifndef TAGREADERREADSTREAMREPLY_H
#define TAGREADERREADSTREAMREPLY_H
#include <QString>
#include <QUrl>
#include <QSharedPointer>
#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<TagReaderReadStreamReply>;
#endif // TAGREADERREADSTREAMREPLY_H

View File

@@ -0,0 +1,25 @@
/*
* Strawberry Music Player
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <QString>
#include <QUrl>
#include "tagreaderreadstreamrequest.h"
TagReaderReadStreamRequest::TagReaderReadStreamRequest(const QUrl &_url, const QString &_filename) : TagReaderRequest(_url, _filename), size(0), mtime(0) {}

View File

@@ -0,0 +1,43 @@
/*
* Strawberry Music Player
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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/>.
*
*/
#ifndef TAGREADERREADSTREAMREQUEST_H
#define TAGREADERREADSTREAMREQUEST_H
#include <QString>
#include <QUrl>
#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<TagReaderReadStreamRequest> Create(const QUrl &_url, const QString &_filename) { return make_shared<TagReaderReadStreamRequest>(_url, _filename); }
quint64 size;
quint64 mtime;
QString token_type;
QString access_token;
};
using TagReaderReadStreamRequestPtr = SharedPtr<TagReaderReadStreamRequest>;
#endif // TAGREADERREADSTREAMREQUEST_H

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2024-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2024-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <QObject>
#include <QString>
#include <QUrl>
#include <QSharedPointer>
#include "tagreaderresult.h"
@@ -38,6 +39,11 @@ class TagReaderReply : public QObject {
return QSharedPointer<T>(new T(filename));
}
template<typename T>
static QSharedPointer<T> Create(const QUrl &url, const QString &filename) {
return QSharedPointer<T>(new T(url, filename));
}
QString filename() const { return filename_; }
TagReaderResult result() const { return result_; }

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2024-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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() {
TagReaderRequest::TagReaderRequest(const QUrl &_url, const QString &_filename) : filename(_filename), url(_url) {
qLog(Debug) << "Tagreader request for" << filename << "deleted";
qLog(Debug) << "New tagreader request for" << filename << url;
}
TagReaderRequest::~TagReaderRequest() {
if (url.isValid()) {
qLog(Debug) << "Tagreader request for" << filename << url << "deleted";
}
else {
qLog(Debug) << "Tagreader request for" << filename << "deleted";
}
}

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2024-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <QString>
#include <QUrl>
#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;
};

View File

@@ -1,7 +1,7 @@
/*
* Strawberry Music Player
* Copyright 2013, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <memory>
#include <algorithm>
@@ -94,13 +95,14 @@
#include <QDateTime>
#include <QtDebug>
#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<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
ScopedPtr<TagLib::FileRef> 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 {
TagReaderResult TagReaderTagLib::Read(SharedPtr<TagLib::FileRef> fileref, 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<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (!fileref || fileref->isNull()) {
qLog(Error) << "TagLib could not open file" << filename;
return TagReaderResult::ErrorCode::FileOpenError;
}
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<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
ParseVorbisComments(vorbis_comment->fieldListMap(), &disc, &compilation, song);
TagLib::List<TagLib::FLAC::Picture*> 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<TagLib::FLAC::Picture*> 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<TagLib::FLAC::Picture*> 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<TagLib::FLAC::Picture*> 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,104 @@ 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<TagLib::FileRef> 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 &token_type,
const QString &access_token,
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<qint64>(size));
song->set_ctime(static_cast<qint64>(mtime));
song->set_mtime(static_cast<qint64>(mtime));
ScopedPtr<StreamTagReader> stream = make_unique<StreamTagReader>(url, filename, size, token_type, access_token);
stream->PreCache();
if (stream->num_requests() > 2) {
qLog(Warning) << "Total requests for file" << filename << stream->num_requests() << stream->cached_bytes();
}
SharedPtr<TagLib::FileRef> 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 +605,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 +719,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 +756,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 +824,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 +1026,7 @@ TagReaderResult TagReaderTagLib::WriteFile(const QString &filename, const Song &
cover = LoadAlbumCoverTagData(filename, save_tag_cover_data);
}
unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
ScopedPtr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (!fileref || fileref->isNull()) {
qLog(Error) << "TagLib could not open file" << filename;
return TagReaderResult::ErrorCode::FileOpenError;
@@ -1310,7 +1377,7 @@ TagReaderResult TagReaderTagLib::LoadEmbeddedCover(const QString &filename, QByt
qLog(Debug) << "Loading cover from" << filename;
unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
ScopedPtr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (!fileref || fileref->isNull()) {
qLog(Error) << "TagLib could not open file" << filename;
return TagReaderResult::ErrorCode::FileOpenError;
@@ -1532,7 +1599,7 @@ TagReaderResult TagReaderTagLib::SaveEmbeddedCover(const QString &filename, cons
return TagReaderResult::ErrorCode::FileDoesNotExist;
}
unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
ScopedPtr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (!fileref || fileref->isNull()) {
qLog(Error) << "TagLib could not open file" << filename;
return TagReaderResult::ErrorCode::FileOpenError;
@@ -1672,7 +1739,7 @@ TagReaderResult TagReaderTagLib::SaveSongPlaycount(const QString &filename, cons
return TagReaderResult::ErrorCode::FileDoesNotExist;
}
unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
ScopedPtr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (!fileref || fileref->isNull()) {
qLog(Error) << "TagLib could not open file" << filename;
return TagReaderResult::ErrorCode::FileOpenError;
@@ -1802,7 +1869,7 @@ TagReaderResult TagReaderTagLib::SaveSongRating(const QString &filename, const f
return TagReaderResult::ErrorCode::Success;
}
unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
ScopedPtr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (!fileref || fileref->isNull()) {
qLog(Error) << "TagLib could not open file" << filename;
return TagReaderResult::ErrorCode::FileOpenError;

View File

@@ -1,7 +1,7 @@
/*
* Strawberry Music Player
* Copyright 2013, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <taglib/mp4tag.h>
#include <taglib/asftag.h>
#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 &token_type, const QString &access_token, 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<TagLib::FileRef> 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;