Initial commit. Movies work, TV and music do not

This commit is contained in:
j4nk 2025-02-16 00:42:42 -05:00
parent 393584bf8f
commit 43f0204a68
4 changed files with 228 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
config.conf
*.jpg
*~
#*
bartonfink.png

215
bot.py Normal file
View File

@ -0,0 +1,215 @@
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.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 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):
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()
if category not in ["movie", "tv", "music"]:
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}"
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"
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()

8
config.conf.template Normal file
View File

@ -0,0 +1,8 @@
[DEFAULT]
qb_jid = # JID for the bot
qb_pass = # Password for the bot
qb_muc = # MUC the bot should connect to
qb_nick = # Nickname for the bot in the MUC
api_url = # URL for the qbittorrent api we are connecting to
api_user = # Username for the qbittorrent api
api_pass = # Password for the qbittorent api

BIN
jellyfin_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB