import asyncio import logging import slixmpp import requests import json import datetime import time import configparser import os import threading from jellyfin_apiclient_python import JellyfinClient import uuid def json_to_key_value_string(json_data): # Parse JSON data data = json.loads(json_data) # Initialize an empty list to store key-value pairs result_lines = [] # Recursively process the JSON data def process_item(item, prefix=''): if isinstance(item, dict): for key, value in item.items(): if isinstance(value, (dict, list)): process_item(value, f"{prefix}{key}.") else: result_lines.append(f"{prefix}{key}: {value}") elif isinstance(item, list): for i, value in enumerate(item): process_item(value, f"{prefix}[{i}].") process_item(data) return "\n".join(result_lines) class QBBot(slixmpp.ClientXMPP): def __init__(self, jid, password, room, nick, api_url, api_username, api_password): slixmpp.ClientXMPP.__init__(self, jid, password) self.room = room self.nick = nick # Initialize Jellyfin caller self.jf_api = None self.jf_api_url = api_url self.jf_api_username = api_username self.jf_api_password = api_password self.jf_api_credentials = None self.jf_api_init() self.jf_api_login() self.media_dictionary = {} self.init_dictionary() self.add_event_handler("session_start", self.session_start) self.add_event_handler("message", self.message) # The groupchat_message event is triggered whenever a message # stanza is received from any chat room. If you also also # register a handler for the 'message' event, MUC messages # will be processed by both handlers. self.add_event_handler("groupchat_message", self.muc_message) # If you wanted more functionality, here's how to register plugins: # self.register_plugin('xep_0030') # Service Discovery # self.register_plugin('xep_0199') # XMPP Ping # Here's how to access plugins once you've registered them: # self['xep_0030'].add_feature('echo_demo') def init_dictionary(self): # put media type conversions here (don't know the pythonic way to do it) self.media_dictionary['tv'] = "Series" self.media_dictionary['movie'] = "Movie" self.media_dictionary['movies'] = "Movie" self.media_dictionary['music'] = "MusicAlbum" def jf_api_init(self): if self.jf_api is not None: print("Warning: API already initialized, doing nothing") return self.jf_api = JellyfinClient() self.jf_api.config.app('xmpp-jellyfin-bot', '0.0.1', 'xmpp-jellyfin-bot', uuid.getnode()) self.jf_api.config.data["auth.ssl"] = False def jf_api_login(self): if self.jf_api_credentials is None: self.jf_api.auth.connect_to_address(self.jf_api_url) self.jf_api.auth.login(self.jf_api_url, self.jf_api_username, self.jf_api_password) self.jf_api_credentials = self.jf_api.auth.credentials.get_credentials() else: self.jf_api.authenticate({"Servers": [self.jf_api_credentials]}, discover=False) async def session_start(self, event): await self.get_roster() self.send_presence() self.plugin['xep_0045'].join_muc(self.room, self.nick) avatar_file = "./bartonfink.png" try: avatar_file = open(avatar_file, 'rb') except IOError: logging.error("Could not find avatar file") return self.disconnect() avatar = avatar_file.read() avatar_type = 'image/png' avatar_id = self['xep_0084'].generate_id(avatar) avatar_bytes = len(avatar) avatar_file.close() used_xep84 = False logging.info('Publish XEP-0084 avatar data') result = await self['xep_0084'].publish_avatar(avatar) if isinstance(result, slixmpp.exceptions.XMPPError): logging.warning('Could not publish XEP-0084 avatar') else: used_xep84 = True logging.info('Update vCard with avatar') result = await self['xep_0153'].set_avatar(avatar=avatar, mtype=avatar_type) if isinstance(result, slixmpp.exceptions.XMPPError): print('Could not set vCard avatar') if used_xep84: logging.info('Advertise XEP-0084 avatar metadata') result = await self['xep_0084'].publish_avatar_metadata([ {'id': avatar_id, 'type': avatar_type, 'bytes': avatar_bytes} # We could advertise multiple avatars to provide # options in image type, source (HTTP vs pubsub), # size, etc. # {'id': ....} ]) if isinstance(result, slixmpp.exceptions.XMPPError): logging.warning('Could not publish XEP-0084 metadata') logging.info('Wait for presence updates to propagate...') #self.schedule('end', 5, self.disconnect, kwargs={'wait': True}) # Most get_*/set_* methods from plugins use Iq stanzas, which # are sent asynchronously. You can almost always provide a # callback that will be executed when the reply is received. def message(self, msg): #query = self.jf_api.jellyfin.search_media_items(term="dream theater distance over time") if msg['type'] in ('chat', 'normal'): msg.reply("I was not designed to take direct messages, please use me in a MUC").send() def muc_message(self, msg): # TODO if msg['mucnick'] != self.nick and msg['body'].startswith(self.nick): message = "```\n" tokens = msg['body'].split() match tokens[1]: case "help": message += "XMPP bot to query a Jellyfin server for content\n" message += "Please use this bot first if you want to download something but are not sure if it's on the server\n" message += "Commands\n" message += "help: Displays this help\n" message += "search CATEGORY SEARCH_QUERY: Search CATEGORY for SEARCH_QUERY on the Jellyfin server\n" case "search": category = tokens[2] category = category.lower() category = self.media_dictionary[category] if category in self.media_dictionary.keys() else category if category not in ["Movie", "Series", "MusicAlbum"]: message += "Error: invalid category " + tokens[2] else: search_query = " ".join(tokens[3:]) query = self.jf_api.jellyfin.search_media_items(term=search_query, media=category) items = query['Items'] #items = [i for i in items if i['MediaType'] != "Unknown"] if len(items) == 0: message += f"{search_query} not found in category {category}\n" match category: case "Movie": for i in items: message += f"{i['Name']}, {i['ProductionYear']}, {i['OfficialRating'] if 'OfficialRating' in i else "unknown rating"}, {i['Container']}\n" case "Series": for i in items: message += f"{i['Name']}, {i['ProductionYear']}, {i['OfficialRating'] if 'OfficialRating' in i else "unknown rating"}, No. Episodes: {i['UserData']['UnplayedItemCount']}\n" case "MusicAlbum": for i in items: message += f"{i['AlbumArtist'] if 'AlbumArtist' in i.keys() else "Unknown"} - {i['Name']}, {i['ProductionYear'] if 'ProductionYear' in i.keys() else "Unknown"}\n" message += "```" self.send_message(mto=msg['from'].bare, mbody=message, mtype='groupchat') return 0 if __name__ == '__main__': # Ideally use optparse or argparse to get JID, # password, and log level. logging.basicConfig(level=logging.INFO, format='%(levelname)-8s %(message)s') config = configparser.ConfigParser() if not os.path.exists("./config.conf"): logging.error("Could not find ./config.conf, please ensure it exists") exit config.read('config.conf') config_default = config['DEFAULT'] xmpp = QBBot(config_default['jf_jid'].strip('\"'), config_default['jf_pass'].strip('\"'), config_default['jf_muc'].strip('\"'), config_default['jf_nick'].strip('\"'), config_default['api_url'].strip('\"'), config_default['api_user'].strip('\"'), config_default['api_pass'].strip('\"')) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0045') # Multi-User Chat xmpp.register_plugin('xep_0199') # XMPP Ping xmpp.register_plugin('xep_0084') # vCard avatar xmpp.register_plugin('xep_0153') # Something required for vCard xmpp.connect() xmpp.process()