Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extensible commands and abstraction of utility functions #91

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions buttons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
from util import xlate
from telegram import InlineKeyboardButton


class NavButtons(object):
def prev(self, cid, i):
return InlineKeyboardButton(
xlate("prev_button"), callback_data=f"{cid}^^^{i}^^^prev"
)

def next(self, cid, i):
return InlineKeyboardButton(
xlate("next_button"), callback_data=f"{cid}^^^{i}^^^next"
)

def done(self, cid, i):
return InlineKeyboardButton(
xlate("done"), callback_data=f"{cid}^^^{i}^^^done"
)


class ExternalButtons(object):
def imdb(self, r):
return InlineKeyboardButton(
"IMDb", url=f"https://imdb.com/title/{r['imdbId']}"
)

def tvdb(self, r):
return InlineKeyboardButton(
"tvdb", url=f"https://thetvdb.com/series/{r['titleSlug']}"
)

def tmdb(self, r):
return InlineKeyboardButton(
"TMDB", url=f"https://www.themoviedb.org/movie/{r['tmdbId']}"
)

def link(self, link):
return InlineKeyboardButton(
link["name"], url=link["url"]
)


class ActionButtons(object):
def add(self, kind, cid, i):
return InlineKeyboardButton(
xlate("add_button", kind=xlate(kind).title()),
callback_data=f"{cid}^^^{i}^^^add",
)

def already_added(self, cid, i):
return InlineKeyboardButton(
xlate("already_added_button"),
callback_data=f"{cid}^^^{i}^^^noop",
)

def cancel(self, cid, i):
return InlineKeyboardButton(
xlate("cancel_search_button"),
callback_data=f"{cid}^^^{i}^^^cancel",
)

def series_anime(self, cid, i):
return InlineKeyboardButton(
xlate("add_series_anime_button"),
callback_data=f"{cid}^^^{i}^^^add^^st=a",
)


class AddButtons(object):
def tag(self, tag, cid, i):
return InlineKeyboardButton(
xlate("add_tag_button", tag=tag["label"]),
callback_data=f"{cid}^^^{i}^^^add^^tt={tag['id']}",
)

def finished_tagging(self, cid, i):
return InlineKeyboardButton(
xlate("finished_tagging_button"),
callback_data=f"{cid}^^^{i}^^^add^^td=1",
)

def monitor(self, o, k, cid, i):
return InlineKeyboardButton(
xlate("monitor_button", option=o),
callback_data=f"{cid}^^^{i}^^^add^^m={k}",
)

def quality(self, q, cid, i):
return InlineKeyboardButton(
xlate("add_quality_button", quality=q["name"]),
callback_data=f"{cid}^^^{i}^^^add^^q={q['id']}",
)

def metadata(self, m, cid, i):
return InlineKeyboardButton(
xlate("add_metadata_button", metadata=m["name"]),
callback_data=f"{cid}^^^{i}^^^add^^m={m['id']}",
)

def path(self, p, cid, i):
return InlineKeyboardButton(
xlate("add_path_button", path=p["path"]),
callback_data=f"{cid}^^^{i}^^^add^^p={p['id']}",
)


class UserButtons(object):
def remove(self, u, cid):
return InlineKeyboardButton(
xlate("remove_user_button"),
callback_data=f"{cid}^^^{u['id']}^^^remove_user",
)

def username(self, u, cid):
return InlineKeyboardButton(
f"{u['username'] if u['username'] != 'None' else u['id']}",
callback_data=f"{cid}^^^{u['id']}^^^noop",
)

def admin(self, u, cid):
return InlineKeyboardButton(
xlate("remove_admin_button") if u["admin"] else xlate("make_admin_button"),
callback_data=f"{cid}^^^{u['id']}^^^{'remove_admin' if u['admin'] else 'make_admin'}",
)


class KeyboardButtons(object):
def __init__(self):
self.nav_buttons = NavButtons()
self.ext_buttons = ExternalButtons()
self.act_buttons = ActionButtons()
self.add_buttons = AddButtons()
self.user_buttons = UserButtons()

@property
def nav(self):
return self.nav_buttons

@property
def ext(self):
return self.ext_buttons

@property
def act(self):
return self.act_buttons

@property
def add(self):
return self.add_buttons

@property
def user(self):
return self.user_buttons
140 changes: 140 additions & 0 deletions commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import util
import settings
import traceback
import sys
import os
from importlib import util as import_util
from telegram.error import BadRequest
from util import xlate, xlate_aliases
sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))


class Command:
_dict = {}
_name = ""
_command_aliases = None
_validation_checks = []
auth_level = None

def __init__(self):
pass

def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
instance = cls()
cls._dict[cls._name] = instance

def _inject_dependency(self, searcharr_instance):
self.searcharr = searcharr_instance

def _validate_authenticated(self, update):
self.auth_level = self.searcharr._authenticated(update.message.from_user.id)
if self.auth_level:
return True
else:
update.message.reply_text(xlate_aliases("auth_required", settings.searcharr_start_command_aliases, "password"))
return None

