forked from platypush/platypush
Black/LINT for Jellyfin plugin.
This commit is contained in:
parent
a94ddd3f05
commit
e7bd61e0d4
2 changed files with 85 additions and 61 deletions
|
@ -4,8 +4,13 @@ import requests
|
||||||
from marshmallow import Schema
|
from marshmallow import Schema
|
||||||
|
|
||||||
from platypush.plugins import Plugin, action
|
from platypush.plugins import Plugin, action
|
||||||
from platypush.schemas.media.jellyfin import JellyfinArtistSchema, \
|
from platypush.schemas.media.jellyfin import (
|
||||||
JellyfinCollectionSchema, JellyfinMovieSchema, JellyfinEpisodeSchema
|
JellyfinArtistSchema,
|
||||||
|
JellyfinCollectionSchema,
|
||||||
|
JellyfinMovieSchema,
|
||||||
|
JellyfinVideoSchema,
|
||||||
|
JellyfinEpisodeSchema,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MediaJellyfinPlugin(Plugin):
|
class MediaJellyfinPlugin(Plugin):
|
||||||
|
@ -20,7 +25,9 @@ class MediaJellyfinPlugin(Plugin):
|
||||||
# Maximum number of results returned per query action
|
# Maximum number of results returned per query action
|
||||||
_default_limit = 100
|
_default_limit = 100
|
||||||
|
|
||||||
def __init__(self, server: str, api_key: str, username: Optional[str] = None, **kwargs):
|
def __init__(
|
||||||
|
self, server: str, api_key: str, username: Optional[str] = None, **kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param server: Jellyfin base server URL (including ``http://`` or ``https://``).
|
:param server: Jellyfin base server URL (including ``http://`` or ``https://``).
|
||||||
:param api_key: Server API key. You can generate one from
|
:param api_key: Server API key. You can generate one from
|
||||||
|
@ -35,16 +42,14 @@ class MediaJellyfinPlugin(Plugin):
|
||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
self.__user_id = None
|
self.__user_id = None
|
||||||
|
|
||||||
def _execute(
|
def _execute(self, method: str, url: str, *args, **kwargs) -> dict:
|
||||||
self, method: str, url: str, *args, **kwargs
|
|
||||||
) -> dict:
|
|
||||||
url = '/' + url.lstrip('/')
|
url = '/' + url.lstrip('/')
|
||||||
url = self.server + url
|
url = self.server + url
|
||||||
|
|
||||||
kwargs['headers'] = {
|
kwargs['headers'] = {
|
||||||
**kwargs.get('headers', {}),
|
**kwargs.get('headers', {}),
|
||||||
'X-Emby-Authorization': 'MediaBrowser Client="Platypush", Device="Platypush", '
|
'X-Emby-Authorization': 'MediaBrowser Client="Platypush", Device="Platypush", '
|
||||||
f'Token="{self._api_key}"'
|
f'Token="{self._api_key}"',
|
||||||
}
|
}
|
||||||
|
|
||||||
rs = getattr(requests, method.lower())(url, *args, **kwargs)
|
rs = getattr(requests, method.lower())(url, *args, **kwargs)
|
||||||
|
@ -58,19 +63,21 @@ class MediaJellyfinPlugin(Plugin):
|
||||||
try:
|
try:
|
||||||
self.__user_id = self._execute('GET', '/Users/Me')['Id']
|
self.__user_id = self._execute('GET', '/Users/Me')['Id']
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
assert e.response.status_code == 400, (
|
assert (
|
||||||
f'Could not get the current user: {e}'
|
e.response.status_code == 400
|
||||||
)
|
), f'Could not get the current user: {e}'
|
||||||
|
|
||||||
self.__user_id = self._execute('GET', '/Users')[0]['Id']
|
self.__user_id = self._execute('GET', '/Users')[0]['Id']
|
||||||
|
|
||||||
return self.__user_id
|
return self.__user_id
|
||||||
|
|
||||||
def _query(
|
def _query(
|
||||||
self, url: str,
|
self,
|
||||||
|
url: str,
|
||||||
schema_class: Optional[Type[Schema]] = None,
|
schema_class: Optional[Type[Schema]] = None,
|
||||||
query: Optional[str] = None,
|
query: Optional[str] = None,
|
||||||
limit: Optional[int] = _default_limit, offset: int = 0,
|
limit: Optional[int] = _default_limit,
|
||||||
|
offset: int = 0,
|
||||||
parent_id: Optional[str] = None,
|
parent_id: Optional[str] = None,
|
||||||
is_played: Optional[bool] = None,
|
is_played: Optional[bool] = None,
|
||||||
is_favourite: Optional[bool] = None,
|
is_favourite: Optional[bool] = None,
|
||||||
|
@ -78,7 +85,7 @@ class MediaJellyfinPlugin(Plugin):
|
||||||
genres: Optional[Iterable[str]] = None,
|
genres: Optional[Iterable[str]] = None,
|
||||||
tags: Optional[Iterable[str]] = None,
|
tags: Optional[Iterable[str]] = None,
|
||||||
years: Optional[Iterable[int]] = None,
|
years: Optional[Iterable[int]] = None,
|
||||||
**kwargs
|
**kwargs,
|
||||||
) -> Iterable[dict]:
|
) -> Iterable[dict]:
|
||||||
filters = []
|
filters = []
|
||||||
if is_played is not None:
|
if is_played is not None:
|
||||||
|
@ -107,35 +114,40 @@ class MediaJellyfinPlugin(Plugin):
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def _flatten_series_result(
|
def _flatten_series_result(self, search_result: dict) -> Iterable[dict]:
|
||||||
self, search_result: dict
|
|
||||||
) -> Iterable[dict]:
|
|
||||||
episodes = []
|
episodes = []
|
||||||
show_id = search_result['Id']
|
show_id = search_result['Id']
|
||||||
seasons = self._execute(
|
seasons = self._execute(
|
||||||
'get', f'/Shows/{show_id}/Seasons',
|
'get',
|
||||||
|
f'/Shows/{show_id}/Seasons',
|
||||||
params={
|
params={
|
||||||
'userId': self._user_id,
|
'userId': self._user_id,
|
||||||
}
|
},
|
||||||
).get('Items', [])
|
).get('Items', [])
|
||||||
|
|
||||||
for i, season in enumerate(seasons):
|
for i, season in enumerate(seasons):
|
||||||
episodes.extend(
|
episodes.extend(
|
||||||
JellyfinEpisodeSchema().dump([
|
JellyfinEpisodeSchema().dump(
|
||||||
{**episode, 'SeasonIndex': i+1}
|
[
|
||||||
|
{**episode, 'SeasonIndex': i + 1}
|
||||||
for episode in self._execute(
|
for episode in self._execute(
|
||||||
'get', f'/Shows/{show_id}/Episodes',
|
'get',
|
||||||
|
f'/Shows/{show_id}/Episodes',
|
||||||
params={
|
params={
|
||||||
'userId': self._user_id,
|
'userId': self._user_id,
|
||||||
'seasonId': season['Id'],
|
'seasonId': season['Id'],
|
||||||
}
|
},
|
||||||
).get('Items', [])
|
).get('Items', [])
|
||||||
], many=True)
|
],
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return episodes
|
return episodes
|
||||||
|
|
||||||
def _serialize_search_results(self, search_results: Iterable[dict]) -> Iterable[dict]:
|
def _serialize_search_results(
|
||||||
|
self, search_results: Iterable[dict]
|
||||||
|
) -> Iterable[dict]:
|
||||||
serialized_results = []
|
serialized_results = []
|
||||||
for result in search_results:
|
for result in search_results:
|
||||||
if result['Type'] == 'CollectionFolder':
|
if result['Type'] == 'CollectionFolder':
|
||||||
|
@ -144,9 +156,9 @@ class MediaJellyfinPlugin(Plugin):
|
||||||
elif result['Type'] == 'Movie':
|
elif result['Type'] == 'Movie':
|
||||||
result = JellyfinMovieSchema().dump(result)
|
result = JellyfinMovieSchema().dump(result)
|
||||||
result['type'] = 'movie' # type: ignore
|
result['type'] = 'movie' # type: ignore
|
||||||
elif result['Type'] == 'Movie':
|
elif result['Type'] == 'Video':
|
||||||
result = JellyfinMovieSchema().dump(result)
|
result = JellyfinVideoSchema().dump(result)
|
||||||
result['type'] = 'movie' # type: ignore
|
result['type'] = 'video' # type: ignore
|
||||||
elif result['Type'] == 'Series':
|
elif result['Type'] == 'Series':
|
||||||
serialized_results += self._flatten_series_result(result)
|
serialized_results += self._flatten_series_result(result)
|
||||||
for r in serialized_results:
|
for r in serialized_results:
|
||||||
|
@ -185,10 +197,17 @@ class MediaJellyfinPlugin(Plugin):
|
||||||
:return: .. schema:: media.jellyfin.JellyfinArtistSchema(many=True)
|
:return: .. schema:: media.jellyfin.JellyfinArtistSchema(many=True)
|
||||||
"""
|
"""
|
||||||
return self._query(
|
return self._query(
|
||||||
'/Artists', schema_class=JellyfinArtistSchema,
|
'/Artists',
|
||||||
limit=limit, offset=offset, is_favourite=is_favourite,
|
schema_class=JellyfinArtistSchema,
|
||||||
is_played=is_played, is_liked=is_liked, genres=genres,
|
limit=limit,
|
||||||
query=query, tags=tags, years=years
|
offset=offset,
|
||||||
|
is_favourite=is_favourite,
|
||||||
|
is_played=is_played,
|
||||||
|
is_liked=is_liked,
|
||||||
|
genres=genres,
|
||||||
|
query=query,
|
||||||
|
tags=tags,
|
||||||
|
years=years,
|
||||||
)
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -202,7 +221,7 @@ class MediaJellyfinPlugin(Plugin):
|
||||||
f'/Users/{self._user_id}/Items',
|
f'/Users/{self._user_id}/Items',
|
||||||
parent_id=None,
|
parent_id=None,
|
||||||
schema_class=JellyfinCollectionSchema,
|
schema_class=JellyfinCollectionSchema,
|
||||||
params=dict(recursive=False),
|
params={'recursive': False},
|
||||||
)
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -260,7 +279,8 @@ class MediaJellyfinPlugin(Plugin):
|
||||||
if collection:
|
if collection:
|
||||||
collections = self.get_collections().output # type: ignore
|
collections = self.get_collections().output # type: ignore
|
||||||
matching_collections = [
|
matching_collections = [
|
||||||
c for c in collections
|
c
|
||||||
|
for c in collections
|
||||||
if c['id'] == collection or c['name'].lower() == collection.lower()
|
if c['id'] == collection or c['name'].lower() == collection.lower()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -272,28 +292,36 @@ class MediaJellyfinPlugin(Plugin):
|
||||||
|
|
||||||
results = self._query(
|
results = self._query(
|
||||||
f'/Users/{self._user_id}/Items',
|
f'/Users/{self._user_id}/Items',
|
||||||
limit=limit, offset=offset, is_favourite=is_favourite,
|
limit=limit,
|
||||||
is_played=is_played, is_liked=is_liked, genres=genres,
|
offset=offset,
|
||||||
query=query, tags=tags, years=years, parent_id=parent_id,
|
is_favourite=is_favourite,
|
||||||
|
is_played=is_played,
|
||||||
|
is_liked=is_liked,
|
||||||
|
genres=genres,
|
||||||
|
query=query,
|
||||||
|
tags=tags,
|
||||||
|
years=years,
|
||||||
|
parent_id=parent_id,
|
||||||
params={
|
params={
|
||||||
**(
|
**(
|
||||||
{'sortOrder': 'Descending' if sort_desc else 'Ascending'}
|
{'sortOrder': 'Descending' if sort_desc else 'Ascending'}
|
||||||
if sort_desc is not None else {}
|
if sort_desc is not None
|
||||||
|
else {}
|
||||||
),
|
),
|
||||||
**(
|
**(
|
||||||
{'hasSubtitles': has_subtitles}
|
{'hasSubtitles': has_subtitles} if has_subtitles is not None else {}
|
||||||
if has_subtitles is not None else {}
|
|
||||||
),
|
),
|
||||||
**(
|
**(
|
||||||
{'minCriticRating': minimum_critic_rating}
|
{'minCriticRating': minimum_critic_rating}
|
||||||
if minimum_critic_rating is not None else {}
|
if minimum_critic_rating is not None
|
||||||
|
else {}
|
||||||
),
|
),
|
||||||
**(
|
**(
|
||||||
{'minCommunityRating': minimum_community_rating}
|
{'minCommunityRating': minimum_community_rating}
|
||||||
if minimum_community_rating is not None else {}
|
if minimum_community_rating is not None
|
||||||
|
else {}
|
||||||
),
|
),
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return self._serialize_search_results(results)
|
return self._serialize_search_results(results)
|
||||||
|
|
||||||
|
|
|
@ -21,8 +21,8 @@ class JellyfinSchema(Schema):
|
||||||
def gen_img_url(self, data: dict, **_) -> dict:
|
def gen_img_url(self, data: dict, **_) -> dict:
|
||||||
if 'image' in self.fields:
|
if 'image' in self.fields:
|
||||||
data['image'] = (
|
data['image'] = (
|
||||||
get_plugin('media.jellyfin').server + # type: ignore
|
get_plugin('media.jellyfin').server
|
||||||
f'/Items/{data["id"]}'
|
+ f'/Items/{data["id"]}' # type: ignore
|
||||||
'/Images/Primary?fillHeight=333&fillWidth=222&quality=96'
|
'/Images/Primary?fillHeight=333&fillWidth=222&quality=96'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,7 +44,8 @@ class JellyfinSchema(Schema):
|
||||||
if not video_format:
|
if not video_format:
|
||||||
if not available_containers:
|
if not available_containers:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
f'The media ID {data["Id"]} has no available video containers'
|
f'The media ID {data["Id"]} has no available video containers',
|
||||||
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
@ -53,17 +54,15 @@ class JellyfinSchema(Schema):
|
||||||
|
|
||||||
plugin = get_plugin('media.jellyfin')
|
plugin = get_plugin('media.jellyfin')
|
||||||
assert plugin, 'The media.jellyfin plugin is not configured'
|
assert plugin, 'The media.jellyfin plugin is not configured'
|
||||||
url = (
|
data['url'] = (
|
||||||
f'{plugin.server}/Videos/{data["Id"]}'
|
f'{plugin.server}/Videos/{data["Id"]}'
|
||||||
f'/stream.{video_format}'
|
f'/stream.{video_format}'
|
||||||
f'?Static=true&api_key={plugin._api_key}'
|
f'?Static=true&api_key={plugin._api_key}'
|
||||||
)
|
)
|
||||||
|
|
||||||
data['url'] = data['file'] = url
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class JellyfinArtistSchema(JellyfinSchema, MediaArtistSchema):
|
class JellyfinArtistSchema(JellyfinSchema, MediaArtistSchema):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -99,13 +98,10 @@ class JellyfinEpisodeSchema(JellyfinVideoSchema):
|
||||||
episode_index = data.get('IndexNumber')
|
episode_index = data.get('IndexNumber')
|
||||||
if episode_index:
|
if episode_index:
|
||||||
season_index = data.get('SeasonIndex', 1)
|
season_index = data.get('SeasonIndex', 1)
|
||||||
episode_index = 's{:02d}e{:02d}'.format(
|
episode_index = 's{:02d}e{:02d}'.format(season_index, episode_index)
|
||||||
season_index, episode_index
|
|
||||||
)
|
|
||||||
|
|
||||||
if episode_index:
|
if episode_index:
|
||||||
prefix += f'{" " if prefix else ""}[{episode_index}] '
|
prefix += f'{" " if prefix else ""}[{episode_index}] '
|
||||||
|
|
||||||
data['Name'] = prefix + data.get('Name', '')
|
data['Name'] = prefix + data.get('Name', '')
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue