Skip to content

Commit

Permalink
Concurrency, streamlined flow, multiple path support, etc. (Waterboy1…
Browse files Browse the repository at this point in the history
…602#24)

* Standardized logging; support for concurrent users (Waterboy1602#22); separate movie/series cmds; support for multiple rootFolders in Sonarr/Radarr; move app to /app in Docker; update README

* Remove rootFolder from config
  • Loading branch information
toddrob99 authored Jul 7, 2020
1 parent bc3c02c commit ed66723
Show file tree
Hide file tree
Showing 12 changed files with 255 additions and 106 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ __pycache__/
*.code-workspace
*.log
config.*

logs/
10 changes: 4 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
FROM python:3.8-alpine

WORKDIR /usr/src
WORKDIR /app
# Install requirements
RUN apk add --no-cache gcc musl-dev libffi-dev openssl-dev
# Copy files to container
COPY . /usr/src/addarr
COPY . /app
# Install ans build Addarr requirements, make symlink to redirect logs to stdout
RUN cd addarr && \
pip install --no-cache-dir -r requirements.txt --upgrade && \
ln -s /dev/stdout /usr/src/telegramBot.log
RUN pip install --no-cache-dir -r requirements.txt --upgrade

ENTRYPOINT ["python"]
CMD ["/usr/src/addarr/addarr.py"]
CMD ["/app/addarr.py"]
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ If you've entered a wrong password, there will be saved a log to `addarr.log` wi

You can start the Transmission command with `/transmission` or `transmission`. After that you will be asked what you want to do. Just press the action you want and it's done.

You can start adding series/movies with`/start` or just `start`. The next steps will be made clear by the bot. At any time you can stop the adding by sending `/stop` or `stop`.
You can start adding series/movies with `/start` or just `start`. The next steps will be made clear by the bot. At any time you can stop the adding by sending `/stop` or `stop`.

Different entrypoints for all these commands can be entered in the config file.
You can also start adding a series or movie with `/movie` or `/series` when the bot is running in English or `/film` or `/serie` when the bot is running in Dutch. This will skip the step of asking if the title represents a series or a movie.

Different entrypoints for all these commands can be entered in the config file, except `/movie`/`/film`/`series`/`serie`, which are defined in `lang.yaml`.

## FUNCTIONS
- Add series/movies to Sonarr/Radarr
Expand All @@ -25,6 +27,8 @@ Different entrypoints for all these commands can be entered in the config file.
These are the default commands:
- Auth: authenticate the chat to use your bot
- Start: start adding a series or movie to Sonarr/Radarr
- Movie (en)/Film (nl): starting adding a movie to Radarr
- Series (en)/Serie (nl): starting adding a series to Sonarr
- Transmission: change the down-/upload speed of Transmission from Temporary Speed Limit to normal or the other way around
- Stop: stop the command you were executing

Expand Down
207 changes: 154 additions & 53 deletions addarr.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
#!/usr/bin/env python3

import datetime
import logging
import re
import time
import os

import yaml
Expand All @@ -16,39 +14,37 @@
Filters,
)

from definitions import CONFIG_PATH, LANG_PATH, CHATID_PATH, ADMIN_PATH
import radarr as radarr
import sonarr as sonarr
from definitions import CONFIG_PATH, LANG_PATH, CHATID_PATH, LOG_PATH, ADMIN_PATH

log = logging
log.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
filename=LOG_PATH,
filemode="a",
level=logging.INFO,
)
import logger

SERIE_MOVIE_AUTHENTICATED, READ_CHOICE, GIVE_OPTION, TSL_NORMAL = range(4)
__version__ = "0.3"

config = yaml.safe_load(open(CONFIG_PATH, encoding="utf8"))

# Set up logging
logLevel = logging.DEBUG if config.get("debugLogging", False) else logging.INFO
logger = logger.getLogger("addarr", logLevel, config.get("logToConsole", False))
logger.debug(f"Addarr v{__version__} starting up...")

SERIE_MOVIE_AUTHENTICATED, READ_CHOICE, GIVE_OPTION, GIVE_PATHS, TSL_NORMAL = range(5)

updater = Updater(config["telegram"]["token"], use_context=True)
dispatcher = updater.dispatcher
lang = config["language"]

transcript = yaml.safe_load(open(LANG_PATH, encoding="utf8"))
transcript = transcript[lang]

output = None
service = None


