diff --git a/bot.py b/bot.py index 9d1cb71..07e8b69 100755 --- a/bot.py +++ b/bot.py @@ -24,19 +24,7 @@ from PIL import Image import requests from nio import AsyncClient, InviteEvent, JoinError, RoomMessageText, MatrixRoom, LoginError, RoomMemberEvent, RoomVisibility, RoomPreset, RoomCreateError, RoomResolveAliasResponse, UploadError, UploadResponse, SyncError -# Couple of custom exceptions - - -class UploadFailed(Exception): - pass - -class CommandRequiresAdmin(Exception): - pass - - -class CommandRequiresOwner(Exception): - pass - +from modules.common.exceptions import CommandRequiresAdmin, CommandRequiresOwner, UploadFailed class Bot: @@ -115,7 +103,7 @@ class Bot: except ValueError: # broken cache? self.logger.warning(f"Image cache for {url} could not be unpacked, attempting to re-upload...") 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): return await self.send_text(room, f"Sorry. Something went wrong fetching {url} and uploading the image to matrix server :(", event=event) diff --git a/modules/apod.py b/modules/apod.py index 05b92e1..cfd8a84 100644 --- a/modules/apod.py +++ b/modules/apod.py @@ -7,7 +7,7 @@ from nio import AsyncClient, UploadError from nio import UploadResponse from modules.common.module import BotModule -from bot import UploadFailed +from modules.common.exceptions import UploadFailed class Apod: diff --git a/modules/common/exceptions.py b/modules/common/exceptions.py new file mode 100644 index 0000000..933eb20 --- /dev/null +++ b/modules/common/exceptions.py @@ -0,0 +1,14 @@ +# Couple of custom exceptions + + +class UploadFailed(Exception): + pass + +class CommandRequiresAdmin(Exception): + pass + + +class CommandRequiresOwner(Exception): + pass + + diff --git a/modules/printing.py b/modules/printing.py index 9fc5902..3e5dcc6 100644 --- a/modules/printing.py +++ b/modules/printing.py @@ -5,7 +5,6 @@ import sys import traceback import cups import httpx -import asyncio import aiofiles import os diff --git a/modules/tautulli.py b/modules/tautulli.py index 89c1e3c..d45516f 100644 --- a/modules/tautulli.py +++ b/modules/tautulli.py @@ -1,81 +1,98 @@ +import time import urllib.request import urllib.parse import urllib.error -import datetime -import pytz + +import aiohttp.web +import requests import os -import sys import json - -import importlib -from importlib import reload - -from nio import MatrixRoom - -from aiohttp import web import asyncio -import nest_asyncio -nest_asyncio.apply() +from aiohttp import web +from future.moves.urllib.parse import urlencode +from nio import MatrixRoom from modules.common.module import BotModule -tautulli_path = os.getenv("TAUTULLI_PATH") +import nest_asyncio +nest_asyncio.apply() -def load_tzlocal(): - try: - global tautulli_path +rooms = dict() +global_bot = None - sys.path.insert(0, tautulli_path) - sys.path.insert(0, "{}/lib".format(tautulli_path)) - module = importlib.import_module("tzlocal") - module = reload(module) - return module - except ModuleNotFoundError: + +async def send_entry(blob, content_type, fmt_params, rooms): + for room_id in rooms: + room = MatrixRoom(room_id=room_id, own_user_id=os.getenv("BOT_OWNERS"), + encrypted=rooms[room_id]) + 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 -def load_tautulli(): - try: - global tautulli_path + width = width or 1000 + height = height or 1500 - sys.path.insert(0, tautulli_path) - sys.path.insert(0, "{}/lib".format(tautulli_path)) - module = importlib.import_module("plexpy") - module = reload(module) - return module - except ModuleNotFoundError: - return None + if img: + params = {'url': 'http://127.0.0.1:32400%s' % (img), 'width': width, 'height': height, 'format': "png"} -plexpy = load_tautulli() -tzlocal = load_tzlocal() + uri = pms_url + '/photo/:/transcode?%s' % urlencode(params) -send_entry_lock = asyncio.Lock() + headers = {'X-Plex-Token': pms_token} -async def send_entry(bot, room, entry): - global send_entry_lock - async with send_entry_lock: - if "art" in entry: - global plexpy - if plexpy: - 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) + session = requests.Session() + try: + r = session.request("GET", uri, headers=headers) + r.raise_for_status() + except Exception: + return None - 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"]) - } + response_status = r.status_code + response_content = r.content + response_headers = r.headers + if response_status in (200, 201): + return response_content, response_headers['Content-Type'] + + +def get_from_entry(entry): + 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 = """ {title} -({year})- Rating: {audience_rating}
@@ -94,25 +111,29 @@ msg_template_plain = """*{title} -({year})- Rating: {audience_rating}* """ -class WebServer: - bot = None - rooms = dict() +def _get_loop(): + try: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + except RuntimeError: + loop = asyncio.get_event_loop() + finally: + return loop + + +class WebServer: def __init__(self, host, port): - self.app = web.Application() self.host = host self.port = port + self.app = web.Application() self.app.router.add_post('/notify', self.notify) - async def run(self): + def run(self): if not self.host or not self.port: return - loop = asyncio.get_event_loop() - runner = web.AppRunner(self.app) - loop.run_until_complete(runner.setup()) - site = web.TCPSite(runner, host=self.host, port=self.port) - loop.run_until_complete(site.start()) + aiohttp.web.run_app(self.app, host=self.host, port=self.port, handle_signals=True, loop=asyncio.get_running_loop()) async def notify(self, request: web.Request) -> web.Response: try: @@ -126,9 +147,9 @@ class WebServer: if "directors" in data: data["directors"] = data["directors"].split(",") - for room_id in self.rooms: - room = MatrixRoom(room_id=room_id, own_user_id=os.getenv("BOT_OWNERS"), encrypted=self.rooms[room_id]) - await send_entry(self.bot, room, data) + global rooms + (blob, content_type, fmt_params) = get_from_entry(data) + await send_entry(blob, content_type, fmt_params, rooms) except Exception as exc: message = str(exc) @@ -136,6 +157,7 @@ class WebServer: return web.Response() + class MatrixModule(BotModule): httpd = None rooms = dict() @@ -143,29 +165,16 @@ class MatrixModule(BotModule): def __init__(self, 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")) - loop = asyncio.get_event_loop() - loop.run_until_complete(self.httpd.run()) def matrix_start(self, bot): super().matrix_start(bot) - self.httpd.bot = bot + global global_bot + global_bot = bot + self.httpd.run() + + def matrix_stop(self, bot): + super().matrix_stop(bot) async def matrix_message(self, bot, room, event): args = event.body.split() @@ -183,7 +192,7 @@ class MatrixModule(BotModule): try: 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() entries = json.loads(connection) if "response" not in entries and "data" not in entries["response"] and "recently_added" not in entries["response"]["data"]: @@ -191,7 +200,8 @@ class MatrixModule(BotModule): return 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: raise ValueError(err.read()) @@ -210,7 +220,8 @@ class MatrixModule(BotModule): await bot.send_text(room, f"Removed {room_id} to rooms notification list") bot.save_settings() - self.httpd.rooms = self.rooms + global rooms + rooms = self.rooms else: await bot.send_text(room, 'Usage: !tautulli | %room_id% %encrypted%') else: @@ -220,16 +231,19 @@ class MatrixModule(BotModule): data = super().get_settings() data["api_key"] = self.api_key data["rooms"] = self.rooms - self.httpd.rooms = self.rooms + global rooms + rooms = self.rooms return data def set_settings(self, data): super().set_settings(data) if data.get("rooms"): self.rooms = data["rooms"] - self.httpd.rooms = self.rooms + global rooms + rooms = self.rooms if data.get("api_key"): self.api_key = data["api_key"] def help(self): return ('Tautulli recently added bot') +