Skip to content

Commit 62cdb20

Browse files
authored
Merge pull request #1565 from glensc/plexid
2 parents 2987868 + a3ecc41 commit 62cdb20

File tree

10 files changed

+191
-6
lines changed

10 files changed

+191
-6
lines changed

plextraktsync/commands/download.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import TYPE_CHECKING
66

77
from plextraktsync.factory import factory
8-
from plextraktsync.util.expand_id import expand_id
8+
from plextraktsync.util.expand_id import expand_plexid
99

1010
if TYPE_CHECKING:
1111
from plextraktsync.plex.PlexApi import PlexApi
@@ -60,10 +60,12 @@ def download(input: list[str], only_subs: bool, target: str):
6060

6161
# Expand ~ as HOME
6262
savepath = Path(target).expanduser()
63-
for id in expand_id(input):
64-
pm = plex.fetch_item(id)
63+
for plex_id in expand_plexid(input):
64+
if plex_id.server:
65+
plex = factory.get_plex_by_id(plex_id.server)
66+
pm = plex.fetch_item(plex_id.key)
6567
if not pm:
66-
print(f"Not found: {id}. Skipping")
68+
print(f"Not found: {plex_id} from {plex}. Skipping")
6769
continue
6870

6971
if not only_subs:

plextraktsync/commands/plex_login.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ def login(username: str, password: str):
220220