def _validate_radarr_enabled(self, update):
if settings.radarr_enabled:
return True
else:
update.message.reply_text(xlate("radarr_disabled"))
return None

def _validate_sonarr_enabled(self, update):
if settings.sonarr_enabled:
return True
else:
update.message.reply_text(xlate("sonarr_disabled"))
return None

def _validate_readarr_enabled(self, update):
if settings.readarr_enabled:
return True
else:
update.message.reply_text(xlate("readarr_disabled"))
return None

def _validated(self, update):
for check in self._validation_checks:
method = getattr(self, check)
if not method(update):
return None
return True

def _execute(self, update, context):
util.log.debug(f"Received {self._name} cmd from [{update.message.from_user.username}]")
if not self._validated(update):
return
self._action(update, context)

def _action(self, update, context):
pass

def _search_collection(self, update, context, kind, plural, search_function, command_aliases):
title = util.strip_entities(update.message)

if not len(title):
x_title = xlate("title").title()
response = xlate_aliases("include_" + kind + "_title_in_cmd", command_aliases, x_title)
update.message.reply_text(response)
return

results = search_function(title)
cid = self.searcharr._generate_cid()
self.searcharr._create_conversation(
id=cid,
username=str(update.message.from_user.username),
kind=kind,
results=results,
)

if not len(results):
update.message.reply_text(xlate("no_matching_" + plural))
return

r = results[0]
reply_message, reply_markup = self.searcharr._prepare_response(
kind, r, cid, 0, len(results)
)
try:
context.bot.sendPhoto(
chat_id=update.message.chat.id,
photo=r["remotePoster"],
caption=reply_message,
reply_markup=reply_markup,
)
except BadRequest as e:
if str(e) in self._bad_request_poster_error_messages:
util.log.error(
f"Error sending photo [{r['remotePoster']}]: BadRequest: {e}. Attempting to send with default poster..."
)
context.bot.sendPhoto(
chat_id=update.message.chat.id,
photo="https://artworks.thetvdb.com/banners/images/missing/movie.jpg",
caption=reply_message,
reply_markup=reply_markup,
)
else:
raise


def load_module(path):
name = os.path.split(path)[-1]
spec = import_util.spec_from_file_location(name, path)
module = import_util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


path = os.path.abspath(__file__)
dirpath = os.path.dirname(path)

for fname in os.listdir(dirpath):
if not fname.startswith('.') and \
not fname.startswith('__') and fname.endswith('.py'):
try:
load_module(os.path.join(dirpath, fname))
except Exception:
traceback.print_exc()
18 changes: 18 additions & 0 deletions commands/book.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from commands import Command
import settings


class Book(Command):
_name = "book"
_command_aliases = settings.readarr_book_command_aliases
_validation_checks = ["_validate_authenticated", "_validate_readarr_enabled"]

def _action(self, update, context):
self._search_collection(
update=update,
context=context,
kind="book",
plural="book",
search_function=self.searcharr.readarr.lookup_book,
command_aliases=settings.readarr_book_command_aliases
)
28 changes: 28 additions & 0 deletions commands/help.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from commands import Command
import settings
from util import xlate, xlate_aliases


class Help(Command):
_name = "help"
_command_aliases = settings.searcharr_help_command_aliases
_validation_checks = ["_validate_authenticated"]

def _action(self, update, context):
response = ""
if self.searcharr.sonarr:
sonarr_help = xlate_aliases("help_sonarr", settings.sonarr_series_command_aliases, "title")
response += f" {sonarr_help}"
if self.searcharr.radarr:
radarr_help = xlate_aliases("help_radarr", settings.radarr_movie_command_aliases, "title")
response += f" {radarr_help}"
if self.searcharr.readarr:
readarr_help = xlate_aliases("help_readarr", settings.readarr_book_command_aliases, "title")
response += f" {readarr_help}"
if response == "":
response = xlate("no_features")

if self.auth_level == 2:
response += " " + xlate_aliases("admin_help", settings.searcharr_users_command_aliases)

update.message.reply_text(response)
18 changes: 18 additions & 0 deletions commands/movie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from commands import Command
import settings


class Movie(Command):
_name = "movie"
_command_aliases = settings.radarr_movie_command_aliases
_validation_checks = ["_validate_authenticated", "_validate_radarr_enabled"]

def _action(self, update, context):
self._search_collection(
update=update,
context=context,
kind="movie",
plural="movies",
search_function=self.searcharr.radarr.lookup_movie,
command_aliases=settings.radarr_movie_command_aliases
)
18 changes: 18 additions & 0 deletions commands/series.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from commands import Command
import settings


class Series(Command):
_name = "series"
_command_aliases = settings.sonarr_series_command_aliases
_validation_checks = ['_validate_authenticated', '_validate_sonarr_enabled']

def _action(self, update, context):
self._search_collection(
update=update,
context=context,
kind="series",
plural="series",
search_function=self.searcharr.sonarr.lookup_series,
command_aliases=settings.sonarr_series_command_aliases
)
Loading