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