Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bundle/win/JellyfinDesktop.iss.in
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ Source: "{#DeployPath}/redist/*.dll"; DestDir: "{app}"; Flags: ignoreversion; Ch
Source: "{#DeployPath}/vc_redist.x64.exe"; DestDir: "{tmp}"; Flags: ignoreversion deleteafterinstall; Check: ShouldExtractVCRedist

[Icons]
Name: "{autoprograms}\Jellyfin Desktop"; Filename: "{app}\{#MyAppExeName}"; Comment: "Jellyfin desktop client"; WorkingDir: "{app}"
Name: "{autodesktop}\Jellyfin Desktop"; Filename: "{app}\{#MyAppExeName}"; Comment: "Jellyfin desktop client"; WorkingDir: "{app}"; Tasks: desktopicon
Name: "{autoprograms}\Jellyfin Desktop"; Filename: "{app}\{#MyAppExeName}"; Comment: "Jellyfin desktop client"; WorkingDir: "{app}"; AppUserModelID: "org.jellyfin.JellyfinDesktop"
Name: "{autodesktop}\Jellyfin Desktop"; Filename: "{app}\{#MyAppExeName}"; Comment: "Jellyfin desktop client"; WorkingDir: "{app}"; Tasks: desktopicon; AppUserModelID: "org.jellyfin.JellyfinDesktop"

[Run]
; Install VCRedist if downloaded
Expand Down
42 changes: 41 additions & 1 deletion native/inputPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,47 @@ class inputPlugin {

const state = playbackManager.getPlayerState();
if (state && state.NowPlayingItem) {
api.player.notifyMetadata(state.NowPlayingItem);
let apiToken = '';
try {
// Attempt to get API token.
if (window.ApiClient) {
if (typeof window.ApiClient.accessToken === 'function') {
apiToken = window.ApiClient.accessToken() || '';
} else if (window.ApiClient.accessToken) {
apiToken = window.ApiClient.accessToken || '';
} else if (window.ApiClient._serverInfo?.AccessToken) {
apiToken = window.ApiClient._serverInfo.AccessToken;
}
}
} catch (e) {
console.error('inputPlugin: failed to get API token:', e);
}

// Get server URL
let serverUrl = '';
try {
if (window.ApiClient && typeof window.ApiClient.serverAddress === 'function') {
serverUrl = window.ApiClient.serverAddress() || '';
}
} catch (e) {
console.error('inputPlugin: failed to get serverAddress:', e);
}
if (!serverUrl) {
// Possible backup solution.
serverUrl = window.location.origin;
}
const metadata = Object.assign({}, state.NowPlayingItem, {
_serverUrl: serverUrl,
_apiToken: apiToken
});
try {
if (typeof api.player.notifyServerUrl === 'function') {
api.player.notifyServerUrl(serverUrl);
}
} catch (e) {
console.error('inputPlugin: notifyServerUrl failed:', e);
}
api.player.notifyMetadata(metadata);

const initialPos = playbackManager.currentTime();
if (initialPos !== undefined && initialPos !== null) {
Expand Down
3 changes: 3 additions & 0 deletions src/input/apple/InputAppleMediaKeys.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ private slots:
GetLocalOriginFunc GetLocalOrigin;
SetCanBeNowPlayingApplicationFunc SetCanBeNowPlayingApplication;

void ensureNowPlayingVisibility();

bool m_pendingUpdate;
quint64 m_currentTime;
PlayerComponent::State m_currentState = PlayerComponent::State::finished;
};

#endif //KONVERGO_INPUTAPPLEMEDIAKEYS_H
37 changes: 29 additions & 8 deletions src/input/apple/InputAppleMediaKeys.mm
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ -(MPRemoteCommandHandlerStatus)gotPlaybackPosition:(MPChangePlaybackPositionComm
{
m_currentTime = 0;
m_pendingUpdate = false;

m_delegate = [[MediaKeysDelegate alloc] initWithInput:this];
connect(&PlayerComponent::Get(), &PlayerComponent::stateChanged, this,
&InputAppleMediaKeys::handleStateChanged);
Expand All @@ -99,6 +100,8 @@ -(MPRemoteCommandHandlerStatus)gotPlaybackPosition:(MPChangePlaybackPositionComm
&InputAppleMediaKeys::handleUpdateDuration);
connect(&PlayerComponent::Get(), &PlayerComponent::onMetaData, this,
&InputAppleMediaKeys::handleUpdateMetaData);
connect(&PlayerComponent::Get(), &PlayerComponent::metadataChanged, this,
&InputAppleMediaKeys::handleUpdateMetaData);

// Connect to AlbumArtProvider signals
if (PlayerComponent::Get().albumArtProvider())
Expand Down Expand Up @@ -145,19 +148,15 @@ static MPNowPlayingPlaybackState convertState(PlayerComponent::State newState)
///////////////////////////////////////////////////////////////////////////////////////////////////
void InputAppleMediaKeys::handleStateChanged(PlayerComponent::State newState)
{
MPNowPlayingPlaybackState newMPState = convertState(newState);
m_currentState = newState;

MPNowPlayingInfoCenter *center = [MPNowPlayingInfoCenter defaultCenter];
NSMutableDictionary *playingInfo = [NSMutableDictionary dictionaryWithDictionary:center.nowPlayingInfo];
[playingInfo setObject:[NSNumber numberWithDouble:static_cast<double>(m_currentTime) / 1000] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
center.nowPlayingInfo = playingInfo;
[MPNowPlayingInfoCenter defaultCenter].playbackState = newMPState;
if (SetNowPlayingVisibility && GetLocalOrigin) {
if (newState == PlayerComponent::State::finished || newState == PlayerComponent::State::canceled || newState == PlayerComponent::State::error)
SetNowPlayingVisibility(GetLocalOrigin(), MRNowPlayingClientVisibilityNeverVisible);
else if (newState == PlayerComponent::State::paused || newState == PlayerComponent::State::playing || newState == PlayerComponent::State::buffering)
SetNowPlayingVisibility(GetLocalOrigin(), MRNowPlayingClientVisibilityAlwaysVisible);
}
center.playbackState = convertState(newState);

ensureNowPlayingVisibility();
m_pendingUpdate = true;
}

Expand Down Expand Up @@ -217,6 +216,10 @@ static MPNowPlayingPlaybackState convertState(PlayerComponent::State newState)
}

MPNowPlayingInfoCenter.defaultCenter.nowPlayingInfo = info;
MPNowPlayingInfoCenter.defaultCenter.playbackState = convertState(m_currentState);

// This will attempt to update the MacOS "Now Playing" UI.
ensureNowPlayingVisibility();
}

///////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -251,3 +254,21 @@ static MPNowPlayingPlaybackState convertState(PlayerComponent::State newState)
{
// No action needed - metadata remains without artwork
}

///////////////////////////////////////////////////////////////////////////////////////////////////
void InputAppleMediaKeys::ensureNowPlayingVisibility()
{
if (!SetNowPlayingVisibility || !GetLocalOrigin) {
return;
}

if (m_currentState == PlayerComponent::State::finished ||
m_currentState == PlayerComponent::State::canceled ||
m_currentState == PlayerComponent::State::error) {
SetNowPlayingVisibility(GetLocalOrigin(), MRNowPlayingClientVisibilityNeverVisible);
} else if (m_currentState == PlayerComponent::State::paused ||
m_currentState == PlayerComponent::State::playing ||
m_currentState == PlayerComponent::State::buffering) {
SetNowPlayingVisibility(GetLocalOrigin(), MRNowPlayingClientVisibilityAlwaysVisible);
}
}
8 changes: 6 additions & 2 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@
#include "SignalManager.h"
#endif

#if defined(Q_OS_WIN) && defined(_M_X64)
#if defined(Q_OS_WIN)
#include <windows.h>
#include <shobjidl.h>
#include <cstdio>
#endif

#if defined(Q_OS_WIN) && defined(_M_X64)
/////////////////////////////////////////////////////////////////////////////////////////
// Check AVX2 support and swap libmpv DLL if needed (before delay-load triggers)
// Only relevant for x64 - ARM64 doesn't have AVX2
Expand Down Expand Up @@ -441,7 +444,8 @@ int main(int argc, char *argv[])
QtWebEngineQuick::initialize();
QApplication app(newArgc, newArgv);

#if defined(Q_OS_WIN)
#if defined(Q_OS_WIN)
SetCurrentProcessExplicitAppUserModelID(L"org.jellyfin.JellyfinDesktop");
// Setting window icon on OSX will break user ability to change it
app.setWindowIcon(QIcon(":/images/icon.png"));
#endif
Expand Down
31 changes: 23 additions & 8 deletions src/player/AlbumArtProvider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ void AlbumArtProvider::requestArtwork(const QVariantMap& metadata, const QUrl& b
return;
}

// Append api_key query parameter for authentication (same method Jellyfin uses for media streams)
if (!m_apiToken.isEmpty())
{
QUrl url(artUrl);
QUrlQuery query(url);
query.addQueryItem("api_key", m_apiToken);
url.setQuery(query);
artUrl = url.toString();
}

// If already downloading this URL, wait for it
if (m_pendingReply && m_pendingUrl == artUrl)
return;
Expand Down Expand Up @@ -122,6 +132,11 @@ QString AlbumArtProvider::extractArtworkUrl(const QVariantMap& metadata, const Q
QString mediaType = metadata.value("MediaType").toString();
QString itemType = metadata.value("Type").toString();

// Preserve the base path (e.g. "/stable" for servers behind a reverse proxy subpath)
QString basePath = baseUrl.path();
if (basePath.endsWith('/'))
basePath.chop(1);

QUrl artUrl = baseUrl;
QUrlQuery query;

Expand All @@ -134,7 +149,7 @@ QString AlbumArtProvider::extractArtworkUrl(const QVariantMap& metadata, const Q

if (!albumId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(albumId));
artUrl.setPath(basePath + QString("/Items/%1/Images/Primary").arg(albumId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
Expand All @@ -150,7 +165,7 @@ QString AlbumArtProvider::extractArtworkUrl(const QVariantMap& metadata, const Q

if (!itemId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(itemId));
artUrl.setPath(basePath + QString("/Items/%1/Images/Primary").arg(itemId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
Expand All @@ -167,7 +182,7 @@ QString AlbumArtProvider::extractArtworkUrl(const QVariantMap& metadata, const Q

if (!seriesId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(seriesId));
artUrl.setPath(basePath + QString("/Items/%1/Images/Primary").arg(seriesId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
Expand All @@ -182,7 +197,7 @@ QString AlbumArtProvider::extractArtworkUrl(const QVariantMap& metadata, const Q

if (!seasonId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(seasonId));
artUrl.setPath(basePath + QString("/Items/%1/Images/Primary").arg(seasonId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
Expand All @@ -198,7 +213,7 @@ QString AlbumArtProvider::extractArtworkUrl(const QVariantMap& metadata, const Q

if (!itemId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(itemId));
artUrl.setPath(basePath + QString("/Items/%1/Images/Primary").arg(itemId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
Expand All @@ -216,7 +231,7 @@ QString AlbumArtProvider::extractArtworkUrl(const QVariantMap& metadata, const Q

if (!itemId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(itemId));
artUrl.setPath(basePath + QString("/Items/%1/Images/Primary").arg(itemId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
Expand All @@ -231,7 +246,7 @@ QString AlbumArtProvider::extractArtworkUrl(const QVariantMap& metadata, const Q

if (!itemId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Backdrop/0").arg(itemId));
artUrl.setPath(basePath + QString("/Items/%1/Images/Backdrop/0").arg(itemId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
Expand All @@ -249,7 +264,7 @@ QString AlbumArtProvider::extractArtworkUrl(const QVariantMap& metadata, const Q

if (!itemId.isEmpty() && !imageTag.isEmpty())
{
artUrl.setPath(QString("/Items/%1/Images/Primary").arg(itemId));
artUrl.setPath(basePath + QString("/Items/%1/Images/Primary").arg(itemId));
query.addQueryItem("tag", imageTag);
query.addQueryItem("maxWidth", "512");
artUrl.setQuery(query);
Expand Down
2 changes: 2 additions & 0 deletions src/player/AlbumArtProvider.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class AlbumArtProvider : public QObject
~AlbumArtProvider() override;

void requestArtwork(const QVariantMap& metadata, const QUrl& baseUrl);
void setApiToken(const QString& token) { m_apiToken = token; }
void cancelPending();

Q_SIGNALS:
Expand All @@ -35,6 +36,7 @@ private Q_SLOTS:
QNetworkAccessManager* m_networkManager;
QNetworkReply* m_pendingReply;
QString m_pendingUrl;
QString m_apiToken;
};

#endif // ALBUMARTPROVIDER_H
29 changes: 29 additions & 0 deletions src/player/PlayerComponent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,10 @@ void PlayerComponent::queueMedia(const QString& url, const QVariantMap& options,
QUrl jellyfinBaseUrl = qurl.adjusted(QUrl::RemovePath | QUrl::RemoveQuery);
emit onMetaData(jellyfinMetadata, jellyfinBaseUrl);

// Store the server base URL for use by notifyMetadata (JS path)
if (m_serverBaseUrl.isEmpty())
m_serverBaseUrl = jellyfinBaseUrl;

// Request album art from the provider
if (m_albumArtProvider)
m_albumArtProvider->requestArtwork(jellyfinMetadata, jellyfinBaseUrl);
Expand Down Expand Up @@ -800,7 +804,32 @@ void PlayerComponent::notifySeek(qint64 positionMs)
///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::notifyMetadata(const QVariantMap& metadata)
{
// Extract server URL embedded by JavaScript as a fallback
if (metadata.contains("_serverUrl"))
{
QUrl url(metadata["_serverUrl"].toString());
if (!url.isEmpty())
m_serverBaseUrl = url;
}

// Extract API token embedded by JavaScript for authenticated artwork requests
if (metadata.contains("_apiToken"))
{
QString token = metadata["_apiToken"].toString();
if (!token.isEmpty() && m_albumArtProvider)
m_albumArtProvider->setApiToken(token);
}

emit metadataChanged(metadata);

if (m_albumArtProvider && !m_serverBaseUrl.isEmpty())
m_albumArtProvider->requestArtwork(metadata, m_serverBaseUrl);
}

///////////////////////////////////////////////////////////////////////////////////////////////////
void PlayerComponent::notifyServerUrl(const QString& url)
{
m_serverBaseUrl = QUrl(url);
}

///////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down
2 changes: 2 additions & 0 deletions src/player/PlayerComponent.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class PlayerComponent : public ComponentBase
Q_INVOKABLE void notifyPosition(qint64 positionMs);
Q_INVOKABLE void notifySeek(qint64 positionMs);
Q_INVOKABLE void notifyMetadata(const QVariantMap& metadata);
Q_INVOKABLE void notifyServerUrl(const QString& url);
Q_INVOKABLE void notifyVolumeChange(double volume);

// 0-100 volume 0=mute and 100=normal
Expand Down Expand Up @@ -277,6 +278,7 @@ private Q_SLOTS:
QTimer* m_playlistTimer;
QVariantList m_queuedItems;

QUrl m_serverBaseUrl;
AlbumArtProvider* m_albumArtProvider;
};

Expand Down
Loading