From 265e646d96a8571622e42bf6d2038d03fb488d4e Mon Sep 17 00:00:00 2001 From: Zvonimir Rudinski Date: Tue, 9 Dec 2025 23:48:05 +0100 Subject: [PATCH] add jellyfin api client --- discord/rpc.py | 4 +- jellyfin.py | 45 -------------------- jellyfin/api_client.py | 95 ++++++++++++++++++++++++++++++++++++++++++ jellyfin/models.py | 13 ++++++ main.py | 52 ++++------------------- 5 files changed, 119 insertions(+), 90 deletions(-) delete mode 100644 jellyfin.py create mode 100644 jellyfin/api_client.py create mode 100644 jellyfin/models.py diff --git a/discord/rpc.py b/discord/rpc.py index fb3d62f..1dedbdf 100644 --- a/discord/rpc.py +++ b/discord/rpc.py @@ -1,6 +1,6 @@ from pypresence import Presence from settings import settings -from discord.models import DiscordRPCPayload +from discord.models import DiscordRPCUpdatePayload import logging class DiscordRPC: @@ -12,7 +12,7 @@ class DiscordRPC: self.rpc.connect() self.logger.info("Connected to Discord RPC.") - def update(self, payload: DiscordRPCPayload): + def update(self, payload: DiscordRPCUpdatePayload): self.logger.info("Updating Discord RPC presence...") self.rpc.update( activity_type=payload.activity_type, diff --git a/jellyfin.py b/jellyfin.py deleted file mode 100644 index ed877ff..0000000 --- a/jellyfin.py +++ /dev/null @@ -1,45 +0,0 @@ -from jellyfin_apiclient_python import JellyfinClient -from getmac import get_mac_address -import os - -def get_client(): - server_address = os.getenv('JELLYFIN_ADDRESS') - username = os.getenv('JELLYFIN_USER') - password = os.getenv('JELLYFIN_PASSWORD') - host_mac = get_mac_address(hostname="localhost") - - client = JellyfinClient() - client.config.app('jellydisc', '0.0.1', os.uname().nodename, host_mac) - client.config.data['auth.ssl'] = True - - client.auth.connect_to_address(server_address) - client.auth.login(server_address, username, password) - - return client - -def get_active_media(client): - server_address = os.getenv('JELLYFIN_ADDRESS') - sessions = client.jellyfin.get_sessions() - - for session in sessions: - # Check if there is a NowPlayingItem - media = session.get('NowPlayingItem') - if not media: - continue - - # Skip non-audio media - media_type = media.get('Type') - if media_type != 'Audio': - continue - - media_id = media.get('Id') - image = f"{server_address}/Items/{media_id}/Images/Primary?maxWidth=300&maxHeight=300" - - return { - 'id': media_id, - 'artist': media.get('AlbumArtist', 'Unknown Artist'), - 'title': media.get('Name', 'Unknown Title'), - 'image': image, - } - - return None diff --git a/jellyfin/api_client.py b/jellyfin/api_client.py new file mode 100644 index 0000000..342b6b1 --- /dev/null +++ b/jellyfin/api_client.py @@ -0,0 +1,95 @@ +from settings import settings +from jellyfin_apiclient_python import JellyfinClient +from getmac import get_mac_address +from jellyfin.models import JellyfinMediaItem, JellyfinMediaType +import logging +import os + +class JellyfinApiClient: + def __init__(self): + 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.0.1', machine_name, unique_id) + self.client.config.data['auth.ssl'] = True + + self.client.auth.connect_to_address(settings.jellyfin_server_url) + self.client.auth.login( + settings.jellyfin_server_url, + settings.jellyfin_username, + settings.jellyfin_password + ) + + self.logger.info("Connected to Jellyfin server.") + + def get_current_playback(self) -> JellyfinMediaItem | None: + self.logger.info("Fetching current playback information...") + sessions = self.client.jellyfin.get_sessions() + + if not sessions: + self.logger.info("No active playback found.") + return None + + for session in sessions: + if session.get('NowPlayingItem'): + self.logger.info("Current playback information fetched.") + return self.to_model(session['NowPlayingItem']) + + self.logger.info("No active playback found.") + return None + + def get_image_url(self, media_id: str) -> str: + 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: + media_type = item.get('Type') + + if media_type == 'Audio': + return self.to_music_model(item) + elif media_type == 'Episode': + return self.to_episode_model(item) + elif media_type == 'Movie': + return self.to_movie_model(item) + + raise ValueError(f"Unsupported media type: {media_type}") + + def to_music_model(self, item: dict) -> JellyfinMediaItem: + media_id = item.get('Id') + + return JellyfinMediaItem( + name=item.get('Name'), + type=JellyfinMediaType.AUDIO, + image_url=self.get_image_url(media_id), + metadata={ + 'artist': item.get('AlbumArtist'), + } + ) + + def to_movie_model(self, item: dict) -> JellyfinMediaItem: + media_id = item.get('Id') + + return JellyfinMediaItem( + name=item.get('Name'), + type=JellyfinMediaType.MOVIE, + image_url=self.get_image_url(media_id), + metadata={} + ) + + def to_episode_model(self, item: dict) -> JellyfinMediaItem: + media_id = item.get('Id') + + return JellyfinMediaItem( + name=item.get('Name'), + type=JellyfinMediaType.EPISODE, + image_url=self.get_image_url(media_id), + metadata={ + 'series': item.get('SeriesName'), + 'season': item.get('ParentIndexNumber'), + 'episode': item.get('IndexNumber'), + } + ) diff --git a/jellyfin/models.py b/jellyfin/models.py new file mode 100644 index 0000000..05754d1 --- /dev/null +++ b/jellyfin/models.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel +from enum import Enum + +class JellyfinMediaType(str, Enum): + AUDIO = 'Audio' + MOVIE = 'Movie' + EPISODE = 'Episode' + +class JellyfinMediaItem(BaseModel): + name: str + type: JellyfinMediaType + image_url: str + metadata: dict diff --git a/main.py b/main.py index 4c3746f..306df5f 100644 --- a/main.py +++ b/main.py @@ -1,46 +1,12 @@ -from dotenv import load_dotenv -from jellyfin import get_client, get_active_media -from discord import get_rpc -from pypresence.types import ActivityType -import time +from discord.rpc import DiscordRPC +from jellyfin.api_client import JellyfinApiClient +import logging -load_dotenv() +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') -rpc = get_rpc() -client = get_client() +discordRPC = DiscordRPC() +jellyfinApiClient = JellyfinApiClient() -def generate_media_id(media): - return f"{media['artist']}-{media['title']}" - -def main_loop(last_media_id): - media = get_active_media(client) - - if media is None: - print("No active media found.") - rpc.clear() - return None - - media_id = generate_media_id(media) - - if media_id != last_media_id: - print(f"Updating Discord RPC: Listening to {media['title']} by {media['artist']}") - - rpc.update( - activity_type=ActivityType.LISTENING, - state=f"by {media['artist']}", - details=f"Listening to {media['title']}", - large_image=media['image'], - large_text=media['title'], - ) - return media_id - else: - print("No change in media. Skipping update.") - return last_media_id - - -if __name__ == "__main__": - last_media_id = None - print("Starting Jellyfin Discord Rich Presence...") - while True: - last_media_id = main_loop(last_media_id) - time.sleep(15) +print(jellyfinApiClient.get_current_playback())