aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/create_message.py11
-rw-r--r--app/db_removal.py33
-rw-r--r--app/initialize_variables.py58
-rw-r--r--app/messagearr.py236
-rw-r--r--app/wsgi.py14
5 files changed, 352 insertions, 0 deletions
diff --git a/app/create_message.py b/app/create_message.py
new file mode 100644
index 0000000..2e23c14
--- /dev/null
+++ b/app/create_message.py
@@ -0,0 +1,11 @@
+import telnyx
+from initialize_variables import sms_service, sms_api_key, api_number
+
+def create_message(number, message):
+ if sms_service == 'telnyx':
+ telnyx.api_key = sms_api_key
+ telnyx.Message.create(
+ from_=api_number,
+ to=number,
+ text=message
+ ) \ No newline at end of file
diff --git a/app/db_removal.py b/app/db_removal.py
new file mode 100644
index 0000000..e03ea2a
--- /dev/null
+++ b/app/db_removal.py
@@ -0,0 +1,33 @@
+from apscheduler.schedulers.background import BackgroundScheduler
+import sqlite3
+import requests
+
+from initialize_variables import radarr_host, headers
+
+# 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')
+ cursor = db.cursor()
+ # First get all of the movie ids in the database
+ cursor.execute('''
+ SELECT movie_id FROM movies
+ ''')
+ movie_ids = cursor.fetchall()
+ # Get all of the movie_ids that are currently downloading/queued and/or missing
+ response = requests.get(f'{radarr_host}/api/v3/queue/', headers=headers)
+ current_movie_ids = []
+ for movie in response.json()['records']:
+ current_movie_ids.append(str(movie['movieId']))
+
+ # Loop through the movie_ids in the database, if they are not in the current_movie_ids list,
+ # that means they are not currently downloading/queued, so delete them from the database
+ for movie_id in movie_ids:
+ if movie_id[0] not in current_movie_ids:
+ cursor.execute('''
+ DELETE FROM movies WHERE movie_id = ?
+ ''', (movie_id[0],))
+ db.commit()
+ db.close() \ No newline at end of file
diff --git a/app/initialize_variables.py b/app/initialize_variables.py
new file mode 100644
index 0000000..6da1c34
--- /dev/null
+++ b/app/initialize_variables.py
@@ -0,0 +1,58 @@
+import os
+import yaml
+import requests
+
+supported_sms_services = ['telnyx']
+
+
+radarr_host = os.environ['RADARR_HOST']
+radarr_api_key = os.environ['RADARR_API_KEY']
+try:
+ home_domain = os.environ['HOME_DOMAIN']
+except:
+ home_domain = None
+api_number = os.environ['API_NUMBER']
+val_nums = os.environ['VALID_SENDERS']
+root_folder_path = os.environ['ROOT_FOLDER_PATH']
+sms_service = os.environ['SMS_SERVICE']
+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()
+sms_api_key = os.environ['SMS_API_KEY']
+
+headers = {
+ 'Content-Type': 'application/json',
+ 'X-Api-Key': radarr_api_key
+}
+
+numbers_responses = {
+ '1': 1, 'one': 1, '1.': 1,
+ '2': 2, 'two': 2, '2.': 2,
+ '3': 3, 'three': 3, '3.': 3
+}
+
+# Open the quality_profile_id.yaml file and see if the quality_profile_id is set
+try:
+ with open('/data/quality_profile_id.yaml', 'r') as f:
+ file = yaml.load(f, Loader=yaml.FullLoader)
+ try:
+ quality_profile_id = int(file['quality_profile_id'])
+ except:
+ print('quality_profile_id is not set or is invalid. Please edit the quality_profile_id.yaml file and add the quality_profile_id from one of the integer values listed within the file')
+ exit()
+except FileNotFoundError:
+ # Create the quality_profile_id.yaml file
+ with open('/data/quality_profile_id.yaml', 'w') as f:
+ quality_profile_id = None
+
+if not quality_profile_id:
+ print('No quality_profile_id found. Please edit the quality_profile_id.yaml file and add the quality_profile_id from one of the integer values listed within the file')
+ data = requests.get(f'{radarr_host}/api/v3/qualityprofile', headers=headers).json()
+ # Open quality_profile_id.yaml and write each profile as a comment to the file
+ with open('/data/quality_profile_id.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: ")
+ exit() \ No newline at end of file
diff --git a/app/messagearr.py b/app/messagearr.py
new file mode 100644
index 0000000..9a58460
--- /dev/null
+++ b/app/messagearr.py
@@ -0,0 +1,236 @@
+import flask
+import datetime
+import requests
+import sqlite3
+import humanize
+
+from create_message import create_message
+from initialize_variables import *
+
+
+"""
+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)
+
+"""
+POST request route to accept incoming message from the SMS API,
+then process the incoming message in order to see if it is a valid command
+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)
+ data = flask.request.get_json()
+ from_number = data['data']['payload']['from']['phone_number']
+ # 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:
+ return 'OK'
+ # If the message starts with /request, that means the user is trying to add a movie
+ unparsed = str(data['data']['payload']['text'])
+ if unparsed.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(unparsed) <= 9:
+ create_message(from_number, "Please include the movie title after the /request command.\nEX: /request The Dark Knight")
+ return 'OK'
+
+ incoming_message = str(data['data']['payload']['text']).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}/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)
+ 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 str(data['data']['payload']['text']).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[str(data['data']['payload']['text']).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}/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}/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}/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]
+ return 'OK'
+
+ elif str(data['data']['payload']['text']).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}/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'
+
+ 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')
+ 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'{radarr_host}/api/v3/movie/lookup/tmdb?tmdbId={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
+ if response.json()['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."
+
+ # 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'
+
+
+# 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:
+ return flask.redirect(home_domain, code=302)
+ else:
+ return 'Method not allowed', 405
+
+@app.errorhandler(404)
+def page_not_found(e):
+ if home_domain:
+ return flask.redirect(home_domain, code=302)
+ else:
+ return 'Page not found', 404 \ No newline at end of file
diff --git a/app/wsgi.py b/app/wsgi.py
new file mode 100644
index 0000000..d3cd6c6
--- /dev/null
+++ b/app/wsgi.py
@@ -0,0 +1,14 @@
+from hypercorn.config import Config
+from hypercorn.asyncio import serve
+from messagearr import app
+import multiprocessing
+import asyncio
+
+from db_removal import sched
+
+if __name__ == '__main__':
+ multiprocessing.Process(target=sched.start()).start()
+ print('Starting server...')
+ config = Config()
+ config.bind = ["0.0.0.0:4545"]
+ asyncio.run(serve(app, config)) \ No newline at end of file