Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Plex Connect Locally #1313

Merged
merged 19 commits into from
Jun 9, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 2 additions & 1 deletion music_assistant/server/models/music_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,8 @@ async def sync_library(self, media_types: tuple[MediaType, ...]) -> None:
# create full db item
# note that we skip the metadata lookup purely to speed up the sync
# the additional metadata is then lazy retrieved afterwards
prov_item.favorite = True
if (self.is_streaming_provider):
lordbah marked this conversation as resolved.
Show resolved Hide resolved
prov_item.favorite = True
library_item = await controller.add_item_to_library(
prov_item, metadata_lookup=False
)
Expand Down
83 changes: 66 additions & 17 deletions music_assistant/server/providers/plex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,16 @@
CONF_LOCAL_SERVER_VERIFY_CERT = "local_server_verify_cert"
CONF_USE_GDM = "use_gdm"
CONF_ACTION_GDM = "gdm"
CONF_ACTION_CONNECT_UNAUTH = "connect_unauth"
CONF_USING_UNAUTH = "unauth"
FAKE_ARTIST_PREFIX = "_fake://"


async def setup(
mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
) -> ProviderInstanceType:
"""Initialize provider(instance) with given configuration."""
if not config.get_value(CONF_AUTH_TOKEN):
if not config.get_value(CONF_AUTH_TOKEN) and not config.get_value(CONF_USING_UNAUTH):
msg = "Invalid login credentials"
raise LoginFailed(msg)

Expand Down Expand Up @@ -125,6 +127,8 @@ async def get_config_entries(
values[CONF_LOCAL_SERVER_VERIFY_CERT] = False
# config flow auth action/step (authenticate button clicked)
if action == CONF_ACTION_AUTH:
values[CONF_USING_UNAUTH] = None
values[CONF_AUTH_TOKEN] = None
async with AuthenticationHelper(mass, values["session_id"]) as auth_helper:
plex_auth = MyPlexPinLogin(headers={"X-Plex-Product": "Music Assistant"}, oauth=True)
auth_url = plex_auth.oauthUrl(auth_helper.callback_url)
Expand All @@ -134,6 +138,9 @@ async def get_config_entries(
raise LoginFailed(msg)
# set the retrieved token on the values object to pass along
values[CONF_AUTH_TOKEN] = plex_auth.token
if action == CONF_ACTION_CONNECT_UNAUTH:
values[CONF_USING_UNAUTH] = "trying"
#values[CONF_AUTH_TOKEN] = "dummy"

# config flow auth action/step to pick the library to use
# because this call is very slow, we only show/calculate the dropdown if we do
Expand All @@ -144,11 +151,12 @@ async def get_config_entries(
label="Library",
required=True,
description="The library to connect to (e.g. Music)",
depends_on=CONF_AUTH_TOKEN,
# We don't need a token if connecting locally
#depends_on=CONF_AUTH_TOKEN,
action=CONF_ACTION_LIBRARY,
action_label="Select Plex Music Library",
)
if action in (CONF_ACTION_LIBRARY, CONF_ACTION_AUTH):
if action in (CONF_ACTION_LIBRARY, CONF_ACTION_AUTH, CONF_ACTION_CONNECT_UNAUTH):
token = mass.config.decrypt_string(values.get(CONF_AUTH_TOKEN))
server_http_ip = values.get(CONF_LOCAL_SERVER_IP)
server_http_port = values.get(CONF_LOCAL_SERVER_PORT)
Expand Down Expand Up @@ -221,6 +229,28 @@ async def get_config_entries(
action=CONF_ACTION_AUTH,
action_label="Authenticate on MyPlex.tv",
value=values.get(CONF_AUTH_TOKEN) if values else None,
required=values.get(CONF_USING_UNAUTH) is None
),
ConfigEntry(
key=CONF_ACTION_CONNECT_UNAUTH,
type=ConfigEntryType.ACTION,
label="Connect locally",
description="Connect locally without a MyPlex account.",
action=CONF_ACTION_CONNECT_UNAUTH,
action_label="Connect locally",
required=False
),
# This is just a flag for this file to know whether or not we're connecting locally.
# It doesn't appear on the GUI.
# I couldn't figure out how to make a class var work in the places it's needed.
ConfigEntry(
key=CONF_USING_UNAUTH,
type=ConfigEntryType.STRING,
label="nolabel",
description="",
value=values.get(CONF_USING_UNAUTH) if values else None,
hidden=True,
required=False
),
conf_libraries,
)
Expand All @@ -232,6 +262,7 @@ class PlexProvider(MusicProvider):
_plex_server: PlexServer = None
_plex_library: PlexMusicSection = None
_myplex_account: MyPlexAccount = None
_baseURL: str = None

async def handle_async_init(self) -> None:
"""Set up the music provider by connecting to the server."""
Expand All @@ -250,11 +281,21 @@ def connect() -> PlexServer:
local_server_protocol = (
"https" if self.config.get_value(CONF_LOCAL_SERVER_SSL) else "http"
)
plex_server = PlexServer(
f"{local_server_protocol}://{self.config.get_value(CONF_LOCAL_SERVER_IP)}:{self.config.get_value(CONF_LOCAL_SERVER_PORT)}",
token=self.config.get_value(CONF_AUTH_TOKEN),
session=session,
)
token=self.config.get_value(CONF_AUTH_TOKEN)
if not self.config.get_value(CONF_USING_UNAUTH) is None:
# Doing local connection, not via plex.tv.
plex_server = PlexServer(
f"{local_server_protocol}://{self.config.get_value(CONF_LOCAL_SERVER_IP)}:{self.config.get_value(CONF_LOCAL_SERVER_PORT)}"
)
# I don't think PlexAPI intends for this to be accessible, but we need it.
self._baseURL = plex_server._baseurl
else:
plex_server = PlexServer(
f"{local_server_protocol}://{self.config.get_value(CONF_LOCAL_SERVER_IP)}:{self.config.get_value(CONF_LOCAL_SERVER_PORT)}",
token,
session=session,
)

except plexapi.exceptions.BadRequest as err:
if "Invalid token" in str(err):
# token invalid, invalidate the config
Expand Down Expand Up @@ -413,10 +454,14 @@ async def _parse_album(self, plex_album: PlexAlbum) -> Album:
item_id=str(album_id),
provider_domain=self.domain,
provider_instance=self.instance_id,
url=plex_album.getWebURL(),
url=plex_album.getWebURL(self._baseURL),
)
},
)
# Only add 5-star rated albums to Favorites. rating will be 10.0 for those.
#TODO: Let user set threshold?
#album.favorite = True if plex_album._data.attrib['rating'] == "10.0" else False

if plex_album.year:
album.year = plex_album.year
if thumb := plex_album.firstAttr("thumb", "parentThumb", "grandparentThumb"):
Expand Down Expand Up @@ -455,7 +500,7 @@ async def _parse_artist(self, plex_artist: PlexArtist) -> Artist:
item_id=str(artist_id),
provider_domain=self.domain,
provider_instance=self.instance_id,
url=plex_artist.getWebURL(),
url=plex_artist.getWebURL(self._baseURL),
)
},
)
Expand Down Expand Up @@ -483,7 +528,7 @@ async def _parse_playlist(self, plex_playlist: PlexPlaylist) -> Playlist:
item_id=plex_playlist.key,
provider_domain=self.domain,
provider_instance=self.instance_id,
url=plex_playlist.getWebURL(),
url=plex_playlist.getWebURL(self._baseURL),
)
},
)
Expand Down Expand Up @@ -526,11 +571,14 @@ async def _parse_track(self, plex_track: PlexTrack) -> Track:
ContentType.try_parse(content) if content else ContentType.UNKNOWN
),
),
url=plex_track.getWebURL(),
url=plex_track.getWebURL(self._baseURL),
)
},
)

# Only add 5-star rated tracks to Favorites. userRating will be 10.0 for those.
#TODO: Let user set threshold?
track.favorite = True if plex_track._data.attrib['userRating'] == "10.0" else False

if plex_track.originalTitle and plex_track.originalTitle != plex_track.grandparentTitle:
# The artist of the track if different from the album's artist.
# For this kind of artist, we just know the name, so we create a fake artist,
Expand Down Expand Up @@ -796,10 +844,11 @@ async def get_myplex_account_and_refresh_token(self, auth_token: str) -> MyPlexA
"""Get a MyPlexAccount object and refresh the token if needed."""

def _refresh_plex_token():
if self._myplex_account is None:
myplex_account = MyPlexAccount(token=auth_token)
self._myplex_account = myplex_account
self._myplex_account.ping()
if self.config.values.get(CONF_USING_UNAUTH) is None:
if self._myplex_account is None:
myplex_account = MyPlexAccount(token=auth_token)
self._myplex_account = myplex_account
self._myplex_account.ping()
return self._myplex_account

return await asyncio.to_thread(_refresh_plex_token)
15 changes: 10 additions & 5 deletions music_assistant/server/providers/plex/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,16 @@ def _get_libraries():
session = requests.Session()
session.verify = local_server_verify_cert
local_server_protocol = "https" if local_server_ssl else "http"
plex_server: PlexServer = PlexServer(
f"{local_server_protocol}://{local_server_ip}:{local_server_port}",
auth_token,
session=session,
)
if auth_token is None:
plex_server: PlexServer = PlexServer(
f"{local_server_protocol}://{local_server_ip}:{local_server_port}"
)
else:
plex_server: PlexServer = PlexServer(
f"{local_server_protocol}://{local_server_ip}:{local_server_port}",
auth_token,
session=session,
)
for media_section in plex_server.library.sections():
media_section: PlexLibrarySection
if media_section.type != PlexMusicSection.TYPE:
Expand Down