Skip to content

Commit 210c5e6

Browse files
Replace python-plexapi with custom Json service
Tidy ups, fixes and improvements
1 parent 9b22dd4 commit 210c5e6

11 files changed

Lines changed: 418 additions & 709 deletions

File tree

mediux_posters/__main__.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from platform import python_version
66
from typing import Annotated, Protocol, TypeVar
77

8-
from plexapi.exceptions import Unauthorized
98
from typer import Abort, Context, Exit, Option, Typer
109

1110
from mediux_posters import __version__, get_cache_root, setup_logging
@@ -63,11 +62,8 @@ def setup(
6362
raise Abort
6463
mediux = Mediux(base_url=settings.mediux.base_url, api_key=settings.mediux.api_key)
6564
service_list = []
66-
try:
67-
if settings.plex.token:
68-
service_list.append(Plex(base_url=settings.plex.base_url, token=settings.plex.token))
69-
except Unauthorized as err:
70-
LOGGER.warning(err)
65+
if settings.plex.token:
66+
service_list.append(Plex(base_url=settings.plex.base_url, token=settings.plex.token))
7167
if settings.jellyfin.token:
7268
service_list.append(
7369
Jellyfin(base_url=settings.jellyfin.base_url, token=settings.jellyfin.token)
@@ -129,14 +125,16 @@ def process_image(
129125
setattr(entry, attribute, True)
130126
else:
131127
if should_log:
132-
LOGGER.info("Downloading '%s' by '%s'", set_data.set_title, set_data.username)
128+
LOGGER.info(
129+
"[Mediux] Downloading '%s' by '%s'", set_data.set_title, set_data.username
130+
)
133131
should_log = False
134132
mediux.download_image(file_id=file_info.id, output=image_file)
135133
setattr(
136134
entry,
137135
attribute,
138136
service.upload_image(
139-
obj=entry, image_file=image_file, kometa_integration=kometa_integration
137+
object_id=entry.id, image_file=image_file, kometa_integration=kometa_integration
140138
),
141139
)
142140
return should_log
@@ -152,7 +150,9 @@ def process_set_data(
152150
) -> bool:
153151
should_log = True
154152
if entry.all_posters_uploaded:
155-
LOGGER.info("All posters have been uploaded, skipping remaining sets")
153+
LOGGER.info(
154+
"[%s] All posters have been uploaded, skipping remaining sets", type(service).__name__
155+
)
156156
return False
157157

158158
should_log = process_image(
@@ -180,7 +180,8 @@ def process_set_data(
180180
kometa_integration=kometa_integration,
181181
)
182182
if media_type is MediaType.SHOW:
183-
for season in entry.seasons:
183+
for season in service.list_seasons(show_id=entry.id):
184+
entry.seasons.append(season)
184185
mediux_season = next(
185186
(x for x in set_data.show.seasons if x.number == season.number), None
186187
)
@@ -198,7 +199,8 @@ def process_set_data(
198199
service=service,
199200
kometa_integration=kometa_integration,
200201
)
201-
for episode in season.episodes:
202+
for episode in service.list_episodes(show_id=entry.id, season_id=season.id):
203+
season.episodes.append(episode)
202204
mediux_episode = next(
203205
(x for x in mediux_season.episodes if x.number == episode.number), None
204206
)
@@ -217,7 +219,8 @@ def process_set_data(
217219
kometa_integration=kometa_integration,
218220
)
219221
elif media_type is MediaType.COLLECTION:
220-
for movie in entry.movies:
222+
for movie in service.list_collection_movies(collection_id=entry.id):
223+
entry.movies.append(movie)
221224
mediux_movie = next(
222225
(x for x in set_data.collection.movies if x.tmdb_id == movie.tmdb_id), None
223226
)
@@ -483,7 +486,7 @@ def media_posters(
483486
style="subtitle",
484487
)
485488
if simple_clean:
486-
LOGGER.info("Cleaning %s cache", entry.display_name)
489+
LOGGER.info("Cleaning %s from cache", entry.display_name)
487490
delete_folder(folder=get_cached_image(slugify(value=entry.display_name)))
488491
if media_type is MediaType.COLLECTION:
489492
for movie in entry.movies:
@@ -635,7 +638,7 @@ def set_posters(
635638
LOGGER.warning("[%s] %s", type(service).__name__, err)
636639
break
637640
if simple_clean:
638-
LOGGER.info("Cleaning %s cache", entry.display_name)
641+
LOGGER.info("Cleaning %s from cache", entry.display_name)
639642
delete_folder(folder=get_cached_image(slugify(value=entry.display_name)))
640643
if media_type is MediaType.COLLECTION:
641644
for movie in entry.movies:

mediux_posters/mediux/service.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
from mediux_posters.utils import MediaType
2020

2121
LOGGER = logging.getLogger(__name__)
22-
MINUTE = 60
22+
# 60 Calls per Minute
23+
CALLS = 60
24+
PERIOD = 60
2325

2426

2527
class Mediux:
@@ -105,7 +107,7 @@ def __init__(self, base_url: str, api_key: str):
105107
)
106108

107109
@sleep_and_retry
108-
@limits(calls=30, period=MINUTE)
110+
@limits(calls=CALLS, period=PERIOD)
109111
def _perform_graphql_request(self, query: str) -> dict[str, Any]:
110112
try:
111113
response = self.client.post("/graphql", json={"query": query})
@@ -318,6 +320,8 @@ def get_set(
318320
else None
319321
)
320322

323+
@sleep_and_retry
324+
@limits(calls=CALLS, period=PERIOD)
321325
def download_image(self, file_id: str, output: Path) -> None:
322326
output.parent.mkdir(parents=True, exist_ok=True)
323327
output.unlink(missing_ok=True)

mediux_posters/services/_base/service.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@
2121

2222

2323
class BaseService(ABC, Generic[T, S, E, C, M]):
24+
@abstractmethod
25+
def list_episodes(self, show_id: int | str, season_id: int | str) -> list[E]: ...
26+
27+
@abstractmethod
28+
def list_seasons(self, show_id: int | str) -> list[S]: ...
29+
2430
@abstractmethod
2531
def list_shows(self, skip_libraries: list[str] | None = None) -> list[T]: ...
2632

@@ -33,6 +39,9 @@ def list_collections(self, skip_libraries: list[str] | None = None) -> list[C]:
3339
@abstractmethod
3440
def get_collection(self, tmdb_id: int) -> C | None: ...
3541

42+
@abstractmethod
43+
def list_collection_movies(self, collection_id: int | str) -> list[M]: ...
44+
3645
@abstractmethod
3746
def list_movies(self, skip_libraries: list[str] | None = None) -> list[M]: ...
3847

@@ -66,5 +75,5 @@ def get(self, media_type: MediaType, tmdb_id: int) -> T | C | M | None:
6675

6776
@abstractmethod
6877
def upload_image(
69-
self, obj: T | S | E | M | C, image_file: Path, kometa_integration: bool
78+
self, object_id: int | str, image_file: Path, kometa_integration: bool
7079
) -> bool: ...

mediux_posters/services/jellyfin/service.py

Lines changed: 24 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
from httpx import Client, HTTPStatusError, RequestError, TimeoutException
1212
from pydantic import TypeAdapter, ValidationError
13-
from ratelimit import limits, sleep_and_retry
1413

1514
from mediux_posters import __version__
1615
from mediux_posters.console import CONSOLE
@@ -26,7 +25,6 @@
2625
)
2726

2827
LOGGER = logging.getLogger(__name__)
29-
MINUTE = 60
3028

3129

3230
class Jellyfin(BaseService[Show, Season, Episode, Collection, Movie]):
@@ -44,8 +42,6 @@ def __init__(self, base_url: str, token: str):
4442
def extract_id(cls, entry: dict, prefix: str = "Tmdb") -> str | None:
4543
return entry.get("ProviderIds", {}).get(prefix)
4644

47-
@sleep_and_retry
48-
@limits(calls=30, period=MINUTE)
4945
def _perform_get_request(
5046
self, endpoint: str, params: dict[str, str | list[str]] | None = None
5147
) -> dict:
@@ -71,8 +67,6 @@ def _perform_get_request(
7167
except TimeoutException as err:
7268
raise ServiceError("Service took too long to respond") from err
7369

74-
@sleep_and_retry
75-
@limits(calls=30, period=MINUTE)
7670
def _perform_post_request(
7771
self, endpoint: str, body: bytes, headers: dict[str, str] | None = None
7872
) -> None:
@@ -114,15 +108,18 @@ def _list_libraries(
114108
except ValidationError as err:
115109
raise ServiceError(err) from err
116110

117-
def _parse_show(self, jellyfin_show: dict) -> Show:
118-
show = TypeAdapter(Show).validate_python(jellyfin_show)
119-
for jellyfin_season in self._list_seasons(show_id=show.id):
120-
season = TypeAdapter(Season).validate_python(jellyfin_season)
121-
for jellyfin_episode in self._list_episodes(show_id=show.id, season_id=season.id):
122-
episode = TypeAdapter(Episode).validate_python(jellyfin_episode)
123-
season.episodes.append(episode)
124-
show.seasons.append(season)
125-
return show
111+
def list_episodes(self, show_id: str, season_id: str) -> list[Episode]:
112+
results = self._perform_get_request(
113+
endpoint=f"/Shows/{show_id}/Episodes",
114+
params={"seasonId": season_id, "fields": ["ProviderIds"]},
115+
).get("Items", [])
116+
return TypeAdapter(list[Episode]).validate_python(results)
117+
118+
def list_seasons(self, show_id: str) -> list[Season]:
119+
results = self._perform_get_request(
120+
endpoint=f"/Shows/{show_id}/Seasons", params={"fields": ["ProviderIds"]}
121+
).get("Items", [])
122+
return TypeAdapter(list[Season]).validate_python(results)
126123

127124
def _list_shows(
128125
self,
@@ -149,48 +146,26 @@ def _list_shows(
149146
if not tmdb or (tmdb_id is not None and int(tmdb) != tmdb_id):
150147
continue
151148
try:
152-
output.append(self._parse_show(jellyfin_show=result))
149+
result = TypeAdapter(Show).validate_python(result)
150+
output.append(result)
153151
except ValidationError as err:
154152
raise ServiceError(err) from err
155153
return output
156154

157-
def _list_seasons(self, show_id: str) -> list[dict]:
158-
return self._perform_get_request(
159-
endpoint=f"/Shows/{show_id}/Seasons", params={"fields": ["ProviderIds"]}
160-
).get("Items", [])
161-
162-
def _list_episodes(self, show_id: str, season_id: str) -> list[dict]:
163-
return self._perform_get_request(
164-
endpoint=f"/Shows/{show_id}/Episodes",
165-
params={"seasonId": season_id, "fields": ["ProviderIds"]},
166-
).get("Items", [])
167-
168155
def list_shows(self, skip_libraries: list[str] | None = None) -> list[Show]:
169156
return self._list_shows(skip_libraries=skip_libraries)
170157

171158
def get_show(self, tmdb_id: int) -> Show | None:
172159
return next(iter(self._list_shows(tmdb_id=tmdb_id)), None)
173160

174-
def _list_collections(
175-
self,
176-
skip_libraries: list[str] | None = None,
177-
tmdb_id: int | None = None, # noqa: ARG002
178-
collection_id: str | None = None, # noqa: ARG002
179-
) -> list[Collection]:
180-
libraries = self._list_libraries(media_type="unknown", skip_libraries=skip_libraries)
181-
output = []
182-
for _library in libraries:
183-
pass
184-
return output
185-
186-
def list_collections(self, skip_libraries: list[str] | None = None) -> list[Collection]:
187-
return self._list_collections(skip_libraries=skip_libraries)
161+
def list_collections(self, skip_libraries: list[str] | None = None) -> list[Collection]: # noqa: ARG002
162+
return []
188163

189-
def get_collection(self, tmdb_id: int) -> Collection | None:
190-
return next(iter(self._list_collections(tmdb_id=tmdb_id)), None)
164+
def get_collection(self, tmdb_id: int) -> Collection | None: # noqa: ARG002
165+
return None
191166

192-
def _parse_movie(self, jellyfin_movie: dict) -> Movie:
193-
return TypeAdapter(Movie).validate_python(jellyfin_movie)
167+
def list_collection_movies(self, collection_id: int | str) -> list[Movie]: # noqa: ARG002
168+
return []
194169

195170
def _list_movies(
196171
self,
@@ -217,7 +192,8 @@ def _list_movies(
217192
if not tmdb or (tmdb_id is not None and int(tmdb) != tmdb_id):
218193
continue
219194
try:
220-
output.append(self._parse_movie(jellyfin_movie=result))
195+
result = TypeAdapter(Movie).validate_python(result)
196+
output.append(result)
221197
except ValidationError as err:
222198
raise ServiceError(err) from err
223199
return output
@@ -230,7 +206,7 @@ def get_movie(self, tmdb_id: int) -> Movie | None:
230206

231207
def upload_image(
232208
self,
233-
obj: Show | Season | Episode | Movie | Collection,
209+
object_id: int | str,
234210
image_file: Path,
235211
kometa_integration: bool, # noqa: ARG002
236212
) -> bool:
@@ -244,7 +220,7 @@ def upload_image(
244220
image_data = b64encode(stream.read())
245221
try:
246222
self._perform_post_request(
247-
endpoint=f"/Items/{obj.id}/Images/{image_type}",
223+
endpoint=f"/Items/{object_id}/Images/{image_type}",
248224
headers=headers,
249225
body=image_data,
250226
)

0 commit comments

Comments
 (0)