Create CordArr

This commit is contained in:
Parker M. 2024-05-18 20:06:51 -05:00
parent f0ec1c5a89
commit 32ab780b46
No known key found for this signature in database
GPG Key ID: 95CD2E0C7E329F2A
14 changed files with 866 additions and 2 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
__pycache__
config.ini
cordarr.db
.DS_Store

View File

@ -1,2 +1,58 @@
# CordArr
Request new movies to Radarr and create temporary Jellyfin account through Discord commands.
<h1 align="center">
<br>
<img src="cordarr.png" width="200" alt="Guava Image"></a>
<br>
CordArr<br>
</h1>
<h3 align="center">
Control your Radarr/Sonarr library and create Jellyfin accounts in Discord
</h3>
<p align="center">
<a href="https://github.com/Rapptz/discord.py/">
<img src="https://img.shields.io/badge/discord-py-blue.svg" alt="discord.py">
</a>
<a href="https://github.com/psf/black">
<img src="https://img.shields.io/badge/code%20style-black-000000.svg" alt="Code Style: Black">
</a>
<a href="https://makeapullrequest.com">
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg">
</a>
</p>
# Overview
CordArr is a self-hosted Discord bot that allows you to add new movies or shows to your Radarr/Sonarr libraries, and allow users to create temporary Jellyfin accounts on your server.
# Instructions
CordArr is built on Python and requires you to install all of the dependencies in the `requirements.txt` file. To do this, you can run the pip install command like `pip install -r requirements.txt`
On first run you will likely get a critical warning in your console, don't worry, this is expected. It will automatically create a `config.ini` file for you in the root of the directory with all of the necessary configuration options.
Fill out the configuration options, then re-run the bot, and everything *should* just work. For information on each configuration option, look below.
Field | Description
--- | ---
BOT_TOKEN | The token for your bot. Create a bot at [discord.com/developers](https://discord.com/developers)
RADARR_HOST_URL | URL for your Radarr instance (e.g. http://localhost:7878)
RADARR_API_KEY | API key for Radarr, found in `Settings > General > API Key`
ROOT_FOLDER_PATH | Path for media root folder, found at the bottom of the page in `Settings > Media Management`
QUALITY_PROFILE_ID | ID for the quality profile on Radarr (in order to get a list of your quality profiles and their IDs, set the other fields first, then re-run CordArr, the config.ini file will update with this information)
ENABLE_JELLYFIN_TEMP_ACCOUNT | `true/false` : Whether or not to enable the `/newaccount` command allowing users to create temporary Jellyfin accounts
<br>
If you choose to enable the Jellyfin temp accounts features, these fields will also be required
Field | Description
--- | ---
JELLYFIN_URL | URL for your Jellyfin server (e.g. http://localhost:8096)
JELLYFIN_API_KEY | API key for Jellyfin - can be created in `Dashboard > API Keys`
ACCOUNT_TIME | Amount of time, in hours, that temporary Jellyfin accounts should exist before being deleted
<br>
<br>
If you have any questions, feel free to email at [contact@pkrm.dev](mailto:contact@pkrm.dev). Thank you for checking out Guava, and happy coding.

51
code/bot.py Normal file
View File

@ -0,0 +1,51 @@
import discord
from discord.ext import commands
from discord.ext import tasks
import datetime
import sqlite3
import os
from validate_config import create_config
from global_variables import LOG, BOT_TOKEN
class MyBot(commands.Bot):
def __init__(self):
super().__init__(
command_prefix="#",
intents=discord.Intents.default(),
)
async def setup_hook(self):
create_config()
delete_old_temp_accounts.start()
for ext in os.listdir("./code/cogs"):
if ext.endswith(".py"):
await self.load_extension(f"cogs.{ext[:-3]}")
bot = MyBot()
bot.remove_command("help")
@bot.event
async def on_ready():
LOG.info(f"{bot.user} has connected to Discord.")
@tasks.loop(seconds=60)
async def delete_old_temp_accounts():
# Delete all of the temporary Jellyfin accounts that have passed
# their expiration time
db = sqlite3.connect("cordarr.db")
cursor = db.cursor()
cursor.execute(
"DELETE FROM jellyfin_accounts WHERE deletion_time < ?",
(datetime.datetime.now(),),
)
db.commit()
db.close()
if __name__ == "__main__":
bot.run(BOT_TOKEN)

27
code/cogs/error.py Normal file
View File

@ -0,0 +1,27 @@
import discord
from discord.ext import commands
from discord import app_commands
class slash_handlers(commands.Cog):
def __init__(self, bot):
self.bot = bot
bot.tree.on_error = self.on_error
async def on_error(self, interaction: discord.Interaction, error):
if (
isinstance(error, app_commands.CheckFailure)
and interaction.command.name == "newaccount"
):
embed = discord.Embed(
title="Jellyfin Account Creation Disabled",
description=f"The owner of {self.bot.user.mention} has disabled the ability to create temporary Jellyfin accounts. Contact an administrator for more information.",
color=0xD01B86
)
await interaction.response.send_message(embed=embed, ephemeral=True)
else:
raise error
async def setup(bot: commands.Bot):
await bot.add_cog(slash_handlers(bot))

59
code/cogs/newaccount.py Normal file
View File

@ -0,0 +1,59 @@
import discord
from discord import app_commands
from discord.ext import commands
import sqlite3
from func.jellyfin import create_jellyfin_account
from global_variables import JELLYFIN_URL, ENABLE_JELLYFIN_TEMP_ACCOUNTS, ACCOUNT_TIME
class NewAccount(commands.Cog):
def __init__(self, bot):
self.bot = bot
@app_commands.command()
@app_commands.check(lambda inter: ENABLE_JELLYFIN_TEMP_ACCOUNTS)
async def newaccount(self, interaction: discord.Interaction):
"Create a new temporary Jellyfin account"
# Make sure the user doesn't already have an account
db = sqlite3.connect("cordarr.db")
cursor = db.cursor()
cursor.execute(
"SELECT * FROM jellyfin_accounts WHERE user_id = ?", (interaction.user.id,)
)
if cursor.fetchone():
embed = discord.Embed(
title="Account Already Exists",
description="Look at your previous DMs with me to find your account information. You will be permitted to create a new account after your current one expires.",
color=0xD01B86
)
return await interaction.response.send_message(embed=embed, ephemeral=True)
# Create a new Jellyfin account for the user
response = create_jellyfin_account(interaction.user.id)
if response:
embed = discord.Embed(
title="Account Created",
description="Your account has been successfully created. Check your DMs for your account information.",
color=0xD01B86
)
await interaction.response.send_message(embed=embed, ephemeral=True)
# Send the user their account information
embed = discord.Embed(
title="Jellyfin Account Information",
description=f"Here is your temporary account information. You will need this to access the Jellyfin server.\n\n**Server URL:** `{JELLYFIN_URL}`\n**Username:** `{response[0]}`\n**Password:** `{response[1]}`\n\nYour account will be automatically deleted in {ACCOUNT_TIME} hours.",
color=0xD01B86
)
await interaction.user.send(embed=embed)
else:
embed = discord.Embed(
title="Unknown Error Occured",
description="Error creating Jellyfin account. Please try again. If the error persists, contact an administrator.",
color=0xD01B86
)
return await interaction.response.send_message(embed=embed, ephemeral=True)
async def setup(bot):
await bot.add_cog(NewAccount(bot))

56
code/cogs/request.py Normal file
View File

@ -0,0 +1,56 @@
import discord
from discord import app_commands
from discord.ext import commands
from func.radarr import get_movies, AddMovieView
class Request(commands.GroupCog, name="request"):
def __init__(self, bot):
self.bot = bot
@app_commands.command(name="movie")
@app_commands.describe(name="Name of the movie to add")
async def request_movie(self, interaction: discord.Interaction, name: str):
"Request a movie to be added to the Radarr library"
get_movies_response = get_movies(name)
if get_movies_response == "NO RESULTS":
embed = discord.Embed(
title="No Results",
description="No results were found for the given movie name. If you are unable to find the movie, contact an administrator to have it added manually.",
color=0xD01B86
)
return await interaction.response.send_message(embed=embed, ephemeral=True)
if get_movies_response == "ALREADY ADDED":
embed = discord.Embed(
title="Already Added",
description="The movie you are trying to add has already been added to the Radarr library.\n\nYou can check the download status of your requests movies by running the `/status` command.",
color=0xD01B86
)
return await interaction.response.send_message(embed=embed, ephemeral=True)
movies, tmdb_ids = get_movies_response
embed = discord.Embed(
title="Results Found",
description="Please select the movie you would like to add from the dropdown below.",
color=0xD01B86
)
view = AddMovieView(movies, tmdb_ids)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
@app_commands.command(name="show")
@app_commands.describe(name="Name of the show/series to add")
async def request_show(self, interaction: discord.Interaction, name: str):
"Request a show/series to be added to the Sonarr library"
embed = discord.Embed(
title="Coming Soon",
description="This feature is not yet implemented. Check back later.",
color=0xD01B86
)
await interaction.response.send_message(embed=embed, ephemeral=True)
async def setup(bot):
await bot.add_cog(Request(bot))

111
code/cogs/status.py Normal file
View File

@ -0,0 +1,111 @@
import discord
from discord import app_commands
from discord.ext import commands
import requests
import sqlite3
import datetime
import humanize
from global_variables import RADARR_HOST_URL, RADARR_HEADERS
class Status(commands.Cog):
def __init__(self, bot):
self.bot = bot
@app_commands.command()
async def status(self, interaction: discord.Interaction):
"Get the status of the movies you have requested"
# Get all the movie_ids that were requested by the user
db = sqlite3.connect("cordarr.db")
cursor = db.cursor()
cursor.execute(
"SELECT movie_id, movie_title FROM movies WHERE user_id = ?",
(interaction.user.id,),
)
requested_movies = cursor.fetchall()
users_movies = {} # Dictionary to store the movies that the user has requested
for movie_id, movie_title in requested_movies:
users_movies[movie_id] = movie_title
# If theres no movies, return a message saying so
if not users_movies:
embed = discord.Embed(
title="No Movies Requested",
description="You have no movies being downloaded at the moment. If you previously added a movie, it is likely that it has finished downloading. If you believe this is an error, please contact an administrator.",
color=0xD01B86
)
return await interaction.response.send_message(embed=embed, ephemeral=True)
# Otherwise, create the default embed to display the movies being downloaded
embed = discord.Embed(
title="Movies Requested",
description="Here are the movies you have requested that are currently being downloaded:\n",
color=0xD01B86
)
# Now, we get the download status of all movies from the Radarr queue
response = requests.get(
f"{RADARR_HOST_URL}/api/v3/queue/", headers=RADARR_HEADERS
).json()
count = 0
added_movie_ids = []
for movie in response["records"]:
movie_id = movie["movieId"]
# If the movie is user requested and is being downloaded
if movie_id in users_movies.keys():
count += 1
added_movie_ids.append(movie_id)
if movie["status"] == "downloading":
# Humanize the download time left, or result to 'Unknown
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 movies will download extremely show and therefore might
# show 'days' in the time left, so strptime appropriately
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 or ValueError:
time_left = "Unknown"
# Add all the information
embed.description += f"\n{count}. **{users_movies[movie_id]}** - Time Left: ` {time_left} `"
else:
embed.description += f"\n{count}. **{users_movies[movie_id]}** - Status: `{str(movie['status']).upper()}`"
# If a movie wasn't found in the Radarr queue, then it has either finished downloading
# or the movie was never found for download
if len(added_movie_ids) != len(users_movies.keys()):
# Grab all of the "missing" movies to see if a movie is missing or finished downloading
response = requests.get(
f"{RADARR_HOST_URL}/api/v3/wanted/missing", headers=RADARR_HEADERS
).json()
for movie in response["records"]:
movie_id = movie["id"]
if movie_id in users_movies.keys() and movie_id not in added_movie_ids:
count += 1
added_movie_ids.append(movie_id)
embed.description += f"\n{count}. **{users_movies[movie_id]}** - Status: ` NOT FOUND `"
# If there are still movies that haven't been added to the embed, then they
# have finished downloading and can be removed from the database
for movie_id in users_movies.keys():
if movie_id not in added_movie_ids:
cursor.execute(
"DELETE FROM movies WHERE user_id = ? AND movie_id = ?",
(interaction.user.id, movie_id),
)
db.commit()
db.close()
await interaction.response.send_message(embed=embed, ephemeral=True)
async def setup(bot):
await bot.add_cog(Status(bot))

33
code/cogs/tree_sync.py Normal file
View File

@ -0,0 +1,33 @@
from discord.ext import commands
from discord import Object
class TreeSync(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.command()
@commands.dm_only()
@commands.is_owner()
async def sync(self, ctx: commands.Context, *, guild: Object = None) -> None:
if not guild or guild == None:
await self.bot.tree.sync()
await ctx.author.send("Synced commands globally")
return
elif guild != None:
self.bot.tree.copy_global_to(guild=guild)
await self.bot.tree.sync(guild=guild)
await ctx.author.send(f"Synced the tree to 1 test guild.")
@sync.error
async def error_sync(self, ctx, error):
if isinstance(error, commands.errors.PrivateMessageOnly):
pass
else:
await ctx.author.send("That is not a valid guild ID")
async def setup(bot):
await bot.add_cog(TreeSync(bot))

64
code/func/jellyfin.py Normal file
View File

@ -0,0 +1,64 @@
import datetime
import requests
import random
import string
import sqlite3
from global_variables import JELLYFIN_URL, JELLYFIN_HEADERS, ACCOUNT_TIME
"""
Create a new Jellyfin account for the user and return the username and password
"""
def create_jellyfin_account(user_id):
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=ACCOUNT_TIME)
# Create the new Jellyfin account
request_1 = requests.post(
f"{JELLYFIN_URL}/Users/New",
headers=JELLYFIN_HEADERS,
json={"Name": username, "Password": password},
)
if request_1.status_code != 200:
return False
# Get the user ID of the new account
jellyfin_user_id = request_1.json()["Id"]
# Get the account policy and make edits
request_2 = requests.get(
f"{JELLYFIN_URL}/Users/{jellyfin_user_id}", headers=JELLYFIN_HEADERS
)
if request_2.status_code != 200:
return False
account_policy = request_2.json()
account_policy["Policy"]["SyncPlayAccess"] = "JoinGroups"
account_policy["Policy"]["EnableContentDownloading"] = False
account_policy["Policy"]["InvalidLoginAttemptCount"] = 3
account_policy["Policy"]["MaxActiveSessions"] = 1
# Update the user with the newly edited policy
request_3 = requests.post(
f"{JELLYFIN_URL}/Users?userId={jellyfin_user_id}",
headers=JELLYFIN_HEADERS,
json=account_policy,
)
if request_3.status_code != 204:
print(request_3.json())
print(request_3.status_code)
print("BROKEN AT REQUEST 3")
return False
# Add the information to the database
db = sqlite3.connect("cordarr.db")
cursor = db.cursor()
cursor.execute(
"INSERT INTO jellyfin_accounts (user_id, jellyfin_user_id, deletion_time) VALUES (?, ?, ?)",
(user_id, jellyfin_user_id, deletion_time),
)
db.commit()
db.close()
return username, password

163
code/func/radarr.py Normal file
View File

@ -0,0 +1,163 @@
import requests
import sqlite3
import discord
from global_variables import (
RADARR_HOST_URL,
RADARR_HEADERS,
ROOT_FOLDER_PATH,
QUALITY_PROFILE_ID,
)
"""
Add a specific movie to the Radarr library
"""
def get_movies(name: str):
# Remove leading/trailing whitespace and replace spaces with URL encoding
name = name.strip().replace(" ", "%20")
# Send a request to the Radarr API to search for the movie
response = requests.get(
f"{RADARR_HOST_URL}/api/v3/movie/lookup?term={name}", headers=RADARR_HEADERS
).json()
if len(response) == 0:
return "NO RESULTS"
# If the movie has alreadt been added, then the added date will be
# something other than 0001-01-01T05:51:00Z
if response[0]["added"] != "0001-01-01T05:51:00Z":
return "ALREADY ADDED"
# Add the top 5 movies and their years to a list of dictionaries and their respective tmdbIds
movies = [
{"title": response[i]["title"], "year": response[i]["year"]}
for i in range(min(5, len(response)))
]
tmdb_ids = {}
for i in range(min(5, len(response))):
tmdb_ids[response[i]["tmdbId"]] = {"description": response[i]["overview"]}
# Try to choose from one of the usual 2 poster images available,
# if not, then just set the "poster" to None
try:
try:
tmdb_ids[response[i]["tmdbId"]]["remotePoster"] = response[i]["images"][0]["remoteUrl"]
except IndexError:
tmdb_ids[response[i]["tmdbId"]]["remotePoster"] = response[i]["images"][1]["remoteUrl"]
except IndexError:
tmdb_ids[response[i]["tmdbId"]]["remotePoster"] = None
return movies, tmdb_ids
"""
Send a request to the Radarr API to add the movie
"""
def add_movie(tmdb_id: int):
# Get the necessary data for the movie
data = requests.get(
f"{RADARR_HOST_URL}/api/v3/movie/lookup/tmdb?tmdbId={tmdb_id}",
headers=RADARR_HEADERS,
).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
# Send the request to add the movie
response = requests.post(
f"{RADARR_HOST_URL}/api/v3/movie", headers=RADARR_HEADERS, json=data
).json()
movie_id = response["id"]
# Return the movie_title, movie_id
return movie_title, movie_id
class AddMovieView(discord.ui.View):
def __init__(self, movies: list, tmdb_ids: dict, *, timeout=180.0):
super().__init__(timeout=timeout)
self.add_item(AddMovieDropdown(movies, tmdb_ids))
class AddMovieDropdown(discord.ui.Select):
def __init__(self, movies: list, tmdb_ids: dict, *, timeout=180.0):
self.movies = movies
self.tmdb_ids = tmdb_ids
super().__init__(
placeholder="Select from the dropdown",
options=[
discord.SelectOption(label=f"{movie['title']} ({movie['year']})")
for movie in movies
],
)
async def callback(self, interaction: discord.Interaction):
# Convert the options to a list of strings and get the index of the selected option
string_options = [option.label for option in self.options]
index = string_options.index(interaction.data["values"][0])
# Convert the tmdbIds dictionary to a list and get the tmdbId of the selected movie
tmdb_id_list = list(self.tmdb_ids.keys())
tmdb_id = tmdb_id_list[index]
tmdbFull = self.tmdb_ids[tmdb_id]
embed = discord.Embed(
title="Is this the movie you want to add?",
description=f"**{self.movies[index]['title']}**\n\n{tmdbFull['description']}",
color=0xD01B86
)
embed.set_image(url=tmdbFull["remotePoster"])
view = RequestButtonView(tmdb_id)
await interaction.response.edit_message(embed=embed, view=view)
class RequestButtonView(discord.ui.View):
def __init__(self, tmdb_id: int, *, timeout=180.0):
super().__init__(timeout=timeout)
self.tmdb_id = tmdb_id
@discord.ui.button(label="Request", style=discord.ButtonStyle.success)
async def request_button(
self, interaction: discord.Interaction, button: discord.ui.Button
):
# Add the movie to the Radarr library
movie_title, movie_id = add_movie(self.tmdb_id)
# Alert the user that the movie has been added
embed = discord.Embed(
title="Movie Requested",
description=f"**{movie_title}** has been requested and will be added to the Radarr library. You can check the download status of your requested movies by running the `/status` command. Please wait ~5 minutes for Radarr to find a download for the movie.",
color=0xD01B86
)
await interaction.response.edit_message(embed=embed, view=None)
# # Add the movie to the Radarr library
# requests.post(
# f"{RADARR_HOST_URL}/api/v3/command",
# headers=RADARR_HEADERS,
# json={"name": "MoviesSearch", "movieIds": movie_id},
# )
# Keep track of the movie for the `/status` command
db = sqlite3.connect("cordarr.db")
cursor = db.cursor()
cursor.execute(
"INSERT INTO movies VALUES (?, ?, ?)",
(interaction.user.id, movie_id, movie_title),
)
db.commit()
db.close()
@discord.ui.button(label="Don't Request", style=discord.ButtonStyle.danger)
async def dont_request_button(
self, interaction: discord.Interaction, button: discord.ui.Button
):
embed = discord.Embed(
title="Request Cancelled",
description="Request has been cancelled. If you would like to request a different movie, run the `/request movie` command again.",
color=0xD01B86
)
await interaction.response.edit_message(embed=embed, view=None)

72
code/global_variables.py Normal file
View File

@ -0,0 +1,72 @@
import configparser
import logging
from colorlog import ColoredFormatter
log_level = logging.DEBUG
log_format = (
" %(log_color)s%(levelname)-8s%(reset)s | %(log_color)s%(message)s%(reset)s"
)
logging.root.setLevel(log_level)
formatter = ColoredFormatter(log_format)
stream = logging.StreamHandler()
stream.setLevel(log_level)
stream.setFormatter(formatter)
LOG = logging.getLogger("pythonConfig")
LOG.setLevel(log_level)
LOG.addHandler(stream)
YES_VALUES = ["yes", "y", "true", "t", "1"]
NO_VALUES = ["no", "n", "false", "f", "0"]
try:
with open("config.ini", "r") as f:
file_contents = f.read()
except FileNotFoundError:
config = configparser.ConfigParser()
config["REQUIRED"] = {
"BOT_TOKEN": "",
"RADARR_HOST_URL": "http://",
"RADARR_API_KEY": "",
"ROOT_FOLDER_PATH": "",
"QUALITY_PROFILE_ID": "",
"ENABLE_JELLYFIN_TEMP_ACCOUNTS": "",
}
config["JELLYFIN_ACCOUNTS"] = {"JELLYFIN_URL": "", "JELLYFIN_API_KEY": ""}
with open("config.ini", "w") as configfile:
config.write(configfile)
LOG.error("Configuration file `config.ini` has been generated. Please fill out all of the necessary information. Refer to the docs for information on what a specific configuration option is.")
exit()
config = configparser.ConfigParser()
config.read_string(file_contents)
BOT_TOKEN = config["REQUIRED"]["BOT_TOKEN"]
RADARR_HOST_URL = config["REQUIRED"]["RADARR_HOST_URL"]
RADARR_API_KEY = config["REQUIRED"]["RADARR_API_KEY"]
ROOT_FOLDER_PATH = config["REQUIRED"]["ROOT_FOLDER_PATH"]
QUALITY_PROFILE_ID = config["REQUIRED"]["QUALITY_PROFILE_ID"]
if config["REQUIRED"]["ENABLE_JELLYFIN_TEMP_ACCOUNTS"].lower() in YES_VALUES:
ENABLE_JELLYFIN_TEMP_ACCOUNTS = True
else:
ENABLE_JELLYFIN_TEMP_ACCOUNTS = False
JELLYFIN_URL = config["JELLYFIN_ACCOUNTS"]["JELLYFIN_URL"]
JELLYFIN_API_KEY = config["JELLYFIN_ACCOUNTS"]["JELLYFIN_API_KEY"]
ACCOUNT_TIME = int(config["JELLYFIN_ACCOUNTS"]["ACCOUNT_TIME"])
RADARR_HEADERS = {
"Content-Type": "application/json",
"X-Api-Key": RADARR_API_KEY
}
JELLYFIN_HEADERS = {
"Content-Type": "application/json",
"X-Emby-Token": JELLYFIN_API_KEY,
}

164
code/validate_config.py Normal file
View File

@ -0,0 +1,164 @@
import configparser
import sqlite3
import requests
from global_variables import LOG, YES_VALUES, NO_VALUES
"""
Validate all of the options passed into the config.ini file
"""
def validate_config(file_contents):
config = configparser.ConfigParser()
config.read_string(file_contents)
errors = 0
try:
# Validate BOT_TOKEN
if not config["REQUIRED"]["BOT_TOKEN"]:
LOG.error("BOT_TOKEN has not been set.")
errors += 1
# Validate RADARR_HOST_URL
if not config["REQUIRED"]["RADARR_HOST_URL"]:
LOG.error("RADARR_HOST_URL has not been set.")
errors += 1
# Validate RADARR_API_KEY
if not config["REQUIRED"]["RADARR_API_KEY"]:
LOG.error("RADARR_API_KEY has not been set.")
errors += 1
radarr_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=radarr_headers)
except requests.exceptions.ConnectionError:
LOG.error("Could not connect to Radarr API. Please check your RADARR_HOST_URL and RADARR_API_KEY")
errors += 1
# Validate ROOT_FOLDER_PATH
if not config["REQUIRED"]["ROOT_FOLDER_PATH"]:
LOG.error("ROOT_FOLDER_PATH has not been set.")
errors += 1
# Validate QUALITY_PROFILE_ID
data = requests.get(
f'{config["REQUIRED"]["RADARR_HOST_URL"]}/api/v3/qualityprofile',
headers=radarr_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"]
LOG.error("Empty or invalid QUALITY_PROFILE_ID passed. Pass one of the valid IDs which are now listed within the config.ini file.")
errors += 1
# Validate ENABLE_JELLYFIN_TEMP_ACCOUNTS
if not config["REQUIRED"]["ENABLE_JELLYFIN_TEMP_ACCOUNTS"]:
LOG.error("ENABLE_JELLYFIN_TEMP_ACCOUNTS has not been set.")
errors += 1
else:
# Validate the value of ENABLE_JELLYFIN_TEMP_ACCOUNTS
if (config["REQUIRED"]["ENABLE_JELLYFIN_TEMP_ACCOUNTS"].lower() not in YES_VALUES + NO_VALUES):
LOG.error("Invalid value passed to ENABLE_JELLYFIN_TEMP_ACCOUNTS. Pass a true/false value.")
errors += 1
if (config["REQUIRED"]["ENABLE_JELLYFIN_TEMP_ACCOUNTS"].lower() in YES_VALUES):
# Validate JELLYFIN_URL
if not config["JELLYFIN_ACCOUNTS"]["JELLYFIN_URL"]:
LOG.error("Empty URL passed to JELLYFIN_URL. Pass a valid URL (e.g. http://localhost:8096)")
errors += 1
# Validate JELLYFIN_API_KEY
if not config["JELLYFIN_ACCOUNTS"]["JELLYFIN_API_KEY"]:
LOG.error("Empty JELLYFIN_API_KEY passed. Create a Jellyfin API key in your Jellyfin dashboard and pass it here.")
errors += 1
# Validate ACCOUNT_TIME
if not config["JELLYFIN_ACCOUNTS"]["ACCOUNT_TIME"]:
LOG.error("Empty ACCOUNT_TIME passed. Pass a valid time in the format of HH:MM:SS (e.g. 00:30:00)")
errors += 1
try:
time = int(config["JELLYFIN_ACCOUNTS"]["ACCOUNT_TIME"])
except ValueError:
LOG.error("Invalid value passed to ACCOUNT_TIME. Pass a valid integer value (e.g. 24)")
errors += 1
# Make sure connection to Jellyfin API can be established
jellyfin_headers = {
"Content-Type": "application/json",
"Authorization": f"MediaBrowser Client=\"other\", device=\"CordArr\", DeviceId=\"cordarr-device-id\", Version=\"0.0.0\", Token=\"{config['JELLYFIN_ACCOUNTS']['JELLYFIN_API_KEY']}\"",
}
response = requests.get(
f"{config['JELLYFIN_ACCOUNTS']['JELLYFIN_URL']}/Users",
headers=jellyfin_headers,
)
if response.status_code != 200:
LOG.error("Could not connect to Jellyfin API. Please check your JELLYFIN_URL and JELLYFIN_API_KEY")
errors += 1
if errors > 0:
LOG.info(f"Found {errors} error(s) in the configuration file. Please fix them before restarting the application.")
exit()
except KeyError:
LOG.critical("You are missing at least one of the configuration options in your config.ini file. In order to regenerate all options, delete the config.ini file and restart the application.")
exit()
"""
This method is called before starting the application - to make and validate the configuration
"""
def create_config():
# While here, we can begin by making the database
db = sqlite3.connect("cordarr.db")
cursor = db.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS movies (user_id, movie_id, movie_title)")
cursor.execute("CREATE TABLE IF NOT EXISTS jellyfin_accounts (user_id, jellyfin_user_id, deletion_time, PRIMARY KEY (user_id))")
db.commit()
db.close()
# 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"] = {
"BOT_TOKEN": "",
"RADARR_HOST_URL": "http://",
"RADARR_API_KEY": "",
"ROOT_FOLDER_PATH": "",
"QUALITY_PROFILE_ID": "",
"ENABLE_JELLYFIN_TEMP_ACCOUNTS": "",
}
config["JELLYFIN_ACCOUNTS"] = {
"JELLYFIN_URL": "",
"JELLYFIN_API_KEY": ""
}

BIN
cordarr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
colorlog==6.8.2
discord.py==2.3.2
requests==2.28.2
humanize==4.9.0