Set player artwork on macos

This commit is contained in:
Andrew Rabert committed 2025-12-27 12:00:52 -05:00
1 parent 8a0091cc24
commit d3858e2c21
10 files changed
+412 -306

No files matched your search

+3 -4
View File
@@ -656,13 +656,12 @@
]
},
{
"section": "mpris",
"section": "albumart",
"hidden": true,
"platforms": [ "linux" ],
"values": [
{
"value": "albumArtCacheSize",
"default": 52428800
"value": "cacheSize",
"default": 10485760
}
]
},
+2
View File
@@ -21,6 +21,8 @@ private slots:
void handlePositionUpdate(quint64 position);
void handleUpdateDuration(qint64 duration);
void handleUpdateMetaData(const QVariantMap& meta);
void handleAlbumArtReady(const QByteArray& imageData, const QString& mimeType);
void handleAlbumArtUnavailable();
private:
void* m_delegate;
+44
View File
@@ -3,10 +3,12 @@
//
#include "InputAppleMediaKeys.h"
#include "player/AlbumArtProvider.h"
#include <QDebug>
#import <dlfcn.h>
#import <AppKit/AppKit.h>
#import <MediaPlayer/MediaPlayer.h>
@interface MediaKeysDelegate : NSObject
@@ -98,6 +100,15 @@ bool InputAppleMediaKeys::initInput()
connect(&PlayerComponent::Get(), &PlayerComponent::onMetaData, this,
&InputAppleMediaKeys::handleUpdateMetaData);
// Connect to AlbumArtProvider signals
if (PlayerComponent::Get().albumArtProvider())
{
connect(PlayerComponent::Get().albumArtProvider(), &AlbumArtProvider::artworkReady,
this, &InputAppleMediaKeys::handleAlbumArtReady);
connect(PlayerComponent::Get().albumArtProvider(), &AlbumArtProvider::artworkUnavailable,
this, &InputAppleMediaKeys::handleAlbumArtUnavailable);
}
if (auto lib =
dlopen("/System/Library/PrivateFrameworks/MediaRemote.framework/MediaRemote", RTLD_NOW))
{
@@ -207,3 +218,36 @@ void InputAppleMediaKeys::handleUpdateMetaData(const QVariantMap& meta)
MPNowPlayingInfoCenter.defaultCenter.nowPlayingInfo = info;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void InputAppleMediaKeys::handleAlbumArtReady(const QByteArray& imageData, const QString& mimeType)
{
// Convert QByteArray to NSData
NSData *nsImageData = [NSData dataWithBytes:imageData.constData() length:imageData.size()];
// Create NSImage from the data
NSImage *nsImage = [[NSImage alloc] initWithData:nsImageData];
if (!nsImage)
{
qDebug() << "AppleMediaKeys: Failed to create NSImage from album art data";
return;
}
// Create MPMediaItemArtwork from NSImage
MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithBoundsSize:nsImage.size
requestHandler:^NSImage * _Nonnull(CGSize size) {
return nsImage;
}];
// Update now playing info with artwork
auto info = [NSMutableDictionary
dictionaryWithDictionary:MPNowPlayingInfoCenter.defaultCenter.nowPlayingInfo];
info[MPMediaItemPropertyArtwork] = artwork;
MPNowPlayingInfoCenter.defaultCenter.nowPlayingInfo = info;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
void InputAppleMediaKeys::handleAlbumArtUnavailable()
{
// No action needed - metadata remains without artwork
}
+32 -289
View File
@@ -2,6 +2,7 @@
#include "MprisRootAdaptor.h"
#include "MprisPlayerAdaptor.h"
#include "player/PlayerComponent.h"
#include "player/AlbumArtProvider.h"
#include "input/InputComponent.h"
#include "system/SystemComponent.h"
#include "settings/SettingsComponent.h"
@@ -15,17 +16,6 @@
#include <QApplication>
#include <QDebug>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QNetworkAccessManager>
#include <QNetworkDiskCache>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QEventLoop>
#include <QTimer>
#include <QUrlQuery>
#include <QMimeDatabase>
#define MPRIS_OBJECT_PATH "/org/mpris/MediaPlayer2"
@@ -47,8 +37,6 @@ MprisComponent::MprisComponent(QObject* parent)
, m_expectedPosition(0)
, m_isNavigating(false)
, m_playerState(PlayerComponent::State::finished)
, m_albumArtManager(new QNetworkAccessManager(this))
, m_pendingArtReply(nullptr)
{
}
@@ -57,7 +45,6 @@ MprisComponent::~MprisComponent()
if (m_enabled)
{
disconnectPlayerSignals();
cleanupAlbumArt();
QDBusConnection::sessionBus().unregisterService(m_serviceName);
QDBusConnection::sessionBus().unregisterObject(MPRIS_OBJECT_PATH);
}
@@ -98,18 +85,6 @@ bool MprisComponent::componentInitialize()
m_enabled = true;
qDebug() << "MPRIS interface registered successfully";
// Set up disk cache for album art
qint64 cacheSize = SettingsComponent::Get().value(SETTINGS_SECTION_MPRIS, "albumArtCacheSize").toLongLong();
if (cacheSize > 0)
{
auto* cache = new QNetworkDiskCache(this);
QString cacheDir = ProfileManager::activeProfile().cacheDir("mpris-albumart");
cache->setCacheDirectory(cacheDir);
cache->setMaximumCacheSize(cacheSize);
m_albumArtManager->setCache(cache);
qDebug() << "MPRIS: Album art cache enabled, max size:" << cacheSize << "bytes, dir:" << cacheDir;
}
return true;
}
@@ -135,6 +110,15 @@ void MprisComponent::componentPostInitialize()
connect(m_player, &PlayerComponent::metadataChanged, this, &MprisComponent::notifyMetadata);
connect(m_player, &PlayerComponent::volumeChanged, this, &MprisComponent::notifyVolumeChange);
// Connect to AlbumArtProvider signals
if (m_player->albumArtProvider())
{
connect(m_player->albumArtProvider(), &AlbumArtProvider::artworkReady,
this, &MprisComponent::onAlbumArtReady);
connect(m_player->albumArtProvider(), &AlbumArtProvider::artworkUnavailable,
this, &MprisComponent::onAlbumArtUnavailable);
}
m_positionTimer = new QTimer(this);
m_positionTimer->setInterval(500);
connect(m_positionTimer, &QTimer::timeout, [this]() {
@@ -487,7 +471,7 @@ void MprisComponent::notifyQueueChange(bool canNext, bool canPrevious)
qDebug() << "MPRIS: Queue empty and stopped, clearing metadata";
m_metadata.clear();
m_currentTrackId.clear();
cleanupAlbumArt();
m_currentArtDataUri.clear();
properties["Metadata"] = m_metadata;
properties["Position"] = static_cast<qint64>(0);
}
@@ -552,7 +536,7 @@ void MprisComponent::notifyPlaybackState(const QString& state)
qDebug() << "MPRIS: Stopped without navigation, clearing metadata";
m_metadata.clear();
m_currentTrackId.clear();
cleanupAlbumArt();
m_currentArtDataUri.clear();
}
else
{
@@ -681,8 +665,7 @@ void MprisComponent::onPlayerFinished()
m_currentTrackId.clear();
m_canGoNext = false;
m_canGoPrevious = false;
cleanupAlbumArt();
m_currentArtDataUri.clear();
QVariantMap properties;
properties["Metadata"] = m_metadata;
@@ -705,7 +688,7 @@ void MprisComponent::onPlayerStateChanged(PlayerComponent::State newState, Playe
qDebug() << "MPRIS: Finished with no next item, clearing metadata";
m_metadata.clear();
m_currentTrackId.clear();
cleanupAlbumArt();
m_currentArtDataUri.clear();
QVariantMap properties;
properties["Metadata"] = m_metadata;
@@ -725,7 +708,7 @@ void MprisComponent::onPlayerStateChanged(PlayerComponent::State newState, Playe
m_currentTrackId.clear();
m_canGoNext = false;
m_canGoPrevious = false;
cleanupAlbumArt();
m_currentArtDataUri.clear();
QVariantMap properties;
properties["Metadata"] = m_metadata;
@@ -745,7 +728,7 @@ void MprisComponent::onPlayerStateChanged(PlayerComponent::State newState, Playe
qDebug() << "MPRIS: Clearing metadata due to error state";
m_metadata.clear();
m_currentTrackId.clear();
cleanupAlbumArt();
m_currentArtDataUri.clear();
QVariantMap properties;
properties["Metadata"] = m_metadata;
@@ -921,15 +904,10 @@ void MprisComponent::updateMetadata(const QVariantMap& jellyfinMeta, const QUrl&
mprisMeta["xesam:contentCreated"] = jellyfinMeta["ProductionYear"].toString();
}
QString artUrl = extractArtworkUrl(jellyfinMeta, baseUrl);
if (!artUrl.isEmpty())
{
QString localArtUrl = handleAlbumArt(artUrl);
if (!localArtUrl.isEmpty())
{
mprisMeta["mpris:artUrl"] = localArtUrl;
}
}
// Album art will be set asynchronously via onAlbumArtReady signal
// Keep existing data URI if available
if (!m_currentArtDataUri.isEmpty())
mprisMeta["mpris:artUrl"] = m_currentArtDataUri;
mprisMeta["mpris:length"] = QVariant::fromValue(m_duration);
@@ -1009,260 +987,25 @@ QString MprisComponent::generateTrackId() const
return QString("/org/mpris/MediaPlayer2/Track/%1").arg(++trackCounter);
}
QString MprisComponent::handleAlbumArt(const QString& artUrl)
void MprisComponent::onAlbumArtReady(const QByteArray& imageData, const QString& mimeType)
{
if (artUrl.isEmpty())
return QString();
// Create data URI from the downloaded image
QString dataUri = QString("data:%1;base64,%2")
.arg(mimeType)
.arg(QString::fromLatin1(imageData.toBase64()));
// For file:// URLs, return as-is
if (artUrl.startsWith("file://"))
m_currentArtDataUri = dataUri;
if (!m_metadata.isEmpty())
{
cleanupAlbumArt();
return artUrl;
}
// For http/https URLs, download and convert to data URI
if (artUrl.startsWith("http://") || artUrl.startsWith("https://"))
{
// If already downloading this URL, wait for it
if (m_pendingArtReply && m_pendingArtUrl == artUrl)
return m_currentArtDataUri;
// If URL hasn't changed and we have a cached data URI, return it
if (m_pendingArtUrl == artUrl && !m_currentArtDataUri.isEmpty())
return m_currentArtDataUri;
cleanupAlbumArt();
m_pendingArtUrl = artUrl;
QNetworkRequest request;
request.setUrl(QUrl(artUrl));
request.setRawHeader("User-Agent", SystemComponent::Get().getUserAgent().toUtf8());
request.setSslConfiguration(SystemComponent::Get().getSSLConfiguration());
m_pendingArtReply = m_albumArtManager->get(request);
if (SettingsComponent::Get().ignoreSSLErrors()) {
connect(m_pendingArtReply, QOverload<const QList<QSslError>&>::of(&QNetworkReply::sslErrors),
m_pendingArtReply, QOverload<>::of(&QNetworkReply::ignoreSslErrors));
}
connect(m_pendingArtReply, &QNetworkReply::finished, this, &MprisComponent::onAlbumArtDownloaded);
return QString();
}
return QString();
}
void MprisComponent::cleanupAlbumArt()
{
if (m_pendingArtReply)
{
m_pendingArtReply->abort();
m_pendingArtReply->deleteLater();
m_pendingArtReply = nullptr;
m_pendingArtUrl.clear();
}
m_currentArtDataUri.clear();
}
void MprisComponent::onAlbumArtDownloaded()
{
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
if (!reply)
return;
if (reply == m_pendingArtReply)
m_pendingArtReply = nullptr;
reply->deleteLater();
bool fromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
if (reply->error() != QNetworkReply::NoError)
{
if (reply->error() != QNetworkReply::OperationCanceledError)
qDebug() << "MPRIS: Album art download failed:" << reply->errorString();
return;
}
QByteArray imageData = reply->readAll();
if (imageData.isEmpty())
{
qDebug() << "MPRIS: Album art download returned empty data";
return;
}
static QMimeDatabase mimeDb;
QString mimeType = mimeDb.mimeTypeForData(imageData).name();
qDebug() << "MPRIS: Album art" << (fromCache ? "cache hit" : "cache miss")
<< "-" << reply->url().toString() << "-" << imageData.size() << "bytes";
if (mimeType.startsWith("image/"))
{
// Create data URI
QString dataUri = QString("data:%1;base64,%2")
.arg(mimeType)
.arg(QString::fromLatin1(imageData.toBase64()));
m_currentArtDataUri = dataUri;
if (!m_metadata.isEmpty())
{
m_metadata["mpris:artUrl"] = dataUri;
emitPropertyChange("org.mpris.MediaPlayer2.Player", "Metadata", m_metadata);
}
}
else
{
qDebug() << "MPRIS: Album art not an image type:" << mimeType;
m_metadata["mpris:artUrl"] = dataUri;
emitPropertyChange("org.mpris.MediaPlayer2.Player", "Metadata", m_metadata);
}
}
QString MprisComponent::extractArtworkUrl(const QVariantMap& metadata, const QUrl& baseUrl)
void MprisComponent::onAlbumArtUnavailable()
{
if (baseUrl.isEmpty())
return QString();
QString mediaType = metadata.value("MediaType").toString();
QString itemType = metadata.value("Type").toString();
QUrl artUrl = baseUrl;
QUrlQuery query;
if (mediaType == "Audio" || itemType == "Audio")
{
if (metadata.contains("AlbumId") && metadata.contains("AlbumPrimaryImageTag"))
{
QString albumId = metadata["AlbumId"].toString();
QString imageTag = metadata["AlbumPrimaryImageTag"].toString();
if (!albumId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(albumId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
return artUrl.toString();
}
}
auto imageTags = metadata["ImageTags"].toMap();
if (imageTags.contains("Primary") && metadata.contains("Id"))
{
QString itemId = metadata["Id"].toString();
QString imageTag = imageTags["Primary"].toString();
if (!itemId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(itemId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
return artUrl.toString();
}
}
}
else if (itemType == "Episode")
{
if (metadata.contains("SeriesId") && metadata.contains("SeriesPrimaryImageTag"))
{
QString seriesId = metadata["SeriesId"].toString();
QString imageTag = metadata["SeriesPrimaryImageTag"].toString();
if (!seriesId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(seriesId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
return artUrl.toString();
}
}
if (metadata.contains("SeasonId") && metadata.contains("SeasonPrimaryImageTag"))
{
QString seasonId = metadata["SeasonId"].toString();
QString imageTag = metadata["SeasonPrimaryImageTag"].toString();
if (!seasonId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(seasonId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
return artUrl.toString();
}
}
auto imageTags = metadata["ImageTags"].toMap();
if (imageTags.contains("Primary") && metadata.contains("Id"))
{
QString itemId = metadata["Id"].toString();
QString imageTag = imageTags["Primary"].toString();
if (!itemId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(itemId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
return artUrl.toString();
}
}
}
else if (itemType == "Movie")
{
auto imageTags = metadata["ImageTags"].toMap();
if (imageTags.contains("Primary") && metadata.contains("Id"))
{
QString itemId = metadata["Id"].toString();
QString imageTag = imageTags["Primary"].toString();
if (!itemId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(itemId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
return artUrl.toString();
}
}
if (imageTags.contains("Backdrop") && metadata.contains("Id"))
{
QString itemId = metadata["Id"].toString();
QString imageTag = imageTags["Backdrop"].toString();
if (!itemId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Backdrop/0").arg(itemId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
return artUrl.toString();
}
}
}
else
{
auto imageTags = metadata["ImageTags"].toMap();
if (imageTags.contains("Primary") && metadata.contains("Id"))
{
QString itemId = metadata["Id"].toString();
QString imageTag = imageTags["Primary"].toString();
if (!itemId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(itemId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
return artUrl.toString();
}
}
}
return QString();
// No action needed - metadata remains without artwork
}
void MprisComponent::updateNavigationCapabilities()
+2 -11
View File
@@ -11,9 +11,6 @@
#include "ComponentManager.h"
#include "player/PlayerComponent.h"
class QNetworkAccessManager;
class QNetworkReply;
#include "MprisRootAdaptor.h"
#include "MprisPlayerAdaptor.h"
@@ -105,7 +102,8 @@ private Q_SLOTS:
void onShuffleModeChanged(bool shuffleEnabled);
void onRepeatModeChanged(const QString& repeatMode);
void onAlbumArtDownloaded();
void onAlbumArtReady(const QByteArray& imageData, const QString& mimeType);
void onAlbumArtUnavailable();
Q_SIGNALS:
void propertiesChanged(const QString& interface,
@@ -120,9 +118,6 @@ private:
void connectPlayerSignals();
void disconnectPlayerSignals();
QString generateTrackId() const;
QString handleAlbumArt(const QString& artUrl);
void cleanupAlbumArt();
QString extractArtworkUrl(const QVariantMap& metadata, const QUrl& baseUrl);
void updateNavigationCapabilities();
bool m_enabled;
@@ -153,11 +148,7 @@ private:
bool m_isNavigating;
QString m_currentArtDataUri;
QString m_pendingArtUrl;
PlayerComponent::State m_playerState;
QNetworkAccessManager* m_albumArtManager;
QNetworkReply* m_pendingArtReply;
};
#endif // MPRISCOMPONENT_H
+273
View File
@@ -0,0 +1,273 @@
#include "AlbumArtProvider.h"
#include "system/SystemComponent.h"
#include "settings/SettingsComponent.h"
#include "core/ProfileManager.h"
#include <QNetworkAccessManager>
#include <QNetworkDiskCache>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrlQuery>
#include <QMimeDatabase>
#include <QDebug>
AlbumArtProvider::AlbumArtProvider(QObject* parent)
: QObject(parent)
, m_networkManager(new QNetworkAccessManager(this))
, m_pendingReply(nullptr)
{
// Set up disk cache for album art
qint64 cacheSize = SettingsComponent::Get().value("albumart", "cacheSize").toLongLong();
if (cacheSize <= 0)
cacheSize = 10485760; // 10MB default
auto* cache = new QNetworkDiskCache(this);
QString cacheDir = ProfileManager::activeProfile().cacheDir("albumart");
cache->setCacheDirectory(cacheDir);
cache->setMaximumCacheSize(cacheSize);
m_networkManager->setCache(cache);
qDebug() << "AlbumArtProvider: Cache enabled, max size:" << cacheSize << "bytes, dir:" << cacheDir;
}
AlbumArtProvider::~AlbumArtProvider()
{
cleanup();
}
void AlbumArtProvider::requestArtwork(const QVariantMap& metadata, const QUrl& baseUrl)
{
QString artUrl = extractArtworkUrl(metadata, baseUrl);
if (artUrl.isEmpty())
{
emit artworkUnavailable();
return;
}
// If already downloading this URL, wait for it
if (m_pendingReply && m_pendingUrl == artUrl)
return;
cleanup();
m_pendingUrl = artUrl;
QNetworkRequest request;
request.setUrl(QUrl(artUrl));
request.setRawHeader("User-Agent", SystemComponent::Get().getUserAgent().toUtf8());
request.setSslConfiguration(SystemComponent::Get().getSSLConfiguration());
m_pendingReply = m_networkManager->get(request);
if (SettingsComponent::Get().ignoreSSLErrors()) {
connect(m_pendingReply, QOverload<const QList<QSslError>&>::of(&QNetworkReply::sslErrors),
m_pendingReply, QOverload<>::of(&QNetworkReply::ignoreSslErrors));
}
connect(m_pendingReply, &QNetworkReply::finished, this, &AlbumArtProvider::onArtworkDownloaded);
}
void AlbumArtProvider::cancelPending()
{
cleanup();
}
void AlbumArtProvider::onArtworkDownloaded()
{
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
if (!reply)
return;
if (reply == m_pendingReply)
m_pendingReply = nullptr;
reply->deleteLater();
bool fromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
if (reply->error() != QNetworkReply::NoError)
{
if (reply->error() != QNetworkReply::OperationCanceledError)
qDebug() << "AlbumArtProvider: Download failed:" << reply->errorString();
emit artworkUnavailable();
return;
}
QByteArray imageData = reply->readAll();
if (imageData.isEmpty())
{
qDebug() << "AlbumArtProvider: Download returned empty data";
emit artworkUnavailable();
return;
}
static QMimeDatabase mimeDb;
QString mimeType = mimeDb.mimeTypeForData(imageData).name();
qDebug() << "AlbumArtProvider:" << (fromCache ? "cache hit" : "cache miss")
<< "-" << reply->url().toString() << "-" << imageData.size() << "bytes";
if (mimeType.startsWith("image/"))
{
emit artworkReady(imageData, mimeType);
}
else
{
qDebug() << "AlbumArtProvider: Not an image type:" << mimeType;
emit artworkUnavailable();
}
}
QString AlbumArtProvider::extractArtworkUrl(const QVariantMap& metadata, const QUrl& baseUrl)
{
if (baseUrl.isEmpty())
return QString();
QString mediaType = metadata.value("MediaType").toString();
QString itemType = metadata.value("Type").toString();
QUrl artUrl = baseUrl;
QUrlQuery query;
if (mediaType == "Audio" || itemType == "Audio")
{
if (metadata.contains("AlbumId") && metadata.contains("AlbumPrimaryImageTag"))
{
QString albumId = metadata["AlbumId"].toString();
QString imageTag = metadata["AlbumPrimaryImageTag"].toString();
if (!albumId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(albumId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
return artUrl.toString();
}
}
auto imageTags = metadata["ImageTags"].toMap();
if (imageTags.contains("Primary") && metadata.contains("Id"))
{
QString itemId = metadata["Id"].toString();
QString imageTag = imageTags["Primary"].toString();
if (!itemId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(itemId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
return artUrl.toString();
}
}
}
else if (itemType == "Episode")
{
if (metadata.contains("SeriesId") && metadata.contains("SeriesPrimaryImageTag"))
{
QString seriesId = metadata["SeriesId"].toString();
QString imageTag = metadata["SeriesPrimaryImageTag"].toString();
if (!seriesId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(seriesId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
return artUrl.toString();
}
}
if (metadata.contains("SeasonId") && metadata.contains("SeasonPrimaryImageTag"))
{
QString seasonId = metadata["SeasonId"].toString();
QString imageTag = metadata["SeasonPrimaryImageTag"].toString();
if (!seasonId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(seasonId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
return artUrl.toString();
}
}
auto imageTags = metadata["ImageTags"].toMap();
if (imageTags.contains("Primary") && metadata.contains("Id"))
{
QString itemId = metadata["Id"].toString();
QString imageTag = imageTags["Primary"].toString();
if (!itemId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(itemId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
return artUrl.toString();
}
}
}
else if (itemType == "Movie")
{
auto imageTags = metadata["ImageTags"].toMap();
if (imageTags.contains("Primary") && metadata.contains("Id"))
{
QString itemId = metadata["Id"].toString();
QString imageTag = imageTags["Primary"].toString();
if (!itemId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(itemId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
return artUrl.toString();
}
}
if (imageTags.contains("Backdrop") && metadata.contains("Id"))
{
QString itemId = metadata["Id"].toString();
QString imageTag = imageTags["Backdrop"].toString();
if (!itemId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Backdrop/0").arg(itemId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
return artUrl.toString();
}
}
}
else
{
auto imageTags = metadata["ImageTags"].toMap();
if (imageTags.contains("Primary") && metadata.contains("Id"))
{
QString itemId = metadata["Id"].toString();
QString imageTag = imageTags["Primary"].toString();
if (!itemId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(itemId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
return artUrl.toString();
}
}
}
return QString();
}
void AlbumArtProvider::cleanup()
{
if (m_pendingReply)
{
m_pendingReply->abort();
m_pendingReply->deleteLater();
m_pendingReply = nullptr;
m_pendingUrl.clear();
}
}
+40
View File
@@ -0,0 +1,40 @@
#ifndef ALBUMARTPROVIDER_H
#define ALBUMARTPROVIDER_H
#include <QObject>
#include <QUrl>
#include <QVariantMap>
#include <QByteArray>
#include <QString>
class QNetworkAccessManager;
class QNetworkReply;
class AlbumArtProvider : public QObject
{
Q_OBJECT
public:
explicit AlbumArtProvider(QObject* parent = nullptr);
~AlbumArtProvider() override;
void requestArtwork(const QVariantMap& metadata, const QUrl& baseUrl);
void cancelPending();
Q_SIGNALS:
void artworkReady(const QByteArray& imageData, const QString& mimeType);
void artworkUnavailable();
private Q_SLOTS:
void onArtworkDownloaded();
private:
QString extractArtworkUrl(const QVariantMap& metadata, const QUrl& baseUrl);
void cleanup();
QNetworkAccessManager* m_networkManager;
QNetworkReply* m_pendingReply;
QString m_pendingUrl;
};
#endif // ALBUMARTPROVIDER_H
+1
View File
@@ -1,4 +1,5 @@
add_sources(PlayerComponent.cpp PlayerComponent.h)
add_sources(AlbumArtProvider.cpp AlbumArtProvider.h)
add_sources(MpvVideoItem.cpp MpvVideoItem.h)
add_sources(OpenGLDetect.cpp OpenGLDetect.h)
add_sources(QtHelper.h)
+10 -2
View File
@@ -14,6 +14,7 @@
#include "settings/SettingsSection.h"
#include "MpvVideoItem.h"
#include "AlbumArtProvider.h"
#include "input/InputComponent.h"
#include <MpvController>
@@ -48,7 +49,8 @@ PlayerComponent::PlayerComponent(QObject* parent)
m_window(nullptr), m_mediaFrameRate(0),
m_restoreDisplayTimer(this), m_reloadAudioTimer(this),
m_streamSwitchImminent(false), m_doAc3Transcoding(false),
m_videoRectangle(-1, 0, 0, 0)
m_videoRectangle(-1, 0, 0, 0),
m_albumArtProvider(new AlbumArtProvider(this))
{
qmlRegisterType<MpvVideoItem>("Konvergo", 1, 0, "MpvVideo"); // deprecated name
qmlRegisterType<MpvVideoItem>("Konvergo", 1, 0, "KonvergoVideo");
@@ -334,7 +336,13 @@ void PlayerComponent::queueMedia(const QString& url, const QVariantMap& options,
m_mpv->command( command);
emit onMetaData(metadata["metadata"].toMap(), qurl.adjusted(QUrl::RemovePath | QUrl::RemoveQuery));
QVariantMap jellyfinMetadata = metadata["metadata"].toMap();
QUrl jellyfinBaseUrl = qurl.adjusted(QUrl::RemovePath | QUrl::RemoveQuery);
emit onMetaData(jellyfinMetadata, jellyfinBaseUrl);
// Request album art from the provider
if (m_albumArtProvider)
m_albumArtProvider->requestArtwork(jellyfinMetadata, jellyfinBaseUrl);
}
/////////////////////////////////////////////////////////////////////////////////////////
+5
View File
@@ -17,6 +17,7 @@
#include <mpv/client.h>
class MpvController;
class AlbumArtProvider;
///////////////////////////////////////////////////////////////////////////////////////////////////
class PlayerComponent : public ComponentBase
@@ -127,6 +128,8 @@ public:
QRect videoRectangle() { return m_videoRectangle; }
AlbumArtProvider* albumArtProvider() const { return m_albumArtProvider; }
void setMpvController(MpvController* controller) {
if (!m_mpv)
m_mpv = controller;
@@ -269,6 +272,8 @@ private:
QString m_currentWebPlaylistItemId;
QTimer* m_playlistTimer;
QVariantList m_queuedItems;
AlbumArtProvider* m_albumArtProvider;
};
#endif // PLAYERCOMPONENT_H