mirror of
https://github.com/jellyfin/jellyfin-desktop.git
synced 2026-10-26 16:45:24 +00:00
Set player artwork on macos
This commit is contained in:
1 parent
8a0091cc24
commit
d3858e2c21
10 files changed
+412
-306
No files matched your search
@@ -656,13 +656,12 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "mpris",
|
||||
"section": "albumart",
|
||||
"hidden": true,
|
||||
"platforms": [ "linux" ],
|
||||
"values": [
|
||||
{
|
||||
"value": "albumArtCacheSize",
|
||||
"default": 52428800
|
||||
"value": "cacheSize",
|
||||
"default": 10485760
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,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)
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user