Compare commits
16 Commits
aa2e450454
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
0bbf7a405e
|
|||
|
6452cb7f41
|
|||
|
740c892946
|
|||
|
e9827723f4
|
|||
|
89209fa675
|
|||
|
f8619aa66c
|
|||
|
b7ff56ced2
|
|||
|
946b1605e0
|
|||
|
32e4a76cd1
|
|||
|
a5659d90e8
|
|||
|
906b07e20c
|
|||
|
d4febbc3b2
|
|||
|
eb69650423
|
|||
|
0c242f15f4
|
|||
|
5ca3a38beb
|
|||
|
090b96bb4e
|
@@ -6,3 +6,6 @@
|
||||
- Added a formatting script (`scripts/format.sh`) that uses `autopep8` to automatically format the codebase for better readability and consistency
|
||||
- Removed caching of last fetched items to ensure the most up-to-date information is always displayed in Discord Rich Presence
|
||||
- Handle paused state correctly by checking the `PlayState` property from Jellyfin and updating Discord Rich Presence accordingly
|
||||
- Set the `auth.ssl` configuration option in Jellyfin Client dynamically based on the `jellyfin_server_url`
|
||||
- Removed `to_rpc_payload` function in favor of a class method within the `JellyfinMediaItem` class for better encapsulation and organization of code
|
||||
- Added major improvements to typing and type hints throughout the codebase for better code clarity and maintainability
|
||||
|
||||
5
changelog/0.1.2.md
Normal file
5
changelog/0.1.2.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 0.1.2
|
||||
|
||||
- Extracted utility functions from JellyfinApiClient to utils modules
|
||||
- Keep client configuration in Settings class
|
||||
- Use `OriginalTitle` instead of `Name` for media items when available to provide more accurate titles in Discord Rich Presence
|
||||
@@ -1,4 +1,4 @@
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, NonNegativeInt
|
||||
from pypresence.types import ActivityType
|
||||
from typing import Optional
|
||||
|
||||
@@ -9,6 +9,6 @@ class DiscordRPCUpdatePayload(BaseModel):
|
||||
subtitle: str
|
||||
image_url: str
|
||||
details: str
|
||||
start: Optional[int]
|
||||
end: Optional[int]
|
||||
start: Optional[NonNegativeInt]
|
||||
end: Optional[NonNegativeInt]
|
||||
activity_type: ActivityType
|
||||
|
||||
@@ -7,17 +7,21 @@ import logging
|
||||
class DiscordRPC:
|
||||
"""
|
||||
Client for interacting with Discord Rich Presence (RPC).
|
||||
|
||||
Attributes:
|
||||
logger (logging.Logger): Logger instance for logging messages.
|
||||
"""
|
||||
logger: logging.Logger = logging.getLogger('DiscordRPC')
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initializes the Discord RPC client and connects to Discord via IPC.
|
||||
"""
|
||||
self.logger = logging.getLogger('DiscordRPC')
|
||||
|
||||
self.logger.info("Connecting to Discord RPC...")
|
||||
|
||||
self.rpc = Presence(settings.discord_app_id)
|
||||
self.rpc.connect()
|
||||
|
||||
self.logger.info("Connected to Discord RPC.")
|
||||
|
||||
def update(self, payload: DiscordRPCUpdatePayload):
|
||||
@@ -28,6 +32,7 @@ class DiscordRPC:
|
||||
payload (DiscordRPCUpdatePayload): The payload containing presence information.
|
||||
"""
|
||||
self.logger.info("Updating Discord RPC presence...")
|
||||
|
||||
self.rpc.update(
|
||||
activity_type=payload.activity_type,
|
||||
details=payload.title,
|
||||
@@ -37,6 +42,7 @@ class DiscordRPC:
|
||||
start=payload.start,
|
||||
end=payload.end
|
||||
)
|
||||
|
||||
self.logger.info("Discord RPC presence updated.")
|
||||
|
||||
def clear(self):
|
||||
@@ -44,5 +50,7 @@ class DiscordRPC:
|
||||
Clears the Discord RPC presence.
|
||||
"""
|
||||
self.logger.info("Clearing Discord RPC presence...")
|
||||
|
||||
self.rpc.clear()
|
||||
|
||||
self.logger.info("Discord RPC presence cleared.")
|
||||
|
||||
@@ -1,36 +1,41 @@
|
||||
from settings import settings
|
||||
from jellyfin_apiclient_python import JellyfinClient
|
||||
from getmac import get_mac_address
|
||||
from jellyfin.models import JellyfinMediaItem, JellyfinMediaType, JellyfinMusicMediaMetadata, JellyfinMovieMediaMetadata, JellyfinEpisodeMediaMetadata
|
||||
from datetime import datetime
|
||||
from typing import Optional, Tuple
|
||||
from jellyfin.models import (
|
||||
JellyfinMediaItem,
|
||||
JellyfinMediaType,
|
||||
JellyfinMusicMediaMetadata,
|
||||
JellyfinMovieMediaMetadata,
|
||||
JellyfinEpisodeMediaMetadata
|
||||
)
|
||||
from jellyfin.utils.config import configure_client
|
||||
from jellyfin.utils.image import get_image_url
|
||||
from jellyfin.utils.models import episode, movie, audio
|
||||
from typing import Optional
|
||||
import logging
|
||||
import time
|
||||
import os
|
||||
|
||||
|
||||
class JellyfinApiClient:
|
||||
"""
|
||||
Client for interacting with the Jellyfin server API.
|
||||
|
||||
Attributes:
|
||||
last_auth_time (Optional[float]): Timestamp of the last authentication.
|
||||
logger (logging.Logger): Logger instance for logging messages.
|
||||
"""
|
||||
last_auth_time: Optional[float] = None
|
||||
logger: logging.Logger = logging.getLogger('JellyfinApiClient')
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initializes the Jellyfin API client and authenticates with the server.
|
||||
"""
|
||||
machine_name = os.uname().nodename
|
||||
unique_id = get_mac_address(hostname="localhost")
|
||||
|
||||
self.logger = logging.getLogger('JellyfinApiClient')
|
||||
|
||||
self.logger.info("Connecting to Jellyfin server...")
|
||||
|
||||
self.client = JellyfinClient()
|
||||
self.client.config.app('jellydisc', '0.1.1', machine_name, unique_id)
|
||||
self.client.config.data['auth.ssl'] = True
|
||||
configure_client(self.client)
|
||||
|
||||
self.last_auth_time = None
|
||||
self.authenticate()
|
||||
|
||||
self.logger.info("Connected to Jellyfin server.")
|
||||
|
||||
def authenticate(self):
|
||||
@@ -47,12 +52,12 @@ class JellyfinApiClient:
|
||||
self.last_auth_time = time.time()
|
||||
self.logger.info("Authenticated with Jellyfin server.")
|
||||
|
||||
def get_current_playback(self) -> JellyfinMediaItem | None:
|
||||
def get_current_media(self) -> Optional[JellyfinMediaItem]:
|
||||
"""
|
||||
Fetches the current playback information from the Jellyfin server.
|
||||
Fetches the current media information from the Jellyfin server.
|
||||
|
||||
Returns:
|
||||
JellyfinMediaItem | None: The current playback media item or None if no active playback is found.
|
||||
Optional[JellyfinMediaItem]: The current playback media item or None if no active playback is found.
|
||||
"""
|
||||
if time.time() - self.last_auth_time > settings.jellyfin_auth_timeout:
|
||||
self.authenticate()
|
||||
@@ -76,18 +81,6 @@ class JellyfinApiClient:
|
||||
self.logger.info("No active playback found.")
|
||||
return None
|
||||
|
||||
def get_image_url(self, media_id: str) -> str:
|
||||
"""
|
||||
Constructs the image URL for a given media item.
|
||||
|
||||
Args:
|
||||
media_id (str): The ID of the media item.
|
||||
Returns:
|
||||
str: The constructed image URL.
|
||||
"""
|
||||
server_address = settings.jellyfin_server_url.rstrip('/')
|
||||
return f"{server_address}/Items/{media_id}/Images/Primary?maxWidth=300&maxHeight=300"
|
||||
|
||||
def to_model(self, item: dict) -> JellyfinMediaItem:
|
||||
"""
|
||||
Converts a Jellyfin media item dictionary to a JellyfinMediaItem model.
|
||||
@@ -100,126 +93,8 @@ class JellyfinApiClient:
|
||||
media_type = item.get('Type')
|
||||
|
||||
if media_type == 'Audio':
|
||||
return self.to_music_model(item)
|
||||
return audio.to_media_item(item)
|
||||
elif media_type == 'Episode':
|
||||
return self.to_episode_model(item)
|
||||
return episode.to_media_item(item, self.client)
|
||||
elif media_type == 'Movie':
|
||||
return self.to_movie_model(item)
|
||||
|
||||
def get_playback_info(
|
||||
self, media: dict) -> Tuple[Optional[int], Optional[int]]:
|
||||
"""
|
||||
Extracts playback start and end timestamps from a media item.
|
||||
|
||||
Args:
|
||||
media (dict): The Jellyfin media item dictionary.
|
||||
Returns:
|
||||
Tuple[Optional[int], Optional[int]]: A tuple containing the start and end timestamps in seconds, or (None, None) if the media is paused.
|
||||
"""
|
||||
play_state = media.get('PlayState')
|
||||
is_paused = play_state.get('IsPaused')
|
||||
|
||||
if is_paused:
|
||||
return (None, None)
|
||||
|
||||
runtime_ticks = media.get('RunTimeTicks')
|
||||
total_runtime_seconds = runtime_ticks // 10_000_000
|
||||
|
||||
playback_position_ticks = play_state.get('PositionTicks')
|
||||
playback_position_seconds = playback_position_ticks // 10_000_000
|
||||
|
||||
start = int(datetime.now().timestamp()) - playback_position_seconds
|
||||
end = start + total_runtime_seconds
|
||||
|
||||
return (start, end)
|
||||
|
||||
def to_music_model(self, item: dict) -> JellyfinMediaItem:
|
||||
"""
|
||||
Converts a Jellyfin music media item dictionary to a JellyfinMediaItem model.
|
||||
|
||||
Args:
|
||||
item (dict): The Jellyfin music media item dictionary.
|
||||
Returns:
|
||||
JellyfinMediaItem: The converted JellyfinMediaItem model.
|
||||
"""
|
||||
media_id = item.get('Id')
|
||||
parent_id = item.get('ParentId')
|
||||
premiere_date = item.get('PremiereDate')
|
||||
premiere_year = datetime.fromisoformat(
|
||||
premiere_date).year if premiere_date else None
|
||||
|
||||
metadata = JellyfinMusicMediaMetadata(
|
||||
artist=item.get('AlbumArtist'), album=f"{
|
||||
item.get('Album')} ({premiere_year})" if premiere_date else item.get('Album'))
|
||||
|
||||
(start, end) = self.get_playback_info(item)
|
||||
|
||||
return JellyfinMediaItem(
|
||||
id=media_id,
|
||||
name=item.get('Name'),
|
||||
type=JellyfinMediaType.AUDIO,
|
||||
image_url=self.get_image_url(parent_id),
|
||||
start=start,
|
||||
end=end,
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
def to_movie_model(self, item: dict) -> JellyfinMediaItem:
|
||||
"""
|
||||
Converts a Jellyfin movie media item dictionary to a JellyfinMediaItem model.
|
||||
|
||||
Args:
|
||||
item (dict): The Jellyfin movie media item dictionary.
|
||||
Returns:
|
||||
JellyfinMediaItem: The converted JellyfinMediaItem model.
|
||||
"""
|
||||
media_id = item.get('Id')
|
||||
premiere_date = item.get('PremiereDate')
|
||||
|
||||
metadata = JellyfinMovieMediaMetadata(date=datetime.fromisoformat(
|
||||
premiere_date).strftime('%d/%m/%Y') if premiere_date else None)
|
||||
|
||||
(start, end) = self.get_playback_info(item)
|
||||
|
||||
return JellyfinMediaItem(
|
||||
id=media_id,
|
||||
name=item.get('Name'),
|
||||
type=JellyfinMediaType.MOVIE,
|
||||
image_url=self.get_image_url(media_id),
|
||||
start=start,
|
||||
end=end,
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
def to_episode_model(self, item: dict) -> JellyfinMediaItem:
|
||||
"""
|
||||
Converts a Jellyfin episode media item dictionary to a JellyfinMediaItem model.
|
||||
|
||||
Args:
|
||||
item (dict): The Jellyfin episode media item dictionary.
|
||||
Returns:
|
||||
JellyfinMediaItem: The converted JellyfinMediaItem model.
|
||||
"""
|
||||
media_id = item.get('Id')
|
||||
parent = self.client.jellyfin.get_item(item.get('ParentId'))
|
||||
parent_id = parent.get('ParentId')
|
||||
series_name = item.get('SeriesName')
|
||||
season_number = item.get('ParentIndexNumber')
|
||||
episode_number = item.get('IndexNumber')
|
||||
|
||||
metadata = JellyfinEpisodeMediaMetadata(
|
||||
subtitle=f"S{
|
||||
season_number:02}E{
|
||||
episode_number:02} of {series_name}")
|
||||
|
||||
(start, end) = self.get_playback_info(item)
|
||||
|
||||
return JellyfinMediaItem(
|
||||
id=media_id,
|
||||
name=item.get('Name'),
|
||||
type=JellyfinMediaType.EPISODE,
|
||||
image_url=self.get_image_url(parent_id),
|
||||
start=start,
|
||||
end=end,
|
||||
metadata=metadata
|
||||
)
|
||||
return movie.to_media_item(item)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from pydantic import BaseModel
|
||||
from enum import Enum
|
||||
from pydantic import BaseModel, HttpUrl, NonNegativeInt
|
||||
from enum import StrEnum
|
||||
from typing import Optional, Union
|
||||
from discord.models import DiscordRPCUpdatePayload
|
||||
from pypresence.types import ActivityType
|
||||
|
||||
|
||||
class JellyfinMediaType(str, Enum):
|
||||
class JellyfinMediaType(StrEnum):
|
||||
AUDIO = 'Audio'
|
||||
MOVIE = 'Movie'
|
||||
EPISODE = 'Episode'
|
||||
@@ -15,7 +17,7 @@ class JellyfinMusicMediaMetadata(BaseModel):
|
||||
|
||||
|
||||
class JellyfinMovieMediaMetadata(BaseModel):
|
||||
date: Optional[str]
|
||||
year: Optional[int]
|
||||
|
||||
|
||||
class JellyfinEpisodeMediaMetadata(BaseModel):
|
||||
@@ -26,9 +28,51 @@ class JellyfinMediaItem(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
type: JellyfinMediaType
|
||||
image_url: str
|
||||
start: Optional[int]
|
||||
end: Optional[int]
|
||||
image_url: HttpUrl
|
||||
start: Optional[NonNegativeInt]
|
||||
end: Optional[NonNegativeInt]
|
||||
metadata: Union[JellyfinMusicMediaMetadata,
|
||||
JellyfinMovieMediaMetadata,
|
||||
JellyfinEpisodeMediaMetadata]
|
||||
|
||||
def to_rpc_payload(self) -> DiscordRPCUpdatePayload:
|
||||
"""
|
||||
Converts a JellyfinMediaItem to a DiscordRPCUpdatePayload.
|
||||
|
||||
Returns:
|
||||
DiscordRPCUpdatePayload: The converted Discord RPC update payload.
|
||||
"""
|
||||
if self.type == JellyfinMediaType.AUDIO:
|
||||
return DiscordRPCUpdatePayload(
|
||||
id=self.id,
|
||||
title=f"Listening to {
|
||||
self.name}",
|
||||
subtitle=f"by {
|
||||
self.metadata.artist}",
|
||||
image_url=str(self.image_url),
|
||||
details=self.metadata.album,
|
||||
activity_type=ActivityType.LISTENING,
|
||||
start=self.start,
|
||||
end=self.end)
|
||||
elif self.type == JellyfinMediaType.MOVIE:
|
||||
return DiscordRPCUpdatePayload(
|
||||
id=self.id,
|
||||
title=f"Watching {self.name}",
|
||||
subtitle=str(self.metadata.year),
|
||||
image_url=str(self.image_url),
|
||||
details=self.name,
|
||||
activity_type=ActivityType.WATCHING,
|
||||
start=self.start,
|
||||
end=self.end
|
||||
)
|
||||
elif self.type == JellyfinMediaType.EPISODE:
|
||||
return DiscordRPCUpdatePayload(
|
||||
id=self.id,
|
||||
title=f"Watching {self.name}",
|
||||
subtitle=self.metadata.subtitle,
|
||||
image_url=str(self.image_url),
|
||||
details=self.name,
|
||||
activity_type=ActivityType.WATCHING,
|
||||
start=self.start,
|
||||
end=self.end
|
||||
)
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
from discord.models import DiscordRPCUpdatePayload
|
||||
from jellyfin.models import JellyfinMediaItem, JellyfinMediaType
|
||||
from pypresence.types import ActivityType
|
||||
|
||||
|
||||
def to_rpc_payload(media_item: JellyfinMediaItem) -> DiscordRPCUpdatePayload:
|
||||
"""
|
||||
Converts a JellyfinMediaItem to a DiscordRPCUpdatePayload.
|
||||
|
||||
Args:
|
||||
media_item (JellyfinMediaItem): The Jellyfin media item to convert.
|
||||
Returns:
|
||||
DiscordRPCUpdatePayload: The converted Discord RPC update payload.
|
||||
"""
|
||||
if media_item.type == JellyfinMediaType.AUDIO:
|
||||
return DiscordRPCUpdatePayload(
|
||||
id=media_item.id,
|
||||
title=f"Listening to {
|
||||
media_item.name}",
|
||||
subtitle=f"by {
|
||||
media_item.metadata.artist}",
|
||||
image_url=media_item.image_url,
|
||||
details=media_item.metadata.album,
|
||||
activity_type=ActivityType.LISTENING,
|
||||
start=media_item.start,
|
||||
end=media_item.end)
|
||||
elif media_item.type == JellyfinMediaType.MOVIE:
|
||||
return DiscordRPCUpdatePayload(
|
||||
id=media_item.id,
|
||||
title=f"Watching {media_item.name}",
|
||||
subtitle=media_item.metadata.date,
|
||||
image_url=media_item.image_url,
|
||||
details=media_item.name,
|
||||
activity_type=ActivityType.WATCHING,
|
||||
start=media_item.start,
|
||||
end=media_item.end
|
||||
)
|
||||
elif media_item.type == JellyfinMediaType.EPISODE:
|
||||
return DiscordRPCUpdatePayload(
|
||||
id=media_item.id,
|
||||
title=f"Watching {media_item.name}",
|
||||
subtitle=media_item.metadata.subtitle,
|
||||
image_url=media_item.image_url,
|
||||
details=media_item.name,
|
||||
activity_type=ActivityType.WATCHING,
|
||||
start=media_item.start,
|
||||
end=media_item.end
|
||||
)
|
||||
41
jellyfin/utils/config.py
Normal file
41
jellyfin/utils/config.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from jellyfin_apiclient_python import JellyfinClient
|
||||
from getmac import get_mac_address
|
||||
from settings import settings
|
||||
import os
|
||||
|
||||
|
||||
def configure_client(client: JellyfinClient):
|
||||
"""
|
||||
Configures the Jellyfin client with application details and SSL settings.
|
||||
|
||||
Args:
|
||||
client (JellyfinClient): The Jellyfin client to configure.
|
||||
"""
|
||||
client.config.app(
|
||||
settings.app_name,
|
||||
settings.app_version,
|
||||
get_machine_name(),
|
||||
get_unique_id()
|
||||
)
|
||||
client.config.data['auth.ssl'] = settings.jellyfin_server_url.startswith(
|
||||
'https://')
|
||||
|
||||
|
||||
def get_machine_name() -> str:
|
||||
"""
|
||||
Retrieves the machine name of the current host.
|
||||
|
||||
Returns:
|
||||
str: The machine name.
|
||||
"""
|
||||
return os.uname().nodename
|
||||
|
||||
|
||||
def get_unique_id() -> str:
|
||||
"""
|
||||
Retrieves the MAC address of the localhost as a unique identifier.
|
||||
|
||||
Returns:
|
||||
str: The MAC address.
|
||||
"""
|
||||
return get_mac_address(hostname="localhost")
|
||||
14
jellyfin/utils/image.py
Normal file
14
jellyfin/utils/image.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from settings import settings
|
||||
|
||||
|
||||
def get_image_url(media_id: str) -> str:
|
||||
"""
|
||||
Constructs the image URL for a given media item.
|
||||
|
||||
Args:
|
||||
media_id (str): The ID of the media item.
|
||||
Returns:
|
||||
str: The constructed image URL.
|
||||
"""
|
||||
server_address = settings.jellyfin_server_url.rstrip('/')
|
||||
return f"{server_address}/Items/{media_id}/Images/Primary?maxWidth=300&maxHeight=300"
|
||||
53
jellyfin/utils/models/audio.py
Normal file
53
jellyfin/utils/models/audio.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from jellyfin.models import (
|
||||
JellyfinMediaItem,
|
||||
JellyfinMediaType,
|
||||
JellyfinMusicMediaMetadata
|
||||
)
|
||||
from jellyfin.utils.playback import get_current_playback
|
||||
from jellyfin.utils.image import get_image_url
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def to_media_item(
|
||||
item: dict
|
||||
) -> JellyfinMediaItem:
|
||||
"""
|
||||
Converts a Jellyfin audio media item dictionary to a JellyfinMediaItem.
|
||||
|
||||
Args:
|
||||
item (dict): The Jellyfin music media item dictionary.
|
||||
Returns:
|
||||
JellyfinMediaItem: The converted JellyfinMediaItem model.
|
||||
"""
|
||||
item_id = item.get('Id')
|
||||
|
||||
# Get album ID (for image)
|
||||
album_id = item.get('ParentId')
|
||||
|
||||
# Construct album title (with year if available)
|
||||
premiere_date = item.get('PremiereDate')
|
||||
premiere_year = None
|
||||
album = item.get('Album')
|
||||
|
||||
if premiere_date:
|
||||
premiere_year = datetime.fromisoformat(premiere_date).year
|
||||
album = f"{album} ({premiere_year})"
|
||||
|
||||
# Construct metadata
|
||||
metadata = JellyfinMusicMediaMetadata(
|
||||
artist=item.get('AlbumArtist'),
|
||||
album=album
|
||||
)
|
||||
|
||||
# Get playback positions
|
||||
(start, end) = get_current_playback(item)
|
||||
|
||||
return JellyfinMediaItem(
|
||||
id=item_id,
|
||||
name=item.get('Name'),
|
||||
type=JellyfinMediaType.AUDIO,
|
||||
image_url=get_image_url(album_id),
|
||||
start=start,
|
||||
end=end,
|
||||
metadata=metadata
|
||||
)
|
||||
52
jellyfin/utils/models/episode.py
Normal file
52
jellyfin/utils/models/episode.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from jellyfin.models import (
|
||||
JellyfinMediaItem,
|
||||
JellyfinMediaType,
|
||||
JellyfinEpisodeMediaMetadata
|
||||
)
|
||||
from jellyfin_apiclient_python import JellyfinClient
|
||||
from jellyfin.utils.playback import get_current_playback
|
||||
from jellyfin.utils.image import get_image_url
|
||||
|
||||
|
||||
def to_media_item(
|
||||
item: dict,
|
||||
client: JellyfinClient
|
||||
) -> JellyfinMediaItem:
|
||||
"""
|
||||
Converts a Jellyfin episode media item dictionary to a JellyfinMediaItem.
|
||||
|
||||
Args:
|
||||
item (dict): The Jellyfin episode media item dictionary.
|
||||
client (JellyfinClient): The Jellyfin client instance.
|
||||
Returns:
|
||||
JellyfinMediaItem: The converted JellyfinMediaItem model.
|
||||
"""
|
||||
item_id = item.get('Id')
|
||||
|
||||
# Construct SxxExx name
|
||||
series_name = item.get('SeriesName')
|
||||
season_number = item.get('ParentIndexNumber')
|
||||
episode_number = item.get('IndexNumber')
|
||||
name = f"{series_name} S{season_number:02}E{episode_number:02}"
|
||||
|
||||
# Get TV show ID (for image)
|
||||
season = client.jellyfin.get_item(item.get('ParentId'))
|
||||
show_id = season.get('ParentId')
|
||||
|
||||
# Construct metadata
|
||||
metadata = JellyfinEpisodeMediaMetadata(
|
||||
subtitle=item.get('Name'),
|
||||
)
|
||||
|
||||
# Get playback positions
|
||||
(start, end) = get_current_playback(item)
|
||||
|
||||
return JellyfinMediaItem(
|
||||
id=item_id,
|
||||
name=name,
|
||||
type=JellyfinMediaType.EPISODE,
|
||||
image_url=get_image_url(show_id),
|
||||
start=start,
|
||||
end=end,
|
||||
metadata=metadata
|
||||
)
|
||||
48
jellyfin/utils/models/movie.py
Normal file
48
jellyfin/utils/models/movie.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from jellyfin.models import (
|
||||
JellyfinMediaItem,
|
||||
JellyfinMediaType,
|
||||
JellyfinMovieMediaMetadata
|
||||
)
|
||||
from jellyfin.utils.playback import get_current_playback
|
||||
from jellyfin.utils.image import get_image_url
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def to_media_item(
|
||||
item: dict
|
||||
) -> JellyfinMediaItem:
|
||||
"""
|
||||
Converts a Jellyfin movie media item dictionary to a JellyfinMediaItem.
|
||||
|
||||
Args:
|
||||
item (dict): The Jellyfin movie media item dictionary.
|
||||
Returns:
|
||||
JellyfinMediaItem: The converted JellyfinMediaItem model.
|
||||
"""
|
||||
item_id = item.get('Id')
|
||||
|
||||
# Get name
|
||||
name = item.get('OriginalTitle')
|
||||
if not name:
|
||||
name = item.get('Name')
|
||||
|
||||
# Construct metadata
|
||||
premiere_date = item.get('PremiereDate')
|
||||
premiere_year = datetime.fromisoformat(premiere_date).year
|
||||
|
||||
metadata = JellyfinMovieMediaMetadata(
|
||||
year=premiere_year
|
||||
)
|
||||
|
||||
# Get playback positions
|
||||
(start, end) = get_current_playback(item)
|
||||
|
||||
return JellyfinMediaItem(
|
||||
id=item_id,
|
||||
name=name,
|
||||
type=JellyfinMediaType.MOVIE,
|
||||
image_url=get_image_url(item_id),
|
||||
start=start,
|
||||
end=end,
|
||||
metadata=metadata
|
||||
)
|
||||
32
jellyfin/utils/playback.py
Normal file
32
jellyfin/utils/playback.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
def get_current_playback(media: dict) -> Tuple[Optional[int], Optional[int]]:
|
||||
"""
|
||||
Extracts playback start and end positions from a Jellyfin media item dictionary.
|
||||
|
||||
Args:
|
||||
media (dict): The Jellyfin media item dictionary.
|
||||
Returns:
|
||||
Tuple[Optional[int], Optional[int]]: A tuple containing the start and end positions in seconds, or (None, None) if the media is paused.
|
||||
"""
|
||||
play_state = media.get('PlayState', {})
|
||||
is_paused = play_state.get('IsPaused', False)
|
||||
|
||||
if is_paused:
|
||||
return (None, None)
|
||||
|
||||
runtime_ticks = media.get('RunTimeTicks', -1)
|
||||
playback_position_ticks = play_state.get('PositionTicks', -1)
|
||||
|
||||
if runtime_ticks < 0 or playback_position_ticks < 0:
|
||||
return (None, None)
|
||||
|
||||
total_runtime_seconds = runtime_ticks // 10_000_000
|
||||
playback_position_seconds = playback_position_ticks // 10_000_000
|
||||
|
||||
start = int(datetime.now().timestamp()) - playback_position_seconds
|
||||
end = start + total_runtime_seconds
|
||||
|
||||
return (start, end)
|
||||
17
main.py
17
main.py
@@ -1,33 +1,32 @@
|
||||
from discord.rpc import DiscordRPC
|
||||
from jellyfin.api_client import JellyfinApiClient
|
||||
from jellyfin.utils import to_rpc_payload
|
||||
from settings import settings
|
||||
import coloredlogs
|
||||
import logging
|
||||
import time
|
||||
|
||||
|
||||
def main():
|
||||
coloredlogs.install(
|
||||
level=logging.INFO,
|
||||
fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
|
||||
discordRPC = DiscordRPC()
|
||||
jellyfinApiClient = JellyfinApiClient()
|
||||
discord_rpc = DiscordRPC()
|
||||
jellyfin_api_client = JellyfinApiClient()
|
||||
|
||||
|
||||
def main():
|
||||
while True:
|
||||
try:
|
||||
media_item = jellyfinApiClient.get_current_playback()
|
||||
media_item = jellyfin_api_client.get_current_media()
|
||||
if not media_item:
|
||||
discordRPC.clear()
|
||||
discord_rpc.clear()
|
||||
time.sleep(settings.poll_interval)
|
||||
continue
|
||||
|
||||
discordRPC.update(to_rpc_payload(media_item))
|
||||
discord_rpc.update(media_item.to_rpc_payload())
|
||||
time.sleep(settings.poll_interval)
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Shutting down...")
|
||||
discordRPC.clear()
|
||||
discord_rpc.clear()
|
||||
break
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ class Settings(BaseSettings):
|
||||
"""
|
||||
Application settings loaded from environment variables or a .env file.
|
||||
"""
|
||||
app_name: str = Field('jellydisc')
|
||||
app_version: str = Field('0.1.2')
|
||||
|
||||
jellyfin_server_url: str = Field(..., env="JELLYFIN_SERVER_URL")
|
||||
jellyfin_username: str = Field(..., env="JELLYFIN_USERNAME")
|
||||
jellyfin_password: str = Field(..., env="JELLYFIN_PASSWORD")
|
||||
|
||||
Reference in New Issue
Block a user