diff --git a/config_example.yaml b/config_example.yaml index 90f33b9..3e3398c 100644 --- a/config_example.yaml +++ b/config_example.yaml @@ -21,6 +21,29 @@ sonarr: addRequesterIdTag: true # Add telegram user id as tag on series adminRestrictions: false +# Sportarr Configuration (sports leagues via https://github.com/Sportarr/Sportarr) +sportarr: + server: + addr: + port : 1867 # Default is 1867 + path: / # Default is / . If set, it must start and finish with / . Eg: /sportarr/ + ssl: false # Default false + auth: + apikey: + username: + password: + search: true # Start search after adding league + seasonFolder: true + excludedRootFolders: # If set must not have a trailing slash Eg: /mnt/Media + - # First excluded folder, add others with a "-" on a new line (same indentation) + narrowRootFolderNames: true # If true, only the last folder name will be shown instead of full path + excludedQualityProfiles: + - + defaultTags: # If set must be existing tags + - telegram + addRequesterIdTag: true # Add telegram user id as tag on league + adminRestrictions: false + # Radarr Configuration radarr: server: @@ -81,6 +104,7 @@ entrypointHelp: help # help entrypoint entrypointAdd: start # start or a custom entrypoint entrypointAllSeries: allSeries # allSeries or a custom entrypoint entrypointAllMovies: allMovies # allMovies or a custom entrypoint +entrypointAllSports: allSports # allSports or a custom entrypoint entrypointTransmission: transmission # transmission or a custom entrypoint entrypointSabnzbd: sabnzbd # sabnzbd or a custom entrypoint diff --git a/src/addarr.py b/src/addarr.py index 2286b2e..7636141 100644 --- a/src/addarr.py +++ b/src/addarr.py @@ -15,6 +15,7 @@ import logger import radarr as radarr import sonarr as sonarr +import sportarr as sportarr import delete as delete import all as all from config import checkConfigValues, config, checkConfig @@ -71,6 +72,14 @@ def main(): all.allSeries, ) + allSports_handler_command = CommandHandler(config["entrypointAllSports"], all.allSports) + allSports_handler_text = MessageHandler( + filters.Regex( + re.compile(r"^" + config["entrypointAllSports"] + "$", re.IGNORECASE) + ), + all.allSports, + ) + allMovies_handler_command = CommandHandler(config["entrypointAllMovies"], all.allMovies) allMovies_handler_text = MessageHandler( filters.Regex( @@ -93,10 +102,10 @@ def main(): SERIE_MOVIE_DELETE: [MessageHandler(filters.TEXT, choiceSerieMovie)], READ_DELETE_CHOICE: [ MessageHandler( - filters.Regex(f'^({i18n.t("addarr.Movie")}|{i18n.t("addarr.Series")})$'), + filters.Regex(f'^({i18n.t("addarr.Movie")}|{i18n.t("addarr.Series")}|{i18n.t("addarr.Sport")})$'), delete.confirmDelete, ), - CallbackQueryHandler(delete.confirmDelete, pattern=f'^({i18n.t("addarr.Movie")}|{i18n.t("addarr.Series")})$') + CallbackQueryHandler(delete.confirmDelete, pattern=f'^({i18n.t("addarr.Movie")}|{i18n.t("addarr.Series")}|{i18n.t("addarr.Sport")})$') ], GIVE_OPTION: [ CallbackQueryHandler(delete.deleteSerieMovie, pattern=f'({i18n.t("addarr.Delete")})'), @@ -123,6 +132,7 @@ def main(): CommandHandler(config["entrypointAdd"], startSerieMovie), CommandHandler(i18n.t("addarr.Movie"), startSerieMovie), CommandHandler(i18n.t("addarr.Series"), startSerieMovie), + CommandHandler(i18n.t("addarr.Sport"), startSerieMovie), MessageHandler( filters.Regex( re.compile(r'^' + config["entrypointAdd"] + '$', re.IGNORECASE) @@ -134,10 +144,10 @@ def main(): SERIE_MOVIE_AUTHENTICATED: [MessageHandler(filters.TEXT, choiceSerieMovie)], READ_CHOICE: [ MessageHandler( - filters.Regex(f'^({i18n.t("addarr.Movie")}|{i18n.t("addarr.Series")})$'), + filters.Regex(f'^({i18n.t("addarr.Movie")}|{i18n.t("addarr.Series")}|{i18n.t("addarr.Sport")})$'), searchSerieMovie, ), - CallbackQueryHandler(searchSerieMovie, pattern=f'^({i18n.t("addarr.Movie")}|{i18n.t("addarr.Series")})$'), + CallbackQueryHandler(searchSerieMovie, pattern=f'^({i18n.t("addarr.Movie")}|{i18n.t("addarr.Series")}|{i18n.t("addarr.Sport")})$'), MessageHandler( filters.Regex(f'^({i18n.t("addarr.New")})$'), startSerieMovie @@ -238,6 +248,8 @@ def main(): application.add_handler(auth_handler_text) application.add_handler(allSeries_handler_command) application.add_handler(allSeries_handler_text) + application.add_handler(allSports_handler_command) + application.add_handler(allSports_handler_text) application.add_handler(allMovies_handler_command) application.add_handler(allMovies_handler_text) application.add_handler(addMovieserie_handler) @@ -341,6 +353,7 @@ async def choiceSerieMovie(update, context): if reply.lower() not in [ i18n.t("addarr.Series").lower(), + i18n.t("addarr.Sport").lower(), i18n.t("addarr.Movie").lower(), ]: logger.debug( @@ -350,6 +363,7 @@ async def choiceSerieMovie(update, context): if context.user_data.get("choice") in [ i18n.t("addarr.Series"), + i18n.t("addarr.Sport"), i18n.t("addarr.Movie"), ]: logger.debug( @@ -367,6 +381,10 @@ async def choiceSerieMovie(update, context): '\U0001F4FA '+i18n.t("addarr.Series"), callback_data=i18n.t("addarr.Series") ), + InlineKeyboardButton( + '\U0001F3C6 '+i18n.t("addarr.Sport"), + callback_data=i18n.t("addarr.Sport") + ), ], [ InlineKeyboardButton( '\U0001F50D '+i18n.t("addarr.New"), @@ -479,7 +497,7 @@ async def searchSerieMovie(update, context): if choice == i18n.t("addarr.Movie"): message=i18n.t("addarr.messages.This", subjectWithArticle=i18n.t("addarr.MovieWithArticle").lower()) else: - message=i18n.t("addarr.messages.This", subjectWithArticle=i18n.t("addarr.SeriesWithArticle").lower()) + message=i18n.t("addarr.messages.This", subjectWithArticle=serieOrSportArticle(context).lower()) msg = await context.bot.send_message( chat_id=update.effective_message.chat_id, text=message, reply_markup=markup ) @@ -571,7 +589,7 @@ async def nextOption(update, context): if choice == i18n.t("addarr.Movie"): message=i18n.t("addarr.messages.This", subjectWithArticle=i18n.t("addarr.MovieWithArticle").lower()) else: - message=i18n.t("addarr.messages.This", subjectWithArticle=i18n.t("addarr.SeriesWithArticle").lower()) + message=i18n.t("addarr.messages.This", subjectWithArticle=serieOrSportArticle(context).lower()) msg = await context.bot.send_message( chat_id=update.effective_message.chat_id, text=message, reply_markup=markup ) @@ -841,7 +859,7 @@ async def addSerieMovie(update, context): if choice == i18n.t("addarr.Movie"): message=i18n.t("addarr.messages.AddSuccess", subjectWithArticle=i18n.t("addarr.MovieWithArticle")) else: - message=i18n.t("addarr.messages.AddSuccess", subjectWithArticle=i18n.t("addarr.SeriesWithArticle")) + message=i18n.t("addarr.messages.AddSuccess", subjectWithArticle=serieOrSportArticle(context)) await context.bot.edit_message_text( message_id=context.user_data["update_msg"], chat_id=update.effective_message.chat_id, @@ -852,7 +870,7 @@ async def addSerieMovie(update, context): if choice == i18n.t("addarr.Movie"): message2=i18n.t("addarr.Notifications.AddSuccess", subjectWithArticle=i18n.t("addarr.MovieWithArticle"),title=context.user_data['output'][position]['title'],first_name=update.effective_message.chat.first_name, chat_id=update.effective_message.chat.id) else: - message2=i18n.t("addarr.Notifications.AddSuccess", subjectWithArticle=i18n.t("addarr.SeriesWithArticle"),title=context.user_data['output'][position]['title'],first_name=update.effective_message.chat.first_name, chat_id=update.effective_message.chat.id) + message2=i18n.t("addarr.Notifications.AddSuccess", subjectWithArticle=serieOrSportArticle(context),title=context.user_data['output'][position]['title'],first_name=update.effective_message.chat.first_name, chat_id=update.effective_message.chat.id) await context.bot.send_message( chat_id=adminNotifyId, text=message2 ) @@ -862,7 +880,7 @@ async def addSerieMovie(update, context): if choice == i18n.t("addarr.Movie"): message=i18n.t("addarr.messages.AddFailed", subjectWithArticle=i18n.t("addarr.MovieWithArticle").lower()) else: - message=i18n.t("addarr.messages.AddFailed", subjectWithArticle=i18n.t("addarr.SeriesWithArticle").lower()) + message=i18n.t("addarr.messages.AddFailed", subjectWithArticle=serieOrSportArticle(context).lower()) await context.bot.edit_message_text( message_id=context.user_data["update_msg"], chat_id=update.effective_message.chat_id, @@ -873,7 +891,7 @@ async def addSerieMovie(update, context): if choice == i18n.t("addarr.Movie"): message2=i18n.t("addarr.Notifications.AddFailed", subjectWithArticle=i18n.t("addarr.MovieWithArticle"),title=context.user_data['output'][position]['title'],first_name=update.effective_message.chat.first_name, chat_id=update.effective_message.chat.id) else: - message2=i18n.t("addarr.Notifications.AddFailed", subjectWithArticle=i18n.t("addarr.SeriesWithArticle"),title=context.user_data['output'][position]['title'],first_name=update.effective_message.chat.first_name, chat_id=update.effective_message.chat.id) + message2=i18n.t("addarr.Notifications.AddFailed", subjectWithArticle=serieOrSportArticle(context),title=context.user_data['output'][position]['title'],first_name=update.effective_message.chat.first_name, chat_id=update.effective_message.chat.id) await context.bot.send_message( chat_id=adminNotifyId, text=message2 ) @@ -883,7 +901,7 @@ async def addSerieMovie(update, context): if choice == i18n.t("addarr.Movie"): message=i18n.t("addarr.messages.Exist", subjectWithArticle=i18n.t("addarr.MovieWithArticle")) else: - message=i18n.t("addarr.messages.Exist", subjectWithArticle=i18n.t("addarr.SeriesWithArticle")) + message=i18n.t("addarr.messages.Exist", subjectWithArticle=serieOrSportArticle(context)) await context.bot.edit_message_text( message_id=context.user_data["update_msg"], chat_id=update.effective_message.chat_id, @@ -895,7 +913,7 @@ async def addSerieMovie(update, context): if choice == i18n.t("addarr.Movie"): message2=i18n.t("addarr.Notifications.Exist", subjectWithArticle=i18n.t("addarr.MovieWithArticle"),title=context.user_data['output'][position]['title'],first_name=update.effective_message.chat.first_name, chat_id=update.effective_message.chat.id) else: - message2=i18n.t("addarr.Notifications.Exist", subjectWithArticle=i18n.t("addarr.SeriesWithArticle"),title=context.user_data['output'][position]['title'],first_name=update.effective_message.chat.first_name, chat_id=update.effective_message.chat.id) + message2=i18n.t("addarr.Notifications.Exist", subjectWithArticle=serieOrSportArticle(context),title=context.user_data['output'][position]['title'],first_name=update.effective_message.chat.first_name, chat_id=update.effective_message.chat.id) await context.bot.send_message( chat_id=adminNotifyId, text=message2 ) @@ -903,9 +921,18 @@ async def addSerieMovie(update, context): return ConversationHandler.END +def serieOrSportArticle(context): + """Sport shares the series flow; only the user-facing wording differs.""" + if context.user_data.get("choice") == i18n.t("addarr.Sport"): + return i18n.t("addarr.SportWithArticle") + return serieOrSportArticle(context) + + def getService(context): if context.user_data.get("choice") == i18n.t("addarr.Series"): return sonarr + elif context.user_data.get("choice") == i18n.t("addarr.Sport"): + return sportarr elif context.user_data.get("choice") == i18n.t("addarr.Movie"): return radarr else: @@ -928,7 +955,9 @@ async def help(update, context): delete=config["entrypointDelete"], movie=i18n.t("addarr.Movie").lower(), serie=i18n.t("addarr.Series").lower(), + sport=i18n.t("addarr.Sport").lower(), allSeries=config["entrypointAllSeries"], + allSports=config["entrypointAllSports"], allMovies=config["entrypointAllMovies"], transmission=config["entrypointTransmission"], sabnzbd=config["entrypointSabnzbd"], diff --git a/src/all.py b/src/all.py index 571d32b..080c549 100644 --- a/src/all.py +++ b/src/all.py @@ -7,6 +7,7 @@ from translations import i18n import radarr as radarr import sonarr as sonarr +import sportarr as sportarr # Set up logging logLevel = logging.DEBUG if config.get("debugLogging", False) else logging.INFO @@ -84,4 +85,40 @@ async def allMovies(update, context): chat_id=update.effective_message.chat_id, text=subString, ) - return ConversationHandler.END \ No newline at end of file + return ConversationHandler.END + +async def allSports(update, context): + if config.get("enableAllowlist") and not checkAllowed(update,"regular"): + #When using this mode, bot will remain silent if user is not in the allowlist.txt + logger.info("Allowlist is enabled, but userID isn't added into 'allowlist.txt'. So bot stays silent") + return ConversationHandler.END + + if sportarr.config.get("adminRestrictions") and not checkAllowed(update,"admin"): + await context.bot.send_message( + chat_id=update.effective_message.chat_id, + text=i18n.t("addarr.NotAdmin"), + ) + return ConversationHandler.END + + if not checkId(update): + if ( + await authentication(update, context) == "added" + ): # To also stop the beginning command + return ConversationHandler.END + else: + result = sportarr.allSeries() + content = format_long_list_message(result) + + if isinstance(content, str): + await context.bot.send_message( + chat_id=update.effective_message.chat_id, + text=content, + ) + else: + # print every substring + for subString in content: + await context.bot.send_message( + chat_id=update.effective_message.chat_id, + text=subString, + ) + return ConversationHandler.END diff --git a/src/definitions.py b/src/definitions.py index ae877b0..d192cb5 100644 --- a/src/definitions.py +++ b/src/definitions.py @@ -20,11 +20,25 @@ "entrypointDelete": "delete", #start or a custom entrypoint "entrypointAllSeries": "allSeries", #allSeries or a custom entrypoint "entrypointAllMovies": "allMovies", #allSeries or a custom entrypoint + "entrypointAllSports": "allSports", #allSports or a custom entrypoint "entrypointTransmission": "transmission", #transmission or a custom entrypoint "entrypointSabnzbd": "sabnzbd", #sabnzbd or a custom entrypoint "logToConsole": True, "debugLogging": False, "language": "en-us", "transmission": { "enable": False }, + # Sportarr ships disabled by default so existing configs keep working + "sportarr": { + "server": { "addr": "", "port": 1867, "path": "/", "ssl": False }, + "auth": { "apikey": "", "username": None, "password": None }, + "search": True, + "seasonFolder": True, + "excludedRootFolders": [], + "narrowRootFolderNames": True, + "excludedQualityProfiles": [], + "defaultTags": [], + "addRequesterIdTag": True, + "adminRestrictions": False, + }, "enableAdmin": False } diff --git a/src/sportarr.py b/src/sportarr.py new file mode 100644 index 0000000..855ca10 --- /dev/null +++ b/src/sportarr.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 + +import json +import logging + +import requests + +import commons as commons +import logger +from config import config + +# Set up logging +logLevel = logging.DEBUG if config.get("debugLogging", False) else logging.INFO +logger = logger.getLogger("addarr.sportarr", logLevel, config.get("logToConsole", False)) + +config = config["sportarr"] + +addSerieNeededFields = ["tvdbId", "tvRageId", "title", "titleSlug", "images", "seasons"] + + +def search(title): + parameters = {"term": title} + req = requests.get(commons.generateApiQuery("sportarr", "series/lookup", parameters)) + parsed_json = json.loads(req.text) + + if req.status_code == 200 and parsed_json: + return parsed_json + else: + return False + + +def giveTitles(parsed_json): + data = [] + for show in parsed_json: + if all( + x in show + for x in ["title", "statistics", "year", "tvdbId"] + ): + data.append( + { + "title": show["title"], + "seasonCount": show["statistics"]["seasonCount"], + "poster": show.get("remotePoster", None), + "year": show["year"], + "id": show["tvdbId"], + "monitored": show["monitored"], + "status": show["status"], + } + ) + return data + + +def inLibrary(tvdbId): + parameters = {} + req = requests.get(commons.generateApiQuery("sportarr", "series", parameters)) + parsed_json = json.loads(req.text) + return next((True for show in parsed_json if show["tvdbId"] == tvdbId), False) + + +def addToLibrary(tvdbId, path, qualityProfileId, tags, seasonsSelected): + parameters = {"term": "tvdb:" + str(tvdbId)} + req = requests.get(commons.generateApiQuery("sportarr", "series/lookup", parameters)) + parsed_json = json.loads(req.text) + data = json.dumps(buildData(parsed_json, path, qualityProfileId, tags, seasonsSelected)) + add = requests.post(commons.generateApiQuery("sportarr", "series"), data=data, headers={'Content-Type': 'application/json'}) + if add.status_code == 201: + return True + else: + return False + + +def removeFromLibrary(tvdbId): + parameters = { + "deleteFiles": str(True) + } + dbId = getDbIdFromImdbId(tvdbId) + delete = requests.delete(commons.generateApiQuery("sportarr", f"series/{dbId}", parameters)) + if delete.status_code == 200: + return True + else: + return False + + +def buildData(json, path, qualityProfileId, tags, seasonsSelected): + built_data = { + "qualityProfileId": qualityProfileId, + "addOptions": { + "ignoreEpisodesWithFiles": True, + "ignoreEpisodesWithoutFiles": False, + "searchForMissingEpisodes": config["search"], + }, + "rootFolderPath": path, + "seasonFolder": config["seasonFolder"], + "monitored": True, + "tags": tags, + "seasons": seasonsSelected, + } + for show in json: + for key, value in show.items(): + if key in addSerieNeededFields: + built_data[key] = value + if key == "seasons": built_data["seasons"] = seasonsSelected + logger.debug(f"Query endpoint is: {commons.generateApiQuery('sonarr', 'series')}") + return built_data + + +def getRootFolders(): + parameters = {} + req = requests.get(commons.generateApiQuery("sportarr", "Rootfolder", parameters)) + parsed_json = json.loads(req.text) + # Remove unmappedFolders from rootFolder data--we don't need that + for item in [ + item for item in parsed_json if item.get("unmappedFolders") is not None + ]: + item.pop("unmappedFolders") + return parsed_json + + +def allSeries(): + parameters = {} + req = requests.get(commons.generateApiQuery("sportarr", "series", parameters)) + parsed_json = json.loads(req.text) + + if req.status_code == 200: + data = [] + for show in parsed_json: + if all( + x in show + for x in ["title", "year", "monitored", "status"] + ): + data.append( + { + "title": show["title"], + "year": show["year"], + "monitored": show["monitored"], + "status": show["status"], + } + ) + return data + else: + return False + + +def getQualityProfiles(): + parameters = {} + req = requests.get(commons.generateApiQuery("sportarr", "qualityProfile", parameters)) + parsed_json = json.loads(req.text) + return parsed_json + + +def getTags(): + parameters = {} + req = requests.get(commons.generateApiQuery("sportarr", "tag", parameters)) + parsed_json = json.loads(req.text) + return parsed_json + + +def createTag(tag): + data_json = { + "id": int(max([t["id"] for t in getTags()], default=0)+1), + "label": str(tag) + } + add = requests.post(commons.generateApiQuery("sportarr", "tag"), json=data_json, headers={'Content-Type': 'application/json'}) + if add.status_code == 200: + return True + else: + return False + +def getSeasons(tvdbId): + parameters = {"term": "tvdb:" + str(tvdbId)} + req = requests.get(commons.generateApiQuery("sportarr", "series/lookup", parameters)) + parsed_json = json.loads(req.text) + return parsed_json[0]["seasons"] + + +def getDbIdFromImdbId(tvdbId): + req = requests.get(commons.generateApiQuery("sportarr", "series", {})) + parsed_json = json.loads(req.text) + dbId = [f["id"] for f in parsed_json if f["tvdbId"] == tvdbId] + return dbId[0] diff --git a/translations/addarr.en-us.yml b/translations/addarr.en-us.yml index dd7c521..80d9382 100644 --- a/translations/addarr.en-us.yml +++ b/translations/addarr.en-us.yml @@ -6,6 +6,8 @@ en-us: MovieWithArticle: The movie Series: Series SeriesWithArticle: The series + Sport: Sport + SportWithArticle: The league Start chatting: Start chatting with Addarr in Telegram. The first time you use "start" you will be asked to enter your password. End: The adding/deleting of movies or series has ended. Title: "Enter the title:" @@ -32,8 +34,9 @@ en-us: \n• To authenticate before first use you should use /%{authenticate}. \n• To add a movie or series use /%{add}. \n• To delete a movie or series use /%{delete}. - \n• To immediately add a movie or series you can use /%{movie} or /%{serie}. + \n• To immediately add a movie, series or league you can use /%{movie}, /%{serie} or /%{sport}. \n• To check which series are in Sonarr you can use /%{allSeries}. + \n• To check which leagues are in Sportarr you can use /%{allSports}. \n• To check which movies are in Radarr you can use /%{allMovies}. \n• To toggle the up- and download speed of Transmission use /%{transmission}. \n• To change the up- and download speed of Sabnzbd use /%{sabnzbd}."