diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/commands/movie_show_response_newaccount.py | 72 | ||||
-rw-r--r-- | app/commands/number_response_request.py | 59 | ||||
-rw-r--r-- | app/commands/request.py | 49 | ||||
-rw-r--r-- | app/commands/status.py | 76 | ||||
-rw-r--r-- | app/create_message.py | 14 | ||||
-rw-r--r-- | app/db_removal.py | 24 | ||||
-rw-r--r-- | app/db_setup.py | 32 | ||||
-rw-r--r-- | app/initialize_variables.py | 142 | ||||
-rw-r--r-- | app/messagearr.py | 245 | ||||
-rw-r--r-- | app/validate_config.py | 261 | ||||
-rw-r--r-- | app/wsgi.py | 6 |
11 files changed, 676 insertions, 304 deletions
diff --git a/app/commands/movie_show_response_newaccount.py b/app/commands/movie_show_response_newaccount.py new file mode 100644 index 0000000..6922560 --- /dev/null +++ b/app/commands/movie_show_response_newaccount.py @@ -0,0 +1,72 @@ +import datetime +import requests +import random +import string +import sqlite3 + +import initialize_variables +from create_message import create_message + + +def movie_show_response_newaccount(from_number, message): + if from_number not in initialize_variables.temp_new_account_requests.keys(): + create_message(from_number, "There is no current request that you can decide on. It might be that your /newaccount command timed out due since you took too long to response. Please try again. If this issue persists, please contact Parker.") + return + + # If its been 5 minutes since prompt was sent, alert user of timed out request + if (datetime.datetime.now() - initialize_variables.temp_new_account_requests[from_number]).total_seconds() / 60 > 5: + del initialize_variables.temp_new_account_requests[from_number] + create_message(from_number, "You waited too long and therefore your request has timed out.\n\nPlease try again by re-running the /newaccount command. If this issue persists, please contact Parker.") + return + + if message.strip().lower() == "show": + active_time = 24 + + elif message.strip().lower() == "movie": + active_time = 4 + + else: + create_message(from_number, "You did not enter a valid response. Please re-send the /newaccount command and try again. If you believe this is an error, please contact Parker.") + return + + # Otherwise, all checks have been completed + username = ''.join(random.choices(string.ascii_lowercase + string.digits, k=5)) + password = ''.join(random.choices(string.ascii_lowercase + string.digits, k=15)) + + deletion_time = datetime.datetime.now() + datetime.timedelta(hours=active_time) + # Create new Jellyfin account + request_1 = requests.post(f'{initialize_variables.jellyfin_url}/Users/New', headers=initialize_variables.jellyfin_headers, json={'Name': username, 'Password': password}) + if request_1.status_code != 200: + create_message(from_number, "Error creating Jellyfin account. Please try again. If the error persists, contact Parker.") + return + + user_id = request_1.json()['Id'] + # Get account policy and make edits + request_2 = requests.get(f'{initialize_variables.jellyfin_url}/Users/{user_id}', headers=initialize_variables.jellyfin_headers) + if request_2.status_code != 200: + create_message(from_number, "Error creating Jellyfin account. Please try again. If the error persists, contact Parker.") + return + + policy = request_2.json()['Policy'] + policy['SyncPlayAccess'] = 'JoinGroups' + policy['EnableContentDownloading'] = False + policy['InvalidLoginAttemptCount'] = 3 + policy['MaxActiveSessions'] = 1 + # Update user with new policy + request_3 = requests.post(f'{initialize_variables.jellyfin_url}/Users/{user_id}/Policy', headers=initialize_variables.jellyfin_headers, json=policy) + if request_3.status_code != 204: + create_message(from_number, "Error creating Jellyfin account. Please try again. If the error persists, contact Parker.") + return + + # Add information to the database + db = sqlite3.connect(initialize_variables.db_path) + cursor = db.cursor() + cursor.execute(''' + INSERT INTO jellyfin_accounts (user_id, deletion_time) + VALUES(?, ?) + ''', (user_id, deletion_time)) + db.commit() + db.close() + + create_message(from_number, f"Username: {username}\nPassword: {password}\n\nYour account will expire in {active_time} hours.") + return diff --git a/app/commands/number_response_request.py b/app/commands/number_response_request.py new file mode 100644 index 0000000..bb9958c --- /dev/null +++ b/app/commands/number_response_request.py @@ -0,0 +1,59 @@ +import datetime +import requests +import sqlite3 + +import initialize_variables +from create_message import create_message + + +def number_response_request(from_number, message): + if from_number not in initialize_variables.temp_movie_ids.keys(): + create_message(from_number, "There is no current request that you can decide on. It might be that your /request command timed out due since you took too long to response. Please try again. If this issue persists, please contact Parker.") + return + + # If its been 5 minutes since prompt was sent, alert user of timed out request + if (datetime.datetime.now() - initialize_variables.temp_movie_ids[from_number]['time']).total_seconds() / 60 > 5: + del initialize_variables.temp_movie_ids[from_number] + create_message(from_number, "You waited too long and therefore your request has timed out.\n\nPlease try again by re-running the /request command. If this issue persists, please contact Parker.") + return + + # Otherwise, all checks have been completed + create_message(from_number, "Just a moment while I add your movie to the library...") + movie_number = initialize_variables.numbers_responses[message.strip()] + try: + tmdb_id = initialize_variables.temp_movie_ids[from_number]['ids'][movie_number - 1] + except IndexError: + create_message(from_number, "You did not enter a valid number. Please re-send the /request command and try again. If you believe this is an error, please contact Parker.") + del initialize_variables.temp_movie_ids[from_number] + return + + data = requests.get(f'{initialize_variables.radarr_host_url}/api/v3/movie/lookup/tmdb?tmdbId={tmdb_id}', headers=initialize_variables.headers) + + data = data.json() + movie_title = data['title'] + # Change the qualityProfileId, monitored, and rootFolderPath values + data['qualityProfileId'] = initialize_variables.quality_profile_id + data['monitored'] = True + data['rootFolderPath'] = initialize_variables.root_folder_path + # Send data to Radarr API + response = requests.post(f'{initialize_variables.radarr_host_url}/api/v3/movie', headers=initialize_variables.headers, json=data) + data = response.json() + movie_id = data['id'] + # Send message to user alerting them that the movie was added to the library + create_message(from_number, f"🎉 {data['title']} has been added to the library!\n\nTo check up on the status of your movie(s) send /status - please wait at least 5 minutes before running this command in order to get an accurate time.") + + # After everything is completed, send Radarr a request to search indexers for new movie + requests.post(f'{initialize_variables.radarr_host_url}/api/v3/command', headers=initialize_variables.headers, json={'name': 'MoviesSearch', 'movieIds': [int(movie_id)]}) + + # Add the movie_id to the database so that users can check up on the status of their movie + db = sqlite3.connect(initialize_variables.db_path) + cursor = db.cursor() + cursor.execute(''' + INSERT INTO movies(from_number, movie_id, movie_title) + VALUES(?, ?, ?) + ''', (from_number, movie_id, movie_title)) + db.commit() + db.close() + + del initialize_variables.temp_movie_ids[from_number] + return
\ No newline at end of file diff --git a/app/commands/request.py b/app/commands/request.py new file mode 100644 index 0000000..316bd70 --- /dev/null +++ b/app/commands/request.py @@ -0,0 +1,49 @@ +import requests +import datetime + +import initialize_variables +from create_message import create_message + + +def request(from_number, message): + # If the user has already run the /request command, delete the entry + # from the temp_movie_ids dict so that they can run the command again + if from_number in initialize_variables.temp_movie_ids.keys(): + del initialize_variables.temp_movie_ids[from_number] + + # If the user did not include a movie title, alert them to do so + if len(message) <= 9: + create_message(from_number, "Please include the movie title after the /request command.\nEX: /request The Dark Knight") + return + + incoming_message = message.split(' ', 1)[1] + movie_request = incoming_message.replace(' ', '%20') + if movie_request.endswith("%20"): + movie_request = movie_request[:-3] + + # Send a request to the radarr API to get the movie info + response = requests.get(f'{initialize_variables.radarr_host_url}/api/v3/movie/lookup?term={movie_request}', headers=initialize_variables.headers) + + if len(response.json()) == 0: + create_message(from_number, "There were no results for that movie. Please make sure you typed the title correctly.") + return + # If the movie is already added to the library, return a message saying so. + if response.json()[0]['added'] != '0001-01-01T05:51:00Z': + create_message(from_number, "This movie is already added to the server.\n\nIf you believe this is an error, please contact Parker.") + return + + # Add top 3 results to a message + message = "" + for i in range(min(3, len(response.json()))): + message += f"{i+1}. {response.json()[i]['folder']}\n\n" + if from_number not in initialize_variables.temp_movie_ids.keys(): + initialize_variables.temp_movie_ids[from_number] = { + 'ids': [], + 'time': datetime.datetime.now() + } + initialize_variables.temp_movie_ids[from_number]['ids'].append(response.json()[i]['tmdbId']) + + message += "Reply with the number associated with the movie you want to download. EX: 1\n\nIf the movie you want is not on the list, make sure you typed the title exactly as it is spelt, or ask Parker to manually add the movie." + + create_message(from_number, message) + return
\ No newline at end of file diff --git a/app/commands/status.py b/app/commands/status.py new file mode 100644 index 0000000..acf9376 --- /dev/null +++ b/app/commands/status.py @@ -0,0 +1,76 @@ +import datetime +import sqlite3 +import requests +import humanize + +import initialize_variables +from create_message import create_message + + +def status(from_number): + # This returns a list of ALL movies being downloaded, but not all of them were + # requested by the user, so we need to filter out the ones that were not requested + response = requests.get(f'{initialize_variables.radarr_host_url}/api/v3/queue/', headers=initialize_variables.headers) + # Get all the movie_ids that were requested by the user + db = sqlite3.connect(initialize_variables.db_path) + cursor = db.cursor() + cursor.execute(''' + SELECT movie_id, movie_title FROM movies WHERE from_number = ? + ''', (from_number,)) + movie_info = cursor.fetchall() + db.close() + + movies = {} # movie_id: movie_title + for movie in movie_info: + movies[movie[0]] = movie[1] + + if len(movies) == 0: + create_message(from_number, "You have no movies being downloaded at the moment.\n\nIf you previously added a movie, it is likely that it has finished downloading. If you believe this is an error, please contact Parker.") + return + + message = "" + # Loop through the response from the radarr API and filter out the movies that were not requested by the user + for movie in response.json()['records']: + movie_id = str(movie['movieId']) + if movie_id in movies.keys(): + if movie['status'] == 'downloading': + # Humanize the time_left value + try: + time_left = humanize.precisedelta(datetime.datetime.strptime(movie['timeleft'], '%H:%M:%S') - datetime.datetime.strptime('00:00:00', '%H:%M:%S'), minimum_unit='seconds') + except ValueError: + # Sometimes movie downloads take a long time and include days in the time_left value + # This is formated as 1.00:00:00 + time_left = humanize.precisedelta(datetime.datetime.strptime(movie['timeleft'], '%d.%H:%M:%S') - datetime.datetime.strptime('00:00:00', '%H:%M:%S'), minimum_unit='seconds') + except KeyError: + time_left = 'Unknown' + + message += f"📥 {movies[movie_id]} - {time_left}\n" + else: + message += f"{movies[movie_id]} - {str(movie['status']).upper()}\n" + + # If the message is empty, that means the user has no movies being downloaded + # Or, no download was found for the movie they requested + if message == "": + # For all movie IDs within the database + for movie_id in movies.keys(): + response = requests.get(f'{initialize_variables.radarr_host_url}/api/v3/movie/{movie_id}', headers=initialize_variables.headers) + # This means that there is no current download, and no file has been found + # MOST likely means a download just wasn't found, so alert the user + data = response.json() + if data['hasFile'] == False: + message += f"{movies[movie_id]} - NOT FOUND\n\nThis means a download was not found for the movie(s), if this is a brand new movie that is likely the reason. If the movie has already been released on DVD/Blu-Ray, please contact Parker." + + # Send message with info about download to user, otherwise, the user has + # no movies being downloaded at the moment so alert them + if message != "": + create_message(from_number, message) + return + else: + create_message(from_number, "You have no movies being downloaded at the moment.\n\nIf you previously added a movie, it is likely that it has finished downloading. If you believe this is an error, please contact Parker.") + return + + # Otherwise, add another part to the message containing movie data + else: + message += "\n\nIf movies consistently show as 'WARNING' or 'QUEUED' or any other error over multiple hours, please contact Parker." + create_message(from_number, message) + return
\ No newline at end of file diff --git a/app/create_message.py b/app/create_message.py index f51c867..48097d5 100644 --- a/app/create_message.py +++ b/app/create_message.py @@ -1,21 +1,21 @@ import telnyx from twilio.rest import Client -from initialize_variables import * +import initialize_variables def create_message(number, message): - if sms_service == 'telnyx': - telnyx.api_key = telnyx_api_key + if initialize_variables.sms_service == 'telnyx': + telnyx.api_key = initialize_variables.telnyx_api_key telnyx.Message.create( - from_=api_number, + from_=initialize_variables.api_number, to=number, text=message ) - if sms_service == 'twilio': - client = Client(twilio_account_sid, twilio_auth_token) + if initialize_variables.sms_service == 'twilio': + client = Client(initialize_variables.twilio_account_sid, initialize_variables.twilio_auth_token) client.messages.create( body=message, - from_=api_number, + from_=initialize_variables.api_number, to=number ) diff --git a/app/db_removal.py b/app/db_removal.py index 1c49dd5..e9f6594 100644 --- a/app/db_removal.py +++ b/app/db_removal.py @@ -1,15 +1,16 @@ from apscheduler.schedulers.background import BackgroundScheduler import sqlite3 import requests +import datetime -from initialize_variables import radarr_host_url, headers +import initialize_variables # Remove all entries from the database of movies that have already finished downloading # This helps to stop from entries building up in the database and slowing down everything sched = BackgroundScheduler(daemon=True) @sched.scheduled_job('cron', hour='0', minute='0') def clear_database(): - db = sqlite3.connect('/data/movies.db') + db = sqlite3.connect(initialize_variables.db_path) cursor = db.cursor() # First get all of the movie ids in the database cursor.execute(''' @@ -17,7 +18,7 @@ def clear_database(): ''') movie_ids = cursor.fetchall() # Get all of the movie_ids that are currently downloading/queued and/or missing - response = requests.get(f'{radarr_host_url}/api/v3/queue/', headers=headers) + response = requests.get(f'{initialize_variables.radarr_host_url}/api/v3/queue/', headers=initialize_variables.headers) current_movie_ids = [] for movie in response.json()['records']: current_movie_ids.append(str(movie['movieId'])) @@ -30,4 +31,21 @@ def clear_database(): DELETE FROM movies WHERE movie_id = ? ''', (movie_id[0],)) db.commit() + db.close() + +@sched.scheduled_job('interval', seconds=10) +def clear_jellyfin_accounts(): + db = sqlite3.connect(initialize_variables.db_path) + cursor = db.cursor() + cursor.execute(''' + SELECT user_id, deletion_time FROM jellyfin_accounts + ''') + data = cursor.fetchall() + for user_id, deletion_time in data: + if datetime.datetime.now() > datetime.datetime.strptime(deletion_time, '%Y-%m-%d %H:%M:%S.%f'): + requests.delete(f'{initialize_variables.jellyfin_url}/Users/{user_id}', headers=initialize_variables.jellyfin_headers) + cursor.execute(''' + DELETE FROM jellyfin_accounts WHERE user_id = ? + ''', (user_id,)) + db.commit() db.close()
\ No newline at end of file diff --git a/app/db_setup.py b/app/db_setup.py new file mode 100644 index 0000000..702449b --- /dev/null +++ b/app/db_setup.py @@ -0,0 +1,32 @@ +import os +import sqlite3 +import initialize_variables + +""" +This function is run before the application starts - creates database and sets connection string +""" +def setup_db(): + IN_DOCKER = os.environ.get('IN_DOCKER', False) + if IN_DOCKER: + db = sqlite3.connect('/data/movies.db') + initialize_variables.db_path = '/data/movies.db' + else: + db = sqlite3.connect('movies.db') + initialize_variables.db_path = 'movies.db' + + cursor = db.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS movies( + from_number TEXT, + movie_id TEXT, + movie_title TEXT + ) + ''') + cursor.execute(''' + CREATE TABLE IF NOT EXISTS jellyfin_accounts( + user_id TEXT, + deletion_time DATETIME + ) + ''') + db.commit() + db.close() diff --git a/app/initialize_variables.py b/app/initialize_variables.py index 9dfda1a..09221f7 100644 --- a/app/initialize_variables.py +++ b/app/initialize_variables.py @@ -1,93 +1,61 @@ -import os -import yaml -import requests - supported_sms_services = ['telnyx', 'twilio'] -radarr_host_url = str(os.environ['RADARR_HOST_URL']) -radarr_api_key = str(os.environ['RADARR_API_KEY']) -enable_kuma_notifications = str(os.environ['ENABLE_KUMA_NOTIFICATIONS']).lower() -sms_service = str(os.environ['SMS_SERVICE']).lower() - -headers = { - 'Content-Type': 'application/json', - 'X-Api-Key': radarr_api_key -} - -# Open the config.yaml file and see if the config is set -try: - with open('/data/config.yaml', 'r') as f: - file = yaml.load(f, Loader=yaml.FullLoader) - try: - quality_profile_id = int(file['quality_profile_id']) - - if str(file['home_domain']) != 'null': - home_domain = str(file['home_domain']) - - api_number = str(file['api_number']) - val_nums = str(file['valid_senders']) - root_folder_path = str(file['root_folder_path']) - - if enable_kuma_notifications == 'true': - notif_receivers_nums = str(file['notif_receivers']) - authorization_header_token = str(file['authorization_header_token']) - - if sms_service not in supported_sms_services: - print(f'{sms_service} is not a supported SMS service. Please choose from the supported list: {supported_sms_services}') - exit() - - if sms_service == 'telnyx': - telnyx_api_key = str(file['telnyx_api_key']) - - if sms_service == 'twilio': - twilio_account_sid = str(file['twilio_account_sid']) - twilio_auth_token = str(file['twilio_auth_token']) - - value_not_set = False - except: - print('One or more values are not set or not set correctly within the config.yaml file. Please edit the file or refer to the docs for more information.') - exit() - -except FileNotFoundError: - # Create the config.yaml file - with open('/data/config.yaml', 'w') as f: - value_not_set = True - -if value_not_set: - print('One or more values are not set or not set correctly within the config.yaml file. Please edit the file or refer to the docs for more information.') - data = requests.get(f'{radarr_host_url}/api/v3/qualityprofile', headers=headers).json() - # Open config.yaml and write each profile as a comment to the file - with open('/data/config.yaml', 'w') as f: - f.write('# Quality Profile ID\'s\n') - for entry in data: - f.write(f'# {entry["id"]} - {entry["name"]}\n') - - f.write("quality_profile_id:\n") - f.write("home_domain: null\n") - f.write("api_number: ''\n") - f.write("valid_senders: ''\n") - f.write("root_folder_path:\n") - - if enable_kuma_notifications == 'true': - f.write("notif_receivers: ''\n") - f.write("authorization_header_token: uptimekumaauthtoken\n") - - if sms_service not in supported_sms_services: - print(f'{sms_service} is not a supported SMS service. Please choose from the supported list: {supported_sms_services}') - exit() - - if sms_service == 'telnyx': - f.write("telnyx_api_key:\n") - - if sms_service == 'twilio': - f.write("twilio_account_sid:\n") - f.write("twilio_auth_token:\n") - - f.write("\n\n# INFORMATION: There should be NO trailing spaced after you enter a value,\n# this will cause errors.\n# There should be one space after the colon though (e.g. quality_profile_id: 1)\n# Check docs for information on each value.") - exit() - numbers_responses = { '1': 1, 'one': 1, '1.': 1, '2': 2, 'two': 2, '2.': 2, '3': 3, 'three': 3, '3.': 3 -}
\ No newline at end of file +} + +def init(): + global sms_service + sms_service = '' + global telnyx_api_key + telnyx_api_key = '' + global twilio_account_sid + twilio_account_sid = '' + global twilio_auth_token + twilio_auth_token = '' + global api_number + api_number = '' + global valid_senders + valid_senders = [] + global radarr_host_url + radarr_host_url = '' + global headers + headers = '' + global root_folder_path + root_folder_path = '' + global quality_profile_id + quality_profile_id = '' + global authorization_header_tokens + authorization_header_tokens= '' + global notifs_recievers + notifs_recievers = [] + global enable_jellyfin_temp_accounts + enable_jellyfin_temp_accounts = '' + global jellyfin_url + jellyfin_url = '' + global jellyfin_headers + jellyfin_headers = '' + global home_domain + home_domain = '' + global db_path + db_path = '' + + global temp_movie_ids + temp_movie_ids = {} + """ + { + 'from_number': { + 'ids': ['tmdb_id_one', 'tmdb_id_two', 'tmdb_id_three'], + 'time': 'time of request' + } + } + """ + global temp_new_account_requests + temp_new_account_requests = {} + """ + { + 'from_number': 'time' + } + """
\ No newline at end of file diff --git a/app/messagearr.py b/app/messagearr.py index 6f437c7..8e6b8e4 100644 --- a/app/messagearr.py +++ b/app/messagearr.py @@ -1,50 +1,15 @@ import flask import datetime -import requests -import sqlite3 -import humanize from create_message import create_message -from initialize_variables import * +from commands.request import request +from commands.status import status +from commands.number_response_request import number_response_request +from commands.movie_show_response_newaccount import movie_show_response_newaccount +import initialize_variables - -""" -Define variables to be used later on -""" app = flask.Flask(__name__) -db = sqlite3.connect('/data/movies.db') -cursor = db.cursor() -cursor.execute(''' - CREATE TABLE IF NOT EXISTS movies( - from_number TEXT, - movie_id TEXT, - movie_title TEXT - ) -''') -db.commit() -db.close() - -temp_movie_ids = {} -""" -{ - 'from_number': { - 'ids': ['tmdb_id_one', 'tmdb_id_two', 'tmdb_id_three'], - 'time': 'time of request' - } -} -""" -valid_senders = [] - -for number in val_nums.split(', '): - valid_senders.append(number) - -if notif_receivers_nums: - notif_receivers = [] - - for number in notif_receivers_nums.split(', '): - notif_receivers.append(number) - """ POST request route to accept incoming notifications from UptimeKuma @@ -52,9 +17,9 @@ regarding the status of certain services. Messages are sent to the 'notif_receivers' list whenever a service goes down or comes back up. """ @app.route('/kuma', methods=['POST']) -def api(): +def kuma(): # Make sure the request is coming from UptimeKuma (Configured to use this authorization token) - if flask.request.headers.get('Authorization') == authorization_header_token: + if flask.request.headers.get('Authorization') == initialize_variables.authorization_header_tokens: data = flask.request.get_json() if data['heartbeat']['status'] == 0: @@ -63,7 +28,7 @@ def api(): elif data['heartbeat']['status'] == 1: message = f"✅ {data['monitor']['name']} is up!" - for number in notif_receivers: + for number in initialize_variables.notifs_recievers: create_message(number, message) return 'OK' @@ -80,183 +45,49 @@ and then run the command if it is valid. @app.route('/incoming', methods=['POST']) def incoming(): # Get the data and define the from_number (number that sent the message) - if sms_service == 'telnyx': + if initialize_variables.sms_service == 'telnyx': from_number = flask.request.get_json()['data']['payload']['from']['phone_number'] message = str(flask.request.get_json()['data']['payload']['text']) - if sms_service == 'twilio': + if initialize_variables.sms_service == 'twilio': from_number = flask.request.form['From'] message = str(flask.request.form['Body']) # Make sure the number is a valid_sender, this stops random people from # adding movies to the library - if from_number not in valid_senders: + if from_number not in initialize_variables.valid_senders: return 'OK' - # If the message starts with /request, that means the user is trying to add a movie - if message.startswith('/request'): - # If the user has already run the /request command, delete the entry - # from the temp_movie_ids dict so that they can run the command again - if from_number in temp_movie_ids.keys(): - del temp_movie_ids[from_number] - # If the user did not include a movie title, alert them to do so - # Just check to make sure that the length of the message is greater than 9 - if len(message) <= 9: - create_message(from_number, "Please include the movie title after the /request command.\nEX: /request The Dark Knight") - return 'OK' - incoming_message = message.split(' ', 1)[1] - movie_request = incoming_message.replace(' ', '%20') - # Send a request to the radarr API to get the movie info - response = requests.get(f'{radarr_host_url}/api/v3/movie/lookup?term={movie_request}', headers=headers) - # If there are no results, alert the user - if len(response.json()) == 0: - create_message(from_number, "There were no results for that movie. Please make sure you typed the title correctly.") - return 'OK' - # If the movie is already added to the library, return a message saying so. - if response.json()[0]['added'] != '0001-01-01T05:51:00Z': - create_message(from_number, "This movie is already added to the server.\n\nIf you believe this is an error, please contact Parker.") - return 'OK' - # Define an empty message variable, we then loop through the first 3 results from the API - # If there are less than 3 results, we loop through the amount of results there are - message = "" - for i in range(min(3, len(response.json()))): - message += f"{i+1}. {response.json()[i]['folder']}\n\n" - if from_number not in temp_movie_ids.keys(): - temp_movie_ids[from_number] = { - 'ids': [], - 'time': datetime.datetime.now() - } - temp_movie_ids[from_number]['ids'].append(response.json()[i]['tmdbId']) - - message += "Reply with the number associated with the movie you want to download. EX: 1\n\nIf the movie you want is not on the list, make sure you typed the title exactly as it is spelt, or ask Parker to manually add the movie." - - create_message(from_number, message) + if message.startswith('/request'): + request(from_number, message) return 'OK' - # Elif the user responded with a variation of 1, 2, or 3 - # This means they are replying to the previous prompt, so now we need to - # add their movie choice to radarr for download - elif message.strip() in numbers_responses.keys(): - # If there is no entry for the user in the temp_movie_ids dict, that means - # they have not yet run the /request command, so alert them to do so. - if from_number not in temp_movie_ids.keys(): - create_message(from_number, "There is no current request that you can decide on. It might be that your /request command timed out due since you took too long to response. Please try again. If this issue persists, please contact Parker.") - return 'OK' - # If the time is greater than 5 minutes, delete the entry from the dict, and alert - # the user that their request timed out - if (datetime.datetime.now() - temp_movie_ids[from_number]['time']).total_seconds() / 60 > 5: - del temp_movie_ids[from_number] - create_message(from_number, "You waited too long and therefore your request has timed out.\n\nPlease try again by re-running the /request command. If this issue persists, please contact Parker.") - return 'OK' - - # Otherwise, all checks have been completed, so alert the user of the - # start of the process - create_message(from_number, "Just a moment while I add your movie to the library...") - movie_number = numbers_responses[message.strip()] - try: - tmdb_id = temp_movie_ids[from_number]['ids'][movie_number - 1] - except IndexError: - create_message(from_number, "You did not enter a valid number. Please re-send the /request command and try again. If you believe this is an error, please contact Parker.") - del temp_movie_ids[from_number] - return 'OK' - - data = requests.get(f'{radarr_host_url}/api/v3/movie/lookup/tmdb?tmdbId={tmdb_id}', headers=headers) - - data = data.json() - movie_title = data['title'] - # Change the qualityProfileId, monitored, and rootFolderPath values - data['qualityProfileId'] = quality_profile_id - data['monitored'] = True - data['rootFolderPath'] = root_folder_path - # Pass this data into a pass request to the radarr API, this will add the movie to the library - response = requests.post(f'{radarr_host_url}/api/v3/movie', headers=headers, json=data) - data = response.json() - movie_id = data['id'] - # Send message to user alerting them that the movie was added to the library - create_message(from_number, f"🎉 {data['title']} has been added to the library!\n\nTo check up on the status of your movie(s) send /status - please wait at least 5 minutes before running this command in order to get an accurate time.") - # Finally, as to not slow up the sending of the message, send this request - # Send a POST request to the radarr API to search for the movie in the indexers - requests.post(f'{radarr_host_url}/api/v3/command', headers=headers, json={'name': 'MoviesSearch', 'movieIds': [int(movie_id)]}) - # Add the movie_id to the database so that users can check up on the status of their movie - db = sqlite3.connect('/data/movies.db') - cursor = db.cursor() - cursor.execute(''' - INSERT INTO movies(from_number, movie_id, movie_title) - VALUES(?, ?, ?) - ''', (from_number, movie_id, movie_title)) - db.commit() - db.close() - - # Delete the entry from the temp_movie_ids dict - del temp_movie_ids[from_number] + # If a user responded with a number, they are responding to + # the 'request' command prompt + elif message.strip() in initialize_variables.numbers_responses.keys(): + number_response_request(from_number, message) return 'OK' - elif message.strip() == '/status': - # This returns a list of ALL movies being downloaded, but not all of them were - # requested by the user, so we need to filter out the ones that were not requested - response = requests.get(f'{radarr_host_url}/api/v3/queue/', headers=headers) - # Get all the movie_ids that were requested by the user - db = sqlite3.connect('/data/movies.db') - cursor = db.cursor() - cursor.execute(''' - SELECT movie_id, movie_title FROM movies WHERE from_number = ? - ''', (from_number,)) - movie_info = cursor.fetchall() - db.close() - # Turn the movie_id, movie_title into key value pairs - movies = {} - for movie in movie_info: - movies[movie[0]] = movie[1] - - # If the user has no movies in the database, alert them to run the /request command - if len(movies) == 0: - create_message(from_number, "You have no movies being downloaded at the moment.\n\nIf you previously added a movie, it is likely that it has finished downloading. If you believe this is an error, please contact Parker.") - return 'OK' + elif message.startswith('/status'): + status(from_number) + return 'OK' - message = "" - # Loop through the response from the radarr API and filter out the movies that were not requested by the user - for movie in response.json()['records']: - movie_id = str(movie['movieId']) - if movie_id in movies.keys(): - if movie['status'] == 'downloading': - # Humanize the time_left value - try: - time_left = humanize.precisedelta(datetime.datetime.strptime(movie['timeleft'], '%H:%M:%S') - datetime.datetime.strptime('00:00:00', '%H:%M:%S'), minimum_unit='seconds') - except ValueError: - # Sometimes movie downloads take a long time and include days in the time_left value - # This is formated as 1.00:00:00 - time_left = humanize.precisedelta(datetime.datetime.strptime(movie['timeleft'], '%d.%H:%M:%S') - datetime.datetime.strptime('00:00:00', '%H:%M:%S'), minimum_unit='seconds') - except KeyError: - time_left = 'Unknown' + elif message.startswith('/newaccount'): + if initialize_variables.enable_jellyfin_temp_accounts.lower() == 'true': + # If number is already in the temp dict, delete it so that they can redo the request + if from_number in initialize_variables.temp_new_account_requests.keys(): + del initialize_variables.temp_new_account_requests[from_number] - message += f"📥 {movies[movie_id]} - {time_left}\n" - else: - message += f"{movies[movie_id]} - {str(movie['status']).upper()}\n" + create_message(from_number, "Will you be watching a TV show or a movie?\n\nRespond with 'show' for TV show, 'movie' for movies") + initialize_variables.temp_new_account_requests[from_number] = datetime.datetime.now() + return 'OK' - # If the message is empty, that means the user has no movies being downloaded - # Or, no download was found for the movie they requested - if message == "": - # For all movie IDs within the database - for movie_id in movies.keys(): - response = requests.get(f'{radarr_host_url}/api/v3/movie/{movie_id}', headers=headers) - # This means that there is no current download, and no file has been found - # MOST likely means a download just wasn't found, so alert the user - data = response.json() - if data['hasFile'] == False: - message += f"{movies[movie_id]} - NOT FOUND\n\nThis means a download was not found for the movie(s), if this is a brand new movie that is likely the reason. If the movie has already been released on DVD/Blu-Ray, please contact Parker." + # User must be responding to above prompt + elif message.strip().lower() in ['show', 'movie']: + if initialize_variables.enable_jellyfin_temp_accounts.lower() == 'true': + movie_show_response_newaccount(from_number, message) + return 'OK' - # If the message is still empty, that means the user has no movies being downloaded - if message != "": - create_message(from_number, message) - return 'OK' - else: - create_message(from_number, "You have no movies being downloaded at the moment.\n\nIf you previously added a movie, it is likely that it has finished downloading. If you believe this is an error, please contact Parker.") - return 'OK' - # Otherwise, add another part to the message containing movie data - else: - message += "\n\nIf movies consistently show as 'WARNING' or 'QUEUED' or any other error over multiple hours, please contact Parker." - create_message(from_number, message) - return 'OK' # No valid commands were found, so just return else: return 'OK' @@ -265,12 +96,12 @@ def incoming(): # Handle 405 errors - when a user attempts a GET request on a POST only route @app.errorhandler(405) def method_not_allowed(e): - if home_domain != 'None': - return flask.redirect(home_domain) + if initialize_variables.home_domain != 'None': + return flask.redirect(initialize_variables.home_domain) return 'Method Not Allowed' @app.errorhandler(404) def page_not_found(e): - if home_domain != 'None': - return flask.redirect(home_domain) - return 'Page Not Found' + if initialize_variables.home_domain != 'None': + return flask.redirect(initialize_variables.home_domain) + return 'Page Not Found'
\ No newline at end of file diff --git a/app/validate_config.py b/app/validate_config.py new file mode 100644 index 0000000..8cad0f5 --- /dev/null +++ b/app/validate_config.py @@ -0,0 +1,261 @@ +import configparser +import os +from simple_chalk import chalk +import validators +import requests +import initialize_variables + +def write_to_config(config): + IN_DOCKER = os.environ.get('IN_DOCKER', False) + if IN_DOCKER: + with open('/data/config.ini', 'w') as configfile: + config.write(configfile) + else: + with open('config.ini', 'w') as configfile: + config.write(configfile) + + +def validate_config(file_contents): + config = configparser.ConfigParser() + config.read_string(file_contents) + + # Check SMS service + if config['REQUIRED']['SMS_SERVICE'].lower() not in initialize_variables.supported_sms_services: + print(chalk.red(f'Invalid or empty SMS_SERVICE option passed. Please choose from the supported list: {initialize_variables.supported_sms_services}')) + exit() + initialize_variables.sms_service = config['REQUIRED']['SMS_SERVICE'].lower() + + # Check API key is Telnyx is selected + if config['REQUIRED']['SMS_SERVICE'].lower() == 'telnyx': + try: + if not config['REQUIRED']['TELNYX_API_KEY']: + print(chalk.red('Empty TELNYX_API_KEY option passed. Please enter the API key for your Telnyx account.')) + exit() + except KeyError: + config['REQUIRED']['TELNYX_API_KEY'] = '' + write_to_config(config) + print(chalk.red('Empty TELNYX_API_KEY option passed. Please enter the API key for your Telnyx account.')) + initialize_variables.telnyx_api_key = config['REQUIRED']['TELNYX_API_KEY'] + + # Check account SID and auth token is Twilio is selected + if config['REQUIRED']['SMS_SERVICE'].lower() == 'twilio': + try: + if not config['REQUIRED']['TWILIO_ACCOUNT_SID'] or not config['REQUIRED']['TWILIO_AUTH_TOKEN']: + print(chalk.red('Empty TWILIO_ACCOUNT_SID or TWILIO_AUTH_TOKEN option passed. Please enter the account SID and auth token for your Twilio account.')) + exit() + except KeyError: + config['REQUIRED']['TWILIO_ACCOUNT_SID'] = '' + config['REQUIRED']['TWILIO_AUTH_TOKEN'] = '' + write_to_config(config) + print(chalk.red('Empty TWILIO_ACCOUNT_SID or TWILIO_AUTH_TOKEN option passed. Please enter the account SID and auth token for your Twilio account.')) + initialize_variables.twilio_account_sid = config['REQUIRED']['TWILIO_ACCOUNT_SID'] + initialize_variables.twilio_auth_token = config['REQUIRED']['TWILIO_AUTH_TOKEN'] + + # Check API_NUMBER + if not config['REQUIRED']['API_NUMBER']: + print(chalk.red('Empty API_NUMBER option passed. Please enter an internationally formatted phone number with no spaces.')) + exit() + if len(config['REQUIRED']['API_NUMBER']) < 12 or len(config['REQUIRED']['API_NUMBER']) > 13 or not config['REQUIRED']['API_NUMBER'].startswith('+'): + print(chalk.red('API_NUMBER must be a valid international phone number with no spaces (e.g. +15459087689)')) + exit() + initialize_variables.api_number = config['REQUIRED']['API_NUMBER'] + + # Check VALID_SENDERS + if not config['REQUIRED']['VALID_SENDERS']: + print(chalk.red('Empty VALID_SENDERS option passed. Please enter a command separated list of internationally formatted phone numbers (e.g. +15359087689, +15256573847)')) + exit() + + for sender in config['REQUIRED']['VALID_SENDERS'].split(', '): + if len(sender) < 12 or len(sender) > 13 or not sender.startswith('+'): + print(chalk.red('At least one number within VALID_SENDER is malformed. Please enter a command separated list of internationally formatted phone numbers (e.g. +15359087689, +15256573847)')) + exit() + else: + initialize_variables.valid_senders.append(sender) + + # Check RADARR_HOST_URL + if not validators.url(config['REQUIRED']['RADARR_HOST_URL']): + print(chalk.red('Invalid or empty URL passed to RADARR_HOST_URL. Pass a valid URL (e.g. http://localhost:7878)')) + exit() + initialize_variables.radarr_host_url = config['REQUIRED']['RADARR_HOST_URL'] + + # Check RADARR_API_KEY + if not config['REQUIRED']['RADARR_API_KEY']: + print(chalk.red('Empty RADARR_API_KEY passed. Obtain an API key from your Radarr instance and paste it in this option.')) + exit() + + initialize_variables.headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': config['REQUIRED']['RADARR_API_KEY'] + } + + # Make sure connection to Radarr API can be established + try: + requests.get(config['REQUIRED']['RADARR_HOST_URL'], headers=initialize_variables.headers) + except requests.exceptions.ConnectionError: + print(chalk.red('Could not connect to Radarr API. Please check your RADARR_HOST_URL and RADARR_API_KEY')) + exit() + + # Check ROOT_FOLDER_PATH + if not config['REQUIRED']['ROOT_FOLDER_PATH']: + print(chalk.red('Empty ROOT_FOLDER_PATH option passed. Please enter a path to a folder within your Radarr instance.')) + exit() + initialize_variables.root_folder_path = config['REQUIRED']['ROOT_FOLDER_PATH'] + + # Check QUALITY_PROFILE_ID + data = requests.get(f'{config["REQUIRED"]["RADARR_HOST_URL"]}/api/v3/qualityprofile', headers=initialize_variables.headers).json() + all_ids = [] + for entry in data: + all_ids.append(str(entry['id'])) + + if not config['REQUIRED']['QUALITY_PROFILE_ID'] or config['REQUIRED']['QUALITY_PROFILE_ID'] not in all_ids: + config['AVAILABLE_QUALITY_IDS'] = {} + for entry in data: + config['AVAILABLE_QUALITY_IDS'][str(entry['id'])] = entry['name'] + + print(chalk.red('Empty or invalid QUALITY_PROFILE_ID passed. Pass one of the valid IDs which are now listed within the config.ini file.')) + write_to_config(config) + exit() + initialize_variables.quality_profile_id = config['REQUIRED']['QUALITY_PROFILE_ID'] + + # Check ENABLE_KUMA_NOTIFICATIONS + if not config['REQUIRED']['ENABLE_KUMA_NOTIFICATIONS'] or config['REQUIRED']['ENABLE_KUMA_NOTIFICATIONS'].lower() not in ['true', 'false']: + print(chalk.red('ENABLE_KUMA_NOTIFICATIONS must be a boolean value (true/false)')) + exit() + + if config['REQUIRED']['ENABLE_KUMA_NOTIFICATIONS'].lower() == 'true': + # Check existence + try: + if not config['KUMA_NOTIFICATIONS']['AUTHORIZATION_HEADER_TOKEN']: + print(chalk.red('Empty AUTHORIZATION_HEADER_TOKEN passed. Make sure to set your authorization header in Uptime Kuma and copy the key here.')) + exit() + except KeyError: + config['KUMA_NOTIFICATIONS']['AUTHORIZATION_HEADER_TOKEN'] = '' + write_to_config(config) + print(chalk.red('Empty AUTHORIZATION_HEADER_TOKEN passed. Make sure to set your authorization header in Uptime Kuma and copy the key here.')) + exit() + initialize_variables.authorization_header_tokens = config['KUMA_NOTIFICATIONS']['AUTHORIZATION_HEADER_TOKEN'] + # Check existence + try: + if not config['KUMA_NOTIFICATIONS']['NOTIF_RECIEVERS']: + print(chalk.red('Empty NOTIF_RECIEVERS passed. This should be a comma separated list of the numbers of people who should recieve uptime notifications - formatted the same way as VALID_SENDERS.')) + exit() + except KeyError: + config['KUMA_NOTIFICATIONS']['NOTIF_RECIEVERS'] = '' + write_to_config(config) + print(chalk.red('Empty NOTIF_RECIEVERS passed. This should be a comma separated list of the numbers of people who should recieve uptime notifications - formatted the same way as VALID_SENDERS.')) + exit() + + # Check validity of NOTIF_RECIEVERS + for sender in config['KUMA_NOTIFICATIONS']['NOTIF_RECIEVERS'].split(', '): + if len(sender) < 12 or len(sender) > 13 or not sender.startswith('+'): + print(chalk.red('At least one number within NOTIF_RECIEVERS is malformed. Please enter a command separated list of internationally formatted phone numbers (e.g. +15359087689, +15256573847)')) + exit() + else: + initialize_variables.notifs_recievers.append(sender) + + # Check ENABLE_JELLYFIN_TEMP_ACCOUNTS + if not config['REQUIRED']['ENABLE_JELLYFIN_TEMP_ACCOUNTS'] or config['REQUIRED']['ENABLE_JELLYFIN_TEMP_ACCOUNTS'].lower() not in ['true', 'false']: + print(chalk.red('ENABLE_JELLYFIN_TEMP_ACCOUNTS must be a boolean value (true/false)')) + exit() + initialize_variables.enable_jellyfin_temp_accounts = config['REQUIRED']['ENABLE_JELLYFIN_TEMP_ACCOUNTS'].lower() + + if config['REQUIRED']['ENABLE_JELLYFIN_TEMP_ACCOUNTS'].lower() == 'true': + # Check existence + try: + if not config['JELLYFIN_ACCOUNTS']['JELLYFIN_URL']: + print(chalk.red('Empty URL passed to JELLYFIN_URL. Pass a valid URL (e.g. http://localhost:8096)')) + exit() + except KeyError: + config['JELLYFIN_ACCOUNTS']['JELLYFIN_URL'] = '' + write_to_config(config) + print(chalk.red('Empty URL passed to JELLYFIN_URL. Pass a valid URL (e.g. http://localhost:8096)')) + exit() + # Check URL validity + if not validators.url(config['JELLYFIN_ACCOUNTS']['JELLYFIN_URL']): + print(chalk.red('Invalid URL passed to JELLYFIN_URL. Pass a valid URL (e.g. http://localhost:8096)')) + exit() + initialize_variables.jellyfin_url = config['JELLYFIN_ACCOUNTS']['JELLYFIN_URL'] + + # Check existence + try: + if not config['JELLYFIN_ACCOUNTS']['JELLYFIN_API_KEY']: + print(chalk.red('Empty JELLYFIN_API_KEY passed. Create a Jellyfin API key in your Jellyfin dashboard and pass it here.')) + exit() + except KeyError: + config['JELLYFIN_ACCOUNTS']['JELLYFIN_API_KEY'] = '' + write_to_config(config) + print(chalk.red('Empty JELLYFIN_API_KEY passed. Create a Jellyfin API key in your Jellyfin dashboard and pass it here.')) + exit() + + # Make sure connection to Jellyfin API can be established + initialize_variables.jellyfin_headers = { + 'Content-Type': 'application/json', + 'Authorization': f"MediaBrowser Client=\"other\", device=\"Messagearr\", DeviceId=\"totally-unique-device-id\", Version=\"0.0.0\", Token=\"{config['JELLYFIN_ACCOUNTS']['JELLYFIN_API_KEY']}\"" + } + + response = requests.get(f"{config['JELLYFIN_ACCOUNTS']['JELLYFIN_URL']}/Users", headers=initialize_variables.jellyfin_headers) + if response.status_code != 200: + print(chalk.red('Could not connect to Jellyfin API. Please check your JELLYFIN_URL and JELLYFIN_API_KEY')) + exit() + + # Validate home domain if it is set + if config['OPTIONAL']['HOME_DOMAIN']: + if not validators.url(config['OPTIONAL']['HOME_DOMAIN']): + print(chalk.red('Invalid HOME_DOMAIN passed. Please enter a valid url (e.g. https://example.com)')) + exit() + else: + initialize_variables.home_domain = config['OPTIONAL']['HOME_DOMAIN'] + +""" +This method is called before starting the application - to make and validate the configuration +""" +def make_config(): + # Attempt to open and validate the configuration file + try: + with open('config.ini', 'r') as config: + file_contents = config.read() + validate_config(file_contents) + + except FileNotFoundError: + try: + with open('/data/config.ini', 'r') as config: + file_contents = config.read() + validate_config(file_contents) + + except FileNotFoundError: + # Create the config.ini file + config = configparser.ConfigParser() + config['REQUIRED'] = { + 'SMS_SERVICE': '', + 'API_NUMBER': '', + 'VALID_SENDERS': '', + 'RADARR_HOST_URL': 'http://', + 'RADARR_API_KEY': '', + 'ROOT_FOLDER_PATH': '', + 'QUALITY_PROFILE_ID': '', + 'ENABLE_KUMA_NOTIFICATIONS': '', + 'ENABLE_JELLYFIN_TEMP_ACCOUNTS': '' + } + + config['OPTIONAL'] = { + 'HOME_DOMAIN': '' + } + + config['KUMA_NOTIFICATIONS'] = { + 'AUTHORIZATION_HEADER_TOKEN': '', + 'NOTIF_RECIEVERS': '' + } + + config['JELLYFIN_ACCOUNTS'] = { + 'JELLYFIN_URL': '', + 'JELLYFIN_API_KEY': '' + } + + IN_DOCKER = os.environ.get('IN_DOCKER', False) + if IN_DOCKER: + with open('/data/config.ini', 'w') as configfile: + config.write(configfile) + + else: + with open('config.ini', 'w') as configfile: + config.write(configfile)
\ No newline at end of file diff --git a/app/wsgi.py b/app/wsgi.py index d3cd6c6..9b73735 100644 --- a/app/wsgi.py +++ b/app/wsgi.py @@ -5,8 +5,14 @@ import multiprocessing import asyncio from db_removal import sched +import initialize_variables +import validate_config +import db_setup if __name__ == '__main__': + initialize_variables.init() + db_setup.setup_db() + validate_config.make_config() multiprocessing.Process(target=sched.start()).start() print('Starting server...') config = Config() |