237 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			237 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 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()
 |