Merge pull request #213 from aspacca/fix_double_listening

fix Bot running twice and refactor tautulli module
This commit is contained in:
Ville Ranki 2022-08-16 22:43:55 +03:00 committed by GitHub
commit 06a242470b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 121 additions and 109 deletions

View File

@ -556,13 +556,15 @@ Commands:
Tautulli instance and API Key: Tautulli instance and API Key:
The module work with an existing installed instance of Tautulli accessible on the machine at the path defined by env variable `TAUTULLI_PATH` The module work with an instance of Tautulli accessible on URL defined by env variable `TAUTULLI_URL`
In order to load art pictures you need to define an instance of Plex Media Server with its token as well by the two env variables `PLEX_MEDIA_SERVER_URL` and `PLEX_MEDIA_SERVER_TOKEN`
You have to provide an api key by using the relevant command. You have to provide an api key by using the relevant command.
Environ variables seen by command: Environ variables seen by command:
* TAUTULLI_PATH: Path accessible from the machine to the installed instance of Tautulli * PLEX_MEDIA_SERVER_URL: Url accessible from the machine to an instance of Plex Media Server
* TAUTULLI_URL: Url accessible from the machine to the installed instance of Tautulli * PLEX_MEDIA_SERVER_TOKEN: Plex Token for the instance of Plex Media Server
* TAUTULLI_URL: Url accessible from the machine to an instance of Tautulli
* TAUTULLI_NOTIFIER_ADDR: Listening address for the Tautulli webhook handler target * TAUTULLI_NOTIFIER_ADDR: Listening address for the Tautulli webhook handler target
* TAUTULLI_NOTIFIER_PORT: Listening port for the Tautulli webhook handler target * TAUTULLI_NOTIFIER_PORT: Listening port for the Tautulli webhook handler target
* BOT_OWNERS: Owner of the rooms in the list for the notification webhook * BOT_OWNERS: Owner of the rooms in the list for the notification webhook

16
bot.py
View File

@ -24,19 +24,7 @@ from PIL import Image
import requests import requests
from nio import AsyncClient, InviteEvent, JoinError, RoomMessageText, MatrixRoom, LoginError, RoomMemberEvent, RoomVisibility, RoomPreset, RoomCreateError, RoomResolveAliasResponse, UploadError, UploadResponse, SyncError from nio import AsyncClient, InviteEvent, JoinError, RoomMessageText, MatrixRoom, LoginError, RoomMemberEvent, RoomVisibility, RoomPreset, RoomCreateError, RoomResolveAliasResponse, UploadError, UploadResponse, SyncError
# Couple of custom exceptions from modules.common.exceptions import CommandRequiresAdmin, CommandRequiresOwner, UploadFailed
class UploadFailed(Exception):
pass
class CommandRequiresAdmin(Exception):
pass
class CommandRequiresOwner(Exception):
pass
class Bot: class Bot:
@ -115,7 +103,7 @@ class Bot:
except ValueError: # broken cache? except ValueError: # broken cache?
self.logger.warning(f"Image cache for {url} could not be unpacked, attempting to re-upload...") self.logger.warning(f"Image cache for {url} could not be unpacked, attempting to re-upload...")
try: try:
matrix_uri, mimetype, w, h, size = await self.upload_image(url, event=event, blob=blob, no_cache=no_cache) matrix_uri, mimetype, w, h, size = await self.upload_image(url, blob=blob, no_cache=no_cache)
except (UploadFailed, ValueError): except (UploadFailed, ValueError):
return await self.send_text(room, f"Sorry. Something went wrong fetching {url} and uploading the image to matrix server :(", event=event) return await self.send_text(room, f"Sorry. Something went wrong fetching {url} and uploading the image to matrix server :(", event=event)

View File

@ -7,7 +7,7 @@ from nio import AsyncClient, UploadError
from nio import UploadResponse from nio import UploadResponse
from modules.common.module import BotModule from modules.common.module import BotModule
from bot import UploadFailed from modules.common.exceptions import UploadFailed
class Apod: class Apod:

View File

@ -0,0 +1,14 @@
# Couple of custom exceptions
class UploadFailed(Exception):
pass
class CommandRequiresAdmin(Exception):
pass
class CommandRequiresOwner(Exception):
pass

View File

@ -5,7 +5,6 @@ import sys
import traceback import traceback
import cups import cups
import httpx import httpx
import asyncio
import aiofiles import aiofiles
import os import os

View File

@ -1,81 +1,98 @@
import time
import urllib.request import urllib.request
import urllib.parse import urllib.parse
import urllib.error import urllib.error
import datetime
import pytz import aiohttp.web
import requests
import os import os
import sys
import json import json
import importlib
from importlib import reload
from nio import MatrixRoom
from aiohttp import web
import asyncio import asyncio
import nest_asyncio from aiohttp import web
nest_asyncio.apply() from future.moves.urllib.parse import urlencode
from nio import MatrixRoom
from modules.common.module import BotModule from modules.common.module import BotModule
tautulli_path = os.getenv("TAUTULLI_PATH") import nest_asyncio
nest_asyncio.apply()
def load_tzlocal(): rooms = dict()
try: global_bot = None
global tautulli_path
sys.path.insert(0, tautulli_path)
sys.path.insert(0, "{}/lib".format(tautulli_path)) async def send_entry(blob, content_type, fmt_params, rooms):
module = importlib.import_module("tzlocal") for room_id in rooms:
module = reload(module) room = MatrixRoom(room_id=room_id, own_user_id=os.getenv("BOT_OWNERS"),
return module encrypted=rooms[room_id])
except ModuleNotFoundError: if blob and content_type:
await global_bot.upload_and_send_image(room, blob, text="", blob=True, blob_content_type=content_type)
await global_bot.send_html(room, msg_template_html.format(**fmt_params),
msg_template_plain.format(**fmt_params))
def get_image(img=None, width=1000, height=1500):
"""
Return image data as array.
Array contains the image content type and image binary
Parameters required: img { Plex image location }
Optional parameters: width { the image width }
height { the image height }
Output: array
"""
pms_url = os.getenv("PLEX_MEDIA_SERVER_URL")
pms_token = os.getenv("PLEX_MEDIA_SERVER_TOKEN")
if not pms_url or not pms_token:
return None return None
def load_tautulli(): width = width or 1000
try: height = height or 1500
global tautulli_path
sys.path.insert(0, tautulli_path) if img:
sys.path.insert(0, "{}/lib".format(tautulli_path)) params = {'url': 'http://127.0.0.1:32400%s' % (img), 'width': width, 'height': height, 'format': "png"}
module = importlib.import_module("plexpy")
module = reload(module)
return module
except ModuleNotFoundError:
return None
plexpy = load_tautulli() uri = pms_url + '/photo/:/transcode?%s' % urlencode(params)
tzlocal = load_tzlocal()
send_entry_lock = asyncio.Lock() headers = {'X-Plex-Token': pms_token}
async def send_entry(bot, room, entry): session = requests.Session()
global send_entry_lock try:
async with send_entry_lock: r = session.request("GET", uri, headers=headers)
if "art" in entry: r.raise_for_status()
global plexpy except Exception:
if plexpy: return None
pms = plexpy.pmsconnect.PmsConnect()
pms_image = pms.get_image(entry["art"], 600, 300)
if pms_image:
(blob, content_type) = pms_image
await bot.upload_and_send_image(room, blob, "", True, content_type)
fmt_params = { response_status = r.status_code
"title": entry["title"], response_content = r.content
"year": entry["year"], response_headers = r.headers
"audience_rating": entry["audience_rating"], if response_status in (200, 201):
"directors": ", ".join(entry["directors"]), return response_content, response_headers['Content-Type']
"actors": ", ".join(entry["actors"]),
"summary": entry["summary"],
"tagline": entry["tagline"], def get_from_entry(entry):
"genres": ", ".join(entry["genres"]) blob = None
} content_type = ""
if "art" in entry:
pms_image = get_image(entry["art"], 600, 300)
if pms_image:
(blob, content_type) = pms_image
fmt_params = {
"title": entry["title"],
"year": entry["year"],
"audience_rating": entry["audience_rating"],
"directors": ", ".join(entry["directors"]),
"actors": ", ".join(entry["actors"]),
"summary": entry["summary"],
"tagline": entry["tagline"],
"genres": ", ".join(entry["genres"])
}
return (blob, content_type, fmt_params)
await bot.send_html(room,
msg_template_html.format(**fmt_params),
msg_template_plain.format(**fmt_params))
msg_template_html = """ msg_template_html = """
<b>{title} -({year})- Rating: {audience_rating}</b><br> <b>{title} -({year})- Rating: {audience_rating}</b><br>
@ -94,14 +111,12 @@ msg_template_plain = """*{title} -({year})- Rating: {audience_rating}*
""" """
class WebServer:
bot = None
rooms = dict()
class WebServer:
def __init__(self, host, port): def __init__(self, host, port):
self.app = web.Application()
self.host = host self.host = host
self.port = port self.port = port
self.app = web.Application()
self.app.router.add_post('/notify', self.notify) self.app.router.add_post('/notify', self.notify)
async def run(self): async def run(self):
@ -126,9 +141,9 @@ class WebServer:
if "directors" in data: if "directors" in data:
data["directors"] = data["directors"].split(",") data["directors"] = data["directors"].split(",")
for room_id in self.rooms: global rooms
room = MatrixRoom(room_id=room_id, own_user_id=os.getenv("BOT_OWNERS"), encrypted=self.rooms[room_id]) (blob, content_type, fmt_params) = get_from_entry(data)
await send_entry(self.bot, room, data) await send_entry(blob, content_type, fmt_params, rooms)
except Exception as exc: except Exception as exc:
message = str(exc) message = str(exc)
@ -136,6 +151,7 @@ class WebServer:
return web.Response() return web.Response()
class MatrixModule(BotModule): class MatrixModule(BotModule):
httpd = None httpd = None
rooms = dict() rooms = dict()
@ -143,29 +159,17 @@ class MatrixModule(BotModule):
def __init__(self, name): def __init__(self, name):
super().__init__(name) super().__init__(name)
global plexpy
if plexpy:
global tautulli_path
plexpy.FULL_PATH = "{}/Tautulli.py".format(tautulli_path)
plexpy.PROG_DIR = os.path.dirname(plexpy.FULL_PATH)
plexpy.DATA_DIR = tautulli_path
plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, plexpy.database.FILENAME)
try:
plexpy.SYS_TIMEZONE = tzlocal.get_localzone()
except (pytz.UnknownTimeZoneError, LookupError, ValueError) as e:
plexpy.SYS_TIMEZONE = pytz.UTC
plexpy.SYS_UTC_OFFSET = datetime.datetime.now(plexpy.SYS_TIMEZONE).strftime('%z')
plexpy.initialize("{}/config.ini".format(tautulli_path))
self.httpd = WebServer(os.getenv("TAUTULLI_NOTIFIER_ADDR"), os.getenv("TAUTULLI_NOTIFIER_PORT")) self.httpd = WebServer(os.getenv("TAUTULLI_NOTIFIER_ADDR"), os.getenv("TAUTULLI_NOTIFIER_PORT"))
loop = asyncio.get_event_loop()
loop.run_until_complete(self.httpd.run())
def matrix_start(self, bot): def matrix_start(self, bot):
super().matrix_start(bot) super().matrix_start(bot)
self.httpd.bot = bot global global_bot
global_bot = bot
loop = asyncio.get_event_loop()
loop.run_until_complete(self.httpd.run())
def matrix_stop(self, bot):
super().matrix_stop(bot)
async def matrix_message(self, bot, room, event): async def matrix_message(self, bot, room, event):
args = event.body.split() args = event.body.split()
@ -183,7 +187,7 @@ class MatrixModule(BotModule):
try: try:
url = "{}/api/v2?apikey={}&cmd=get_recently_added&count=10".format(os.getenv("TAUTULLI_URL"), self.api_key) url = "{}/api/v2?apikey={}&cmd=get_recently_added&count=10".format(os.getenv("TAUTULLI_URL"), self.api_key)
req = urllib.request.Request(url+"&media_type="+media_type) req = urllib.request.Request(url + "&media_type=" + media_type)
connection = urllib.request.urlopen(req).read() connection = urllib.request.urlopen(req).read()
entries = json.loads(connection) entries = json.loads(connection)
if "response" not in entries and "data" not in entries["response"] and "recently_added" not in entries["response"]["data"]: if "response" not in entries and "data" not in entries["response"] and "recently_added" not in entries["response"]["data"]:
@ -191,7 +195,8 @@ class MatrixModule(BotModule):
return return
for entry in entries["response"]["data"]["recently_added"]: for entry in entries["response"]["data"]["recently_added"]:
await send_entry(bot, room, entry) (blob, content_type, fmt_params) = get_from_entry(entry)
await send_entry(blob, content_type, fmt_params, {room.room_id: room})
except urllib.error.HTTPError as err: except urllib.error.HTTPError as err:
raise ValueError(err.read()) raise ValueError(err.read())
@ -210,7 +215,8 @@ class MatrixModule(BotModule):
await bot.send_text(room, f"Removed {room_id} to rooms notification list") await bot.send_text(room, f"Removed {room_id} to rooms notification list")
bot.save_settings() bot.save_settings()
self.httpd.rooms = self.rooms global rooms
rooms = self.rooms
else: else:
await bot.send_text(room, 'Usage: !tautulli <movie|show|artist>|<add|remove> %room_id% %encrypted%') await bot.send_text(room, 'Usage: !tautulli <movie|show|artist>|<add|remove> %room_id% %encrypted%')
else: else:
@ -220,16 +226,19 @@ class MatrixModule(BotModule):
data = super().get_settings() data = super().get_settings()
data["api_key"] = self.api_key data["api_key"] = self.api_key
data["rooms"] = self.rooms data["rooms"] = self.rooms
self.httpd.rooms = self.rooms global rooms
rooms = self.rooms
return data return data
def set_settings(self, data): def set_settings(self, data):
super().set_settings(data) super().set_settings(data)
if data.get("rooms"): if data.get("rooms"):
self.rooms = data["rooms"] self.rooms = data["rooms"]
self.httpd.rooms = self.rooms global rooms
rooms = self.rooms
if data.get("api_key"): if data.get("api_key"):
self.api_key = data["api_key"] self.api_key = data["api_key"]
def help(self): def help(self):
return ('Tautulli recently added bot') return ('Tautulli recently added bot')