221221
sc = ServerConfig()
222222
sc.add_server(
223+
id=plex.machineIdentifier,
223224
name=server.name,
224225
token=token,
225226
urls=server_urls(server),

plextraktsync/config/PlexServerConfig.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class PlexServerConfig:
1212
name: str
1313
token: str
1414
urls: list[str]
15+
# The machineIdentifier value of this server
16+
id: str = None
1517

1618
def asdict(self):
1719
data = asdict(self)

plextraktsync/config/ServerConfig.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ def get_server(self, name: str):
2020
except KeyError:
2121
raise RuntimeError(f"Server with name {name} is not defined")
2222

23+
def server_by_id(self, id: str):
24+
for name, server in self.servers.items():
25+
if "id" in server and id == server["id"]:
26+
return self.get_server(name)
27+
2328
def load(self):
2429
if self.loaded:
2530
return self

plextraktsync/plex/PlexApi.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ class PlexApi:
3232
def __init__(self, plex: PlexServer):
3333
self.plex = plex
3434

35+
def __str__(self):
36+
return str(self.plex)
37+
3538
def plex_base_url(self, section="server"):
3639
return f"https://app.plex.tv/desktop/#!/{section}/{self.plex.machineIdentifier}"
3740

plextraktsync/plex/PlexId.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
5+
6+
@dataclass(unsafe_hash=True)
7+
class PlexId:
8+
key: int | str
9+
media_type: str = None
10+
provider: str = None
11+
server: str = None
12+
13+
METADATA = "metadata.provider.plex.tv"
14+
METADATA_URL = "https://metadata.provider.plex.tv/library/metadata"
15+
16+
@property
17+
def metadata_url(self):
18+
return f"{self.METADATA_URL}/{self.key}"
19+
20+
@property
21+
def is_discover(self):
22+
return self.provider == self.METADATA
23+
24+
def __repr__(self):
25+
keys = [self.__class__.__name__, self.server, self.provider, self.key]
26+
fields = map(str, filter(None, keys))
27+
28+
return f'<{":".join(fields)}>'
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from __future__ import annotations
2+
3+
from urllib.parse import parse_qs, urlparse
4+
5+
from .PlexId import PlexId
6+
7+
8+
class PlexIdFactory:
9+
@classmethod
10+
def create(cls, key: str | int):
11+
if isinstance(key, int) or key.isnumeric():
12+
return PlexId(int(key))
13+
elif key.startswith("https:") or key.startswith("http:"):
14+
return cls.from_url(key)
15+
elif key.startswith("plex://"):
16+
return cls.from_plex_guid(key)
17+
18+
raise RuntimeError(f"Unable to create PlexId: {key}")
19+
20+
@classmethod
21+
def from_plex_guid(cls, id):
22+
key = id.rsplit('/', 1)[-1]
23+
return PlexId(key, provider=PlexId.METADATA)
24+
25+
@staticmethod
26+
def from_url(url: str):
27+
"""
28+
Extracts id from urls like:
29+
https://app.plex.tv/desktop/#!/server/abcdefg/details?key=%2Flibrary%2Fmetadata%2F13202
30+
https://app.plex.tv/desktop/#!/server/abcdefg/playHistory?filters=metadataItemID%3D6041&filterTitle=&isParentType=false
31+
https://app.plex.tv/desktop/#!/provider/tv.plex.provider.discover/details?key=%2Flibrary%2Fmetadata%2F5d7768532e80df001ebe18e7
32+
https://app.plex.tv/desktop/#!/provider/tv.plex.provider.discover/details?key=/library/metadata/5d776a8e51dd69001fe24eb8'
33+
"""
34+
35+
result = urlparse(url)
36+
if result.fragment[0] != "!":
37+
raise RuntimeError(f"Unable to parse: {url}")
38+
39+
fragment = urlparse(result.fragment)
40+
parsed = parse_qs(fragment.query)
41+
42+
if fragment.path.startswith('!/server/'):
43+
server = fragment.path.split('/')[2]
44+
else:
45+
server = None
46+
47+
if "key" in parsed:
48+
key = ",".join(parsed["key"])
49+
if key.startswith("/library/metadata/"):
50+
id = key[len("/library/metadata/"):]
51+
if fragment.path == '!/provider/tv.plex.provider.discover/details':
52+
return PlexId(id, provider=PlexId.METADATA)
53+
return PlexId(int(id), server=server)
54+
55+
if "filters" in parsed:
56+
filters = parse_qs(parsed["filters"][0])
57+
if "metadataItemID" in filters:
58+
return PlexId(int(filters["metadataItemID"][0]), server=server)
59+
60+
raise RuntimeError(f"Unable to parse: {url}")

plextraktsync/util/Factory.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ def media_factory(self):
5858

5959
return mf
6060

61+
def get_plex_by_id(self, server_id: str):
62+
server_config = self.server_config_factory.server_by_id(server_id)
63+
if server_config is not None and server_config is not self.server_config:
64+
self.invalidate(["plex_api", "plex_server", "server_config"])
65+
self.run_config.server = server_config.name
66+
67+
return self.plex_api
68+
6169
@cached_property
6270
def plex_server(self):
6371
from plextraktsync.factory import factory
@@ -79,12 +87,17 @@ def has_plex_token(self):
7987
return False
8088

8189
@cached_property
82-
def server_config(self):
90+
def server_config_factory(self):
8391
from plextraktsync.config.ServerConfig import ServerConfig
8492

93+
return ServerConfig()
94+
95+
@cached_property
96+
def server_config(self):
97+
8598
config = self.config
8699
run_config = self.run_config
87-
server_config = ServerConfig()
100+
server_config = self.server_config_factory
88101
server_name = run_config.server
89102

90103
if server_name is None:

plextraktsync/util/expand_id.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ def plex_id(id):
3232
return f"https://metadata.provider.plex.tv/library/metadata/{key}"
3333

3434

35+
def expand_plexid(input):
36+
from plextraktsync.plex.PlexIdFactory import PlexIdFactory
37+
38+
for id in input:
39+
yield PlexIdFactory.create(id)
40+
41+
3542
def expand_id(input):
3643
"""
3744
Takes list of id or urls and resolves to Plex media id.

tests/test_plex_id.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env python3 -m pytest
2+
from plextraktsync.plex.PlexId import PlexId
3+
from plextraktsync.plex.PlexIdFactory import PlexIdFactory
4+
5+
# sha1 of "PlexTraktSync"
6+
SERVER_ID = "2ac365831928c7ad926484eae3c65e3a88112c54"
7+
8+
9+
def test_plex_id_numeric():
10+
pid = PlexIdFactory.create(10)
11+
assert pid.key == 10
12+
assert pid.media_type is None
13+
assert pid.provider is None
14+
assert pid.server is None
15+
assert pid.is_discover is False
16+
17+
pid = PlexIdFactory.create("10")
18+
assert pid.key == 10
19+
assert pid.media_type is None
20+
assert pid.provider is None
21+
assert pid.server is None
22+
assert pid.is_discover is False
23+
24+
25+
def test_plex_id_urls():
26+
pid = PlexIdFactory.create(
27+
f"https://app.plex.tv/desktop/#!/server/{SERVER_ID}/details?key=%2Flibrary%2Fmetadata%2F13202"
28+
)
29+
assert pid.key == 13202
30+
assert pid.media_type is None
31+
assert pid.provider is None
32+
assert pid.server == SERVER_ID
33+
assert pid.is_discover is False
34+
35+
pid = PlexIdFactory.create(
36+
f"https://app.plex.tv/desktop/#!/server/{SERVER_ID}/playHistory?filters=metadataItemID%3D6041&filterTitle=&isParentType=false"
37+
)
38+
assert pid.key == 6041
39+
assert pid.media_type is None
40+
assert pid.provider is None
41+
assert pid.server == SERVER_ID
42+
assert pid.is_discover is False
43+
44+
45+
def test_plex_id_discover_url():
46+
pid = PlexIdFactory.create(
47+
"https://app.plex.tv/desktop/#!/provider/tv.plex.provider.discover/details?key=%2Flibrary%2Fmetadata%2F5d7768532e80df001ebe18e7"
48+
)
49+
assert pid.key == "5d7768532e80df001ebe18e7"
50+
assert pid.media_type is None
51+
assert pid.provider == PlexId.METADATA
52+
assert pid.server is None
53+
assert pid.is_discover is True
54+
55+
56+
def test_plex_id_discover_url2():
57+
pid = PlexIdFactory.create(
58+
"https://app.plex.tv/desktop/#!/provider/tv.plex.provider.discover/details?key=/library/metadata/5d776a8e51dd69001fe24eb8"
59+
)
60+
assert pid.key == "5d776a8e51dd69001fe24eb8"
61+
assert pid.media_type is None
62+
assert pid.provider == PlexId.METADATA
63+
assert pid.server is None
64+
assert pid.is_discover is True

0 commit comments

Comments
 (0)