def main():
open(LOG_PATH, "w").close() # clear logfile at startup of script
auth_handler = CommandHandler("entrypointAuth", authentication)
addMovieserie = ConversationHandler(
entry_points=[
CommandHandler(config["entrypointAdd"], startSerieMovie),
CommandHandler(transcript["Movie"], startSerieMovie),
CommandHandler(transcript["Serie"], startSerieMovie),
MessageHandler(
Filters.regex(
re.compile(r"" + config["entrypointAdd"] + "", re.IGNORECASE)
Expand All @@ -65,14 +61,20 @@ def main():
)
],
GIVE_OPTION: [
MessageHandler(Filters.regex(f'({transcript["Add"]})'), addSerieMovie),
MessageHandler(Filters.regex(f'({transcript["Add"]})'), pathSerieMovie),
MessageHandler(
Filters.regex(f'({transcript["Next result"]})'), nextOption
),
MessageHandler(
Filters.regex(f'({transcript["New"]})'), startSerieMovie
),
],
GIVE_PATHS: [
MessageHandler(
Filters.regex(re.compile(r"^(Path: )(.*)$", re.IGNORECASE)),
addSerieMovie,
),
],
},
fallbacks=[
CommandHandler("stop", stop),
Expand Down Expand Up @@ -101,7 +103,8 @@ def main():
dispatcher.add_handler(auth_handler)
dispatcher.add_handler(addMovieserie)
dispatcher.add_handler(changeTransmissionSpeed)
print(transcript["Start chatting"])

logger.info(transcript["Start chatting"])
updater.start_polling()
updater.idle()

Expand Down Expand Up @@ -240,18 +243,9 @@ def authentication(update, context):
file.close()
return "added"
else:
with open(LOG_PATH, "a") as file:
ts = time.time()
sttime = datetime.datetime.fromtimestamp(ts).strftime("%d%m%Y_%H:%M:%S - ")
file.write(
sttime
+ "@"
+ str(update.message.from_user.username)
+ " - "
+ str(password)
+ "\n"
)
file.close()
logger.warning(
f"Failed authentication attempt by [{update.message.from_user.username}]. Password entered: [{password}]"
)
context.bot.send_message(
chat_id=update.effective_message.chat_id, text=transcript["Wrong password"]
)
Expand All @@ -261,6 +255,7 @@ def authentication(update, context):


def stop(update, context):
clearUserData(context)
context.bot.send_message(
chat_id=update.effective_message.chat_id, text=transcript["End"]
)
Expand All @@ -269,6 +264,23 @@ def stop(update, context):

def startSerieMovie(update, context):
if checkId(update):
if update.message.text[1:].lower() in [
transcript["Serie"].lower(),
transcript["Movie"].lower(),
]:
logger.debug(
f"User issued {update.message.text} command, so setting user_data[choice] accordingly"
)
context.user_data.update(
{
"choice": transcript["Serie"]
if update.message.text[1:].lower() == transcript["Serie"].lower()
else transcript["Movie"]
}
)
elif update.message.text.lower() == transcript["New"].lower():
logger.debug("User issued New command, so clearing user_data")
clearUserData(context)
context.bot.send_message(
chat_id=update.effective_message.chat_id, text=transcript["Title"]
)
Expand All @@ -288,29 +300,41 @@ def choiceSerieMovie(update, context):
return ConversationHandler.END
else:
text = update.message.text
context.user_data["title"] = text
reply_keyboard = [[transcript["Movie"], transcript["Serie"]]]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
update.message.reply_text(transcript["What is this?"], reply_markup=markup)
return READ_CHOICE
if text[1:].lower() not in [
transcript["Serie"].lower(),
transcript["Movie"].lower(),
]:
context.user_data["title"] = text
if context.user_data.get("choice") in [
transcript["Serie"],
transcript["Movie"],
]:
logger.debug(
f"user_data[choice] is {context.user_data['choice']}, skipping step of selecting movie/series"
)
return searchSerieMovie(update, context)
else:
reply_keyboard = [[transcript["Movie"], transcript["Serie"]]]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
update.message.reply_text(transcript["What is this?"], reply_markup=markup)
return READ_CHOICE


def searchSerieMovie(update, context):
title = context.user_data["title"]
del context.user_data["title"]
choice = update.message.text
context.user_data["choice"] = choice
if context.user_data.get("title"):
context.user_data.pop("title")
if not context.user_data.get("choice"):
choice = update.message.text
context.user_data["choice"] = choice
else:
choice = context.user_data["choice"]
context.user_data["position"] = 0

global service
if choice == transcript["Serie"]:
service = sonarr
elif choice == transcript["Movie"]:
service = radarr
service = getService(context)

global output
position = context.user_data["position"]
output = service.giveTitles(service.search(title))
context.user_data["output"] = service.giveTitles(service.search(title))

reply_keyboard = [
[transcript[choice.lower()]["Add"], transcript["Next result"]],
Expand All @@ -322,9 +346,10 @@ def searchSerieMovie(update, context):
text=transcript[choice.lower()]["This"],
)
context.bot.sendPhoto(
chat_id=update.effective_message.chat_id, photo=output[position]["poster"]
chat_id=update.effective_message.chat_id,
photo=context.user_data["output"][position]["poster"],
)
text = output[position]["title"] + " (" + str(output[position]["year"]) + ")"
text = f"{context.user_data['output'][position]['title']} ({context.user_data['output'][position]['year']})"
context.bot.send_message(
chat_id=update.effective_message.chat_id, text=text, reply_markup=markup
)
Expand All @@ -337,7 +362,7 @@ def nextOption(update, context):

choice = context.user_data["choice"]

if position < len(output):
if position < len(context.user_data["output"]):
reply_keyboard = [
[transcript[choice.lower()]["Add"], transcript["Next result"]],
[transcript["New"], transcript["Stop"]],
Expand All @@ -349,9 +374,15 @@ def nextOption(update, context):
text=transcript[choice.lower()]["This"],
)
context.bot.sendPhoto(
chat_id=update.effective_message.chat_id, photo=output[position]["poster"]
chat_id=update.effective_message.chat_id,
photo=context.user_data["output"][position]["poster"],
)
text = (
context.user_data["output"][position]["title"]
+ " ("
+ str(context.user_data["output"][position]["year"])
+ ")"
)
text = output[position]["title"] + " (" + str(output[position]["year"]) + ")"
context.bot.send_message(
chat_id=update.effective_message.chat_id, text=text, reply_markup=markup
)
Expand All @@ -365,32 +396,102 @@ def nextOption(update, context):
return stop()


def pathSerieMovie(update, context):
service = getService(context)
paths = service.getRootFolders()
context.user_data.update({"paths": [p["path"] for p in paths]})
if len(paths) == 1:
# There is only 1 path, so use it!
logger.debug("Only found 1 path, so proceeding with that one...")
context.user_data["path"] = paths[0]["path"]
return addSerieMovie(update, context)
formattedPaths = [f"Path: {p['path']}" for p in paths]

if len(paths) % 2 > 0:
oddItem = formattedPaths.pop(-1)
reply_keyboard = [
[formattedPaths[i], formattedPaths[i + 1]]
for i in range(0, len(formattedPaths), 2)
]
if len(paths) % 2 > 0:
reply_keyboard.append([oddItem])
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
context.bot.send_message(
chat_id=update.effective_message.chat_id,
text=transcript["Select a path"],
reply_markup=markup,
)
return GIVE_PATHS


def addSerieMovie(update, context):
position = context.user_data["position"]
choice = context.user_data["choice"]
idnumber = output[position]["id"]
idnumber = context.user_data["output"][position]["id"]

if not context.user_data.get("path"):
# Path selection should be in the update message
if update.message.text.replace("Path: ", "").strip() in context.user_data.get(
"paths", {}
):
context.user_data["path"] = update.message.text.replace(
"Path: ", ""
).strip()
else:
logger.debug(
f"Message text [{update.message.text.replace('Path: ', '').strip()}] doesn't match any of the paths. Sending paths for selection..."
)
return pathSerieMovie(update, context)

path = context.user_data["path"]
service = getService(context)

if not service.inLibrary(idnumber):
if service.addToLibrary(idnumber):
if service.addToLibrary(idnumber, path):
context.bot.send_message(
chat_id=update.effective_message.chat_id,
text=transcript[choice.lower()]["Success"],
)
clearUserData(context)
return ConversationHandler.END
else:
context.bot.send_message(
chat_id=update.effective_message.chat_id,
text=transcript[choice.lower()]["Failed"],
)
clearUserData(context)
return ConversationHandler.END

else:
context.bot.send_message(
chat_id=update.effective_message.chat_id,
text=transcript[choice.lower()]["Exist"],
)
clearUserData(context)
return ConversationHandler.END


def getService(context):
if context.user_data.get("choice") == transcript["Serie"]:
return sonarr
elif context.user_data.get("choice") == transcript["Movie"]:
return radarr
else:
raise ValueError(
f"Cannot determine service based on unknown or missing choice: {context.user_data.get('choice')}."
)


def clearUserData(context):
logger.debug(
"Removing choice, title, position, paths, and output from context.user_data..."
)
for x in [
x
for x in ["choice", "title", "position", "output", "paths", "path"]
if x in context.user_data.keys()
]:
context.user_data.pop(x)


if __name__ == "__main__":
main()
Loading

0 comments on commit ed66723

Please sign in to comment.