Compare commits

...

7 Commits

11 changed files with 262 additions and 181 deletions

View File

@@ -1,4 +1,5 @@
# 0.1.2
- Extracted utility functions from JellyfinApiClient to JellyfinUtils
- 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

View File

@@ -1,9 +1,16 @@
from settings import settings
from jellyfin_apiclient_python import JellyfinClient
from jellyfin.models import JellyfinMediaItem, JellyfinMediaType, JellyfinMusicMediaMetadata, JellyfinMovieMediaMetadata, JellyfinEpisodeMediaMetadata
from jellyfin.utils import JellyfinUtils
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
@@ -26,7 +33,7 @@ class JellyfinApiClient:
self.logger.info("Connecting to Jellyfin server...")
self.client = JellyfinClient()
JellyfinUtils.configure_client(self.client)
configure_client(self.client)
self.authenticate()
self.logger.info("Connected to Jellyfin server.")
@@ -45,9 +52,9 @@ class JellyfinApiClient:
self.last_auth_time = time.time()
self.logger.info("Authenticated with Jellyfin server.")
def get_current_playback(self) -> Optional[JellyfinMediaItem]:
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:
Optional[JellyfinMediaItem]: The current playback media item or None if no active playback is found.
@@ -74,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.
@@ -98,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)

View File

@@ -17,7 +17,7 @@ class JellyfinMusicMediaMetadata(BaseModel):
class JellyfinMovieMediaMetadata(BaseModel):
date: Optional[str]
year: Optional[int]
class JellyfinEpisodeMediaMetadata(BaseModel):
@@ -58,7 +58,7 @@ class JellyfinMediaItem(BaseModel):
return DiscordRPCUpdatePayload(
id=self.id,
title=f"Watching {self.name}",
subtitle=self.metadata.date,
subtitle=str(self.metadata.year),
image_url=str(self.image_url),
details=self.name,
activity_type=ActivityType.WATCHING,

View File

@@ -1,37 +0,0 @@
from jellyfin_apiclient_python import JellyfinClient
from getmac import get_mac_address
from settings import settings
import os
class JellyfinUtils:
@staticmethod
def configure_client(client: JellyfinClient):
client.config.app(
settings.app_name,
settings.app_version,
JellyfinUtils.get_machine_name(),
JellyfinUtils.get_unique_id()
)
client.config.data['auth.ssl'] = settings.jellyfin_server_url.startswith(
'https://')
@staticmethod
def get_machine_name() -> str:
"""
Retrieves the machine name of the current host.
Returns:
str: The machine name.
"""
return os.uname().nodename
@staticmethod
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")

41
jellyfin/utils/config.py Normal file
View 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
View 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"

View 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
)

View 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
)

View 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
)

View 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)

View File

@@ -16,7 +16,7 @@ def main():
while True:
try:
media_item = jellyfin_api_client.get_current_playback()
media_item = jellyfin_api_client.get_current_media()
if not media_item:
discord_rpc.clear()
time.sleep(settings.poll_interval)