Skip to content
Open
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
19 changes: 14 additions & 5 deletions music_assistant/providers/plex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@
ERR_NO_LIBRARIES,
ERR_TRACK_NOT_FOUND,
FAKE_ARTIST_PREFIX,
PLEX_PRODUCT,
)
from music_assistant.providers.plex.helpers import (
configure_plex_identity,
discover_local_servers,
get_favorite_from_rating,
get_libraries,
Expand Down Expand Up @@ -133,6 +135,7 @@ async def setup(
if not config.get_value(CONF_AUTH_TOKEN):
raise LoginFailed(ERR_INVALID_CREDENTIALS)

configure_plex_identity(mass.server_id)
return PlexProvider(mass, manifest, config, SUPPORTED_FEATURES)


Expand All @@ -149,6 +152,10 @@ async def get_config_entries( # noqa: PLR0915
action: [optional] action key called from config entries UI.
values: the (intermediate) raw values for config entries sent with the action.
"""
# ensure plexapi announces "Music Assistant" with a stable client identifier before
# any auth/connection happens (so OAuth tokens stay valid across restarts)
configure_plex_identity(mass.server_id)
Comment thread
anatosun marked this conversation as resolved.

# handle action GDM discovery
if action == CONF_ACTION_GDM:
server_details = await discover_local_servers()
Expand Down Expand Up @@ -179,7 +186,7 @@ async def get_config_entries( # noqa: PLR0915
assert values
values[CONF_AUTH_TOKEN] = None
async with AuthenticationHelper(mass, str(values["session_id"])) as auth_helper:
plex_auth = MyPlexPinLogin(headers={"X-Plex-Product": "Music Assistant"}, oauth=True)
plex_auth = MyPlexPinLogin(oauth=True)
# Generate the PIN/code by calling the Plex API
await asyncio.to_thread(plex_auth._getCode)
auth_url = plex_auth.oauthUrl(auth_helper.callback_url)
Expand Down Expand Up @@ -417,12 +424,14 @@ def connect() -> PlexServer:
if self.config.get_value(CONF_LOCAL_SERVER_SSL)
else False
)
# Add Music Assistant client identification headers
# Add Music Assistant client identification headers. The client identifier
# is announced globally via configure_plex_identity() (plexapi rebuilds it
# from BASE_HEADERS per request, overriding any session-level value), so we
# only set the per-connection product/platform/version here.
session.headers.update(
{
"X-Plex-Client-Identifier": self.instance_id,
"X-Plex-Product": "Music Assistant",
"X-Plex-Platform": "Music Assistant",
"X-Plex-Product": PLEX_PRODUCT,
"X-Plex-Platform": PLEX_PRODUCT,
"X-Plex-Version": self.mass.version,
Comment thread
anatosun marked this conversation as resolved.
}
)
Expand Down
6 changes: 6 additions & 0 deletions music_assistant/providers/plex/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

from __future__ import annotations

from typing import Final

# Identity announced to Plex / plex.tv. Without this, plexapi falls back to the host
# machine's name (e.g. in the plex.tv "authorized devices" list and during auth).
PLEX_PRODUCT: Final = "Music Assistant"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better to just use the existing APPLICATION_NAME constant from MA constants.py?


CONF_ACTION_AUTH_MYPLEX = "auth_myplex"
CONF_ACTION_AUTH_LOCAL = "auth_local"
CONF_ACTION_CLEAR_AUTH = "auth"
Expand Down
24 changes: 23 additions & 1 deletion music_assistant/providers/plex/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import asyncio
from typing import TYPE_CHECKING, cast

import plexapi
import requests
from music_assistant_models.enums import ImageType
from music_assistant_models.media_items import MediaItemImage, UniqueList
Expand All @@ -13,14 +14,35 @@
from plexapi.library import MusicSection as PlexMusicSection
from plexapi.server import PlexServer

from music_assistant.providers.plex.constants import AUTH_TOKEN_UNAUTH
from music_assistant.providers.plex.constants import AUTH_TOKEN_UNAUTH, PLEX_PRODUCT

if TYPE_CHECKING:
from plexapi.base import PlexObject

from music_assistant.mass import MusicAssistant


def configure_plex_identity(client_id: str) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the identity is set in plexapi's process-global state and always with mass.server_id, every Plex provider instance now advertises the same X-Plex-Client-Identifier. Is that OK with multiple instances?

"""
Make every plexapi client announce "Music Assistant" with a stable identity.

plexapi builds each request's headers from these process-global defaults. The device
name otherwise falls back to the machine's hostname, and - critically - the client
identifier defaults to the MAC address, which is unstable in containers. Plex binds
OAuth tokens to the client identifier, so an unstable one makes plex.tv reject the
stored token after a restart. We pin it to Music Assistant's persistent server id.
Comment thread
OzGav marked this conversation as resolved.

:param client_id: Stable client identifier to advertise (Music Assistant's server id).
"""
plexapi.X_PLEX_PRODUCT = PLEX_PRODUCT
plexapi.X_PLEX_DEVICE_NAME = PLEX_PRODUCT
plexapi.BASE_HEADERS["X-Plex-Product"] = PLEX_PRODUCT
plexapi.BASE_HEADERS["X-Plex-Device-Name"] = PLEX_PRODUCT
if client_id:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this ever be false?

plexapi.X_PLEX_IDENTIFIER = client_id
plexapi.BASE_HEADERS["X-Plex-Client-Identifier"] = client_id


async def get_libraries(
mass: MusicAssistant,
auth_token: str | None,
Expand Down