Fixes + Use SQLAlchemy

This commit is contained in:
Parker M. 2025-01-21 20:38:33 -06:00
parent 023ee141eb
commit b0ea7ab935
Signed by: parker
GPG Key ID: 505ED36FC12B5D5E
10 changed files with 165 additions and 134 deletions

View File

@ -1,10 +1,9 @@
import discord import discord
from discord.ext import commands from discord.ext import commands
from discord.ext import tasks
import os import os
from utils.database import Base, engine
import utils.config as config import utils.config as config
from utils.jellyfin_delete import delete_accounts
class MyBot(commands.Bot): class MyBot(commands.Bot):
@ -15,7 +14,6 @@ class MyBot(commands.Bot):
) )
async def setup_hook(self): async def setup_hook(self):
delete_old_temp_accounts.start()
for ext in os.listdir("./code/cogs"): for ext in os.listdir("./code/cogs"):
if ext.endswith(".py"): if ext.endswith(".py"):
await self.load_extension(f"cogs.{ext[:-3]}") await self.load_extension(f"cogs.{ext[:-3]}")
@ -30,11 +28,7 @@ async def on_ready():
config.LOG.info(f"{bot.user} has connected to Discord.") config.LOG.info(f"{bot.user} has connected to Discord.")
@tasks.loop(seconds=60)
async def delete_old_temp_accounts():
delete_accounts()
if __name__ == "__main__": if __name__ == "__main__":
Base.metadata.create_all(bind=engine)
config.load_config() config.load_config()
bot.run(config.BOT_TOKEN) bot.run(config.BOT_TOKEN)

View File

@ -1,9 +1,11 @@
import discord import discord
from discord import app_commands from discord import app_commands
from discord.ext import commands from discord.ext import commands, tasks
import sqlite3
from utils.database import Session
from utils.jellyfin_create import create_jellyfin_account from utils.jellyfin_create import create_jellyfin_account
from utils.jellyfin_delete import delete_accounts
from utils.models import JellyfinAccounts
from utils.config import ( from utils.config import (
JELLYFIN_PUBLIC_URL, JELLYFIN_PUBLIC_URL,
JELLYFIN_ENABLED, JELLYFIN_ENABLED,
@ -15,19 +17,20 @@ class NewAccount(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
def cog_load(self):
self.delete_accounts_loop.start()
@app_commands.command() @app_commands.command()
@app_commands.check(lambda inter: JELLYFIN_ENABLED) @app_commands.check(lambda inter: JELLYFIN_ENABLED)
async def newaccount(self, interaction: discord.Interaction) -> None: async def newaccount(self, interaction: discord.Interaction) -> None:
"""Create a new temporary Jellyfin account""" """Create a new temporary Jellyfin account"""
# Make sure the user doesn't already have an account # Make sure the user doesn't already have an account
db = sqlite3.connect("data/cordarr.db") with Session() as session:
cursor = db.cursor() account = (
cursor.execute( session.query(JellyfinAccounts)
"SELECT * FROM jellyfin_accounts WHERE user_id = ?", .filter(JellyfinAccounts.user_id == interaction.user.id)
(interaction.user.id,), .first()
) )
account = cursor.fetchone()
db.close()
# Account already allocated # Account already allocated
if account: if account:
embed = discord.Embed( embed = discord.Embed(
@ -63,8 +66,8 @@ class NewAccount(commands.Cog):
title="Jellyfin Account Information", title="Jellyfin Account Information",
description=( description=(
# fmt: off # fmt: off
"Here is your temporary account information.\n\n", "Here is your temporary account information.\n\n"
f"**Server URL:** `[{JELLYFIN_PUBLIC_URL}]({JELLYFIN_PUBLIC_URL})`\n" f"**Server URL:** `{JELLYFIN_PUBLIC_URL}`\n"
f"**Username:** `{response[0]}`\n" f"**Username:** `{response[0]}`\n"
f"**Password:** `{response[1]}`\n\n" f"**Password:** `{response[1]}`\n\n"
"Your account will be automatically deleted in" "Your account will be automatically deleted in"
@ -88,6 +91,10 @@ class NewAccount(commands.Cog):
embed=embed, ephemeral=True embed=embed, ephemeral=True
) )
@tasks.loop(minutes=1)
async def delete_accounts_loop(self):
delete_accounts()
async def setup(bot): async def setup(bot):
await bot.add_cog(NewAccount(bot)) await bot.add_cog(NewAccount(bot))

View File

@ -2,8 +2,9 @@ import discord
from discord import app_commands from discord import app_commands
from discord.ext import commands from discord.ext import commands
import requests import requests
import sqlite3
from utils.models import Requests
from utils.database import Session
from utils.config import ( from utils.config import (
RADARR_HOST_URL, RADARR_HOST_URL,
RADARR_HEADERS, RADARR_HEADERS,
@ -22,15 +23,18 @@ class Status(commands.Cog):
# Defer the response # Defer the response
await interaction.response.defer(ephemeral=True) await interaction.response.defer(ephemeral=True)
db = sqlite3.connect("data/cordarr.db") with Session() as session:
cursor = db.cursor() requested_content = (
cursor.execute( session.query(
"SELECT title, release_year, local_id, tmdbid, tvdbid FROM" Requests.title,
" requests WHERE user_id = ?", Requests.release_year,
(interaction.user.id,), Requests.local_id,
) Requests.tmdbid,
requested_content = cursor.fetchall() Requests.tvdbid,
db.close() )
.filter(Requests.user_id == interaction.user.id)
.all()
)
# No content requested # No content requested
if len(requested_content) == 0: if len(requested_content) == 0:
@ -42,7 +46,7 @@ class Status(commands.Cog):
), ),
color=0xD01B86, color=0xD01B86,
) )
return await interaction.followup.send(embed=embed, ephemeral=True) return await interaction.followup.send(embed=embed)
# Create template embed # Create template embed
embed = discord.Embed( embed = discord.Embed(
@ -75,7 +79,7 @@ class Status(commands.Cog):
embed.description += radarr_desc + sonarr_desc + non_queue_desc embed.description += radarr_desc + sonarr_desc + non_queue_desc
# Send the follow-up message # Send the follow-up message
await interaction.edit_original_response(embed=embed, ephemeral=True) await interaction.followup.send(embed=embed)
def unpack_content(self, requested_content: list) -> tuple: def unpack_content(self, requested_content: list) -> tuple:
""" """
@ -92,18 +96,18 @@ class Status(commands.Cog):
sonarr_content_info = {} sonarr_content_info = {}
for content in requested_content: for content in requested_content:
title, (release_year), local_id, tmdbid, tvdbid = content title, release_year, local_id, tmdbid, tvdbid = content
if tmdbid is not None: if tmdbid is not None:
radarr_content_info[int(local_id)] = { radarr_content_info[local_id] = {
"title": title, "title": title,
"release_year": int(release_year), "release_year": release_year,
"tmdbid": int(tmdbid), "tmdbid": tmdbid,
} }
else: else:
sonarr_content_info[int(local_id)] = { sonarr_content_info[local_id] = {
"title": title, "title": title,
"release_year": int(release_year), "release_year": release_year,
"tvdbid": int(tvdbid), "tvdbid": tvdbid,
} }
return radarr_content_info, sonarr_content_info return radarr_content_info, sonarr_content_info
@ -169,7 +173,7 @@ class Status(commands.Cog):
for content in requested_content: for content in requested_content:
title, release_year, local_id, tmdbid, _ = content title, release_year, local_id, tmdbid, _ = content
# If not in queue # If not in queue
if int(local_id) not in added_ids: if local_id not in added_ids:
# Pull the movie data from the service # Pull the movie data from the service
if tmdbid is not None: if tmdbid is not None:
data = requests.get( data = requests.get(
@ -185,15 +189,16 @@ class Status(commands.Cog):
# If the movie has a file, then it has finished downloading # If the movie has a file, then it has finished downloading
if data.get("hasFile", True): if data.get("hasFile", True):
# Remove from database # Remove from database
db = sqlite3.connect("data/cordarr.db") with Session() as session:
cursor = db.cursor() request = (
cursor.execute( session.query(Requests)
"DELETE FROM requests WHERE user_id = ? AND" .filter(Requests.user_id == user_id)
" local_id = ?", .filter(Requests.local_id == local_id)
(user_id, int(local_id)), .first()
) )
db.commit() session.delete(request)
db.close() session.commit()
# If series and only a portion of episodes have been downloaded # If series and only a portion of episodes have been downloaded
if data.get("statistics").get("percentOfEpisodes"): if data.get("statistics").get("percentOfEpisodes"):
description += ( description += (

View File

@ -4,7 +4,6 @@ import sys
import os import os
import logging import logging
import requests import requests
import sqlite3
from colorlog import ColoredFormatter from colorlog import ColoredFormatter
@ -108,10 +107,9 @@ schema = {
def load_config() -> None: def load_config() -> None:
""" """
Load DB, then load and validate the config file Load the config file and validate it
If the file does not exist, generate it If the file does not exist, generate it
""" """
database_setup()
if os.path.exists("/.dockerenv"): if os.path.exists("/.dockerenv"):
file_path = "config/config.yaml" file_path = "config/config.yaml"
else: else:
@ -157,26 +155,6 @@ jellyfin:
) )
def database_setup() -> None:
"""
Create the database if it does not exist
"""
if not os.path.exists("data"):
os.makedirs("data")
db = sqlite3.connect("data/cordarr.db")
cursor = db.cursor()
cursor.execute(
"CREATE TABLE IF NOT EXISTS requests (title TEXT, release_year TEXT,"
" local_id INTEGER, tmdbid INTEGER, tvdbid INTEGER, user_id INTEGER)"
)
cursor.execute(
"CREATE TABLE IF NOT EXISTS jellyfin_accounts (user_id INTEGER,"
" jellyfin_user_id INTEGER, deletion_time DATETIME)"
)
db.commit()
db.close()
def validate_config(contents) -> None: def validate_config(contents) -> None:
""" """
Validate the contents of the config file and assign variables Validate the contents of the config file and assign variables

View File

@ -1,6 +1,7 @@
import discord import discord
import sqlite3
from utils.models import Requests
from utils.database import Session
from utils.content_add import add_content from utils.content_add import add_content
""" """
@ -163,30 +164,26 @@ class RequestButtonView(discord.ui.View):
) )
# Keep track of the requests for the `/status` command # Keep track of the requests for the `/status` command
db = sqlite3.connect("data/cordarr.db") with Session() as session:
cursor = db.cursor() session.add(
cursor.execute( Requests(
"INSERT INTO requests (title, release_year, local_id, tmdbid," title=self.content_info["title"],
" tvdbid, user_id) VALUES (?, ?, ?, ?, ?, ?)", release_year=self.content_info["year"],
( local_id=local_id,
self.content_info["title"], tmdbid=(
self.content_info["year"], self.content_info["contentId"]
local_id, if self.service == "radarr"
( else None
self.content_info["contentId"] ),
if self.service == "radarr" tvdbid=(
else None None
), if self.service == "radarr"
( else self.content_info["contentId"]
None ),
if self.service == "radarr" user_id=interaction.user.id,
else self.content_info["contentId"] )
), )
interaction.user.id, session.commit()
),
)
db.commit()
db.close()
@discord.ui.button(label="Don't Request", style=discord.ButtonStyle.danger) @discord.ui.button(label="Don't Request", style=discord.ButtonStyle.danger)
async def dont_request_button( async def dont_request_button(

9
code/utils/database.py Normal file
View File

@ -0,0 +1,9 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
database_url = "sqlite:///data/cordarr.db"
engine = create_engine(database_url)
Session = sessionmaker(bind=engine)
Base = declarative_base()

View File

@ -1,10 +1,11 @@
import datetime import datetime
import requests import requests
import random import random
import sqlite3
from wonderwords import RandomWord from wonderwords import RandomWord
from string import ascii_lowercase, digits from string import ascii_lowercase, digits
from utils.database import Session
from utils.models import JellyfinAccounts
from utils.config import ( from utils.config import (
JELLYFIN_URL, JELLYFIN_URL,
JELLYFIN_HEADERS, JELLYFIN_HEADERS,
@ -67,14 +68,14 @@ def create_jellyfin_account(user_id):
return False return False
# Add the information to the database # Add the information to the database
db = sqlite3.connect("data/cordarr.db") with Session() as session:
cursor = db.cursor() session.add(
cursor.execute( JellyfinAccounts(
"INSERT INTO jellyfin_accounts (user_id, jellyfin_user_id," user_id=user_id,
" deletion_time) VALUES (?, ?, ?)", jellyfin_user_id=jellyfin_user_id,
(user_id, jellyfin_user_id, deletion_time), deletion_time=deletion_time,
) )
db.commit() )
db.close() session.commit()
return username, password return username, password

View File

@ -1,8 +1,9 @@
import datetime import datetime
import sqlite3
import requests import requests
from utils.config import JELLYFIN_URL, JELLYFIN_HEADERS from utils.database import Session
from utils.models import JellyfinAccounts
from utils.config import LOG, JELLYFIN_URL, JELLYFIN_HEADERS
def delete_accounts(): def delete_accounts():
@ -10,29 +11,36 @@ def delete_accounts():
Delete Jellyfin accounts that have passed their deletion time Delete Jellyfin accounts that have passed their deletion time
""" """
# Get all expired Jellyfin accounts # Get all expired Jellyfin accounts
db = sqlite3.connect("data/cordarr.db") with Session() as session:
cursor = db.cursor() jellyfin_user_ids = (
cursor.execute( session.query(JellyfinAccounts.jellyfin_user_id)
"SELECT jellyfin_user_id FROM jellyfin_accounts WHERE" .filter(JellyfinAccounts.deletion_time < datetime.datetime.now())
" deletion_time < ?", .all()
(datetime.datetime.now(),),
)
jellyfin_user_ids = cursor.fetchall()
# Delete the Jellyfin accounts
for jellyfin_user_id in jellyfin_user_ids:
request = requests.delete(
f"{JELLYFIN_URL}/Users/{jellyfin_user_id[0]}",
headers=JELLYFIN_HEADERS,
) )
# If 204 - account deleted
# If 404 - account not found
# Either way, remove account from database
if request.status_code in (404, 204):
cursor.execute(
"DELETE FROM jellyfin_accounts WHERE jellyfin_user_id = ?",
(jellyfin_user_id,),
)
db.commit() # Delete each account
db.close() for jellyfin_user_id in jellyfin_user_ids:
print(f"Deleting account {jellyfin_user_id[0]}")
try:
response = requests.delete(
f"{JELLYFIN_URL}/Users/{jellyfin_user_id[0]}",
headers=JELLYFIN_HEADERS,
)
response.raise_for_status()
# Get the account and delete it
account = (
session.query(JellyfinAccounts)
.filter(
JellyfinAccounts.jellyfin_user_id
== jellyfin_user_id[0]
)
.first()
)
session.delete(account)
except:
LOG.error(
"Failed deleting Jellyfin account w/ ID"
f" {jellyfin_user_id[0]}"
)
# Commit changes
session.commit()

31
code/utils/models.py Normal file
View File

@ -0,0 +1,31 @@
from sqlalchemy import (
Column,
Integer,
String,
DateTime,
BigInteger,
)
from utils.database import Base
class Requests(Base):
__tablename__ = "requests"
id = Column(Integer, primary_key=True, index=True)
title = Column(String)
release_year = Column(Integer)
local_id = Column(Integer)
tmdbid = Column(Integer)
tvdbid = Column(Integer)
user_id = Column(BigInteger)
class JellyfinAccounts(Base):
__tablename__ = "jellyfin_accounts"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(BigInteger)
jellyfin_user_id = Column(String)
deletion_time = Column(DateTime)

View File

@ -4,4 +4,5 @@ wonderwords==2.2.0
PyYAML==6.0.2 PyYAML==6.0.2
jsonschema==4.23.0 jsonschema==4.23.0
jsonschema-specifications==2024.10.1 jsonschema-specifications==2024.10.1
discord.py==2.4.0 discord.py==2.4.0
SQLAlchemy==2.0.37