1353 lines
52 KiB
Python
1353 lines
52 KiB
Python
import os
|
|
import sys
|
|
from cryptography.fernet import Fernet
|
|
import string
|
|
import secrets
|
|
from passlib.hash import argon2
|
|
import psycopg
|
|
from argon2 import PasswordHasher
|
|
from argon2.exceptions import HashingError
|
|
import logging
|
|
import random
|
|
|
|
# Generate a random password
|
|
def generate_random_password(length=12):
|
|
characters = string.ascii_letters + string.digits + string.punctuation
|
|
return ''.join(random.choice(characters) for i in range(length))
|
|
|
|
# Hash the password using Argon2
|
|
def hash_password(password):
|
|
ph = PasswordHasher()
|
|
try:
|
|
return ph.hash(password)
|
|
except HashingError as e:
|
|
print(f"Error hashing password: {e}")
|
|
return None
|
|
|
|
# Set up basic configuration for logging
|
|
logging.basicConfig(level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
|
# Append the pinepods directory to sys.path for module import
|
|
sys.path.append('/pinepods')
|
|
|
|
try:
|
|
# Attempt to import additional modules
|
|
import database_functions.functions
|
|
# import Auth.Passfunctions
|
|
|
|
def hash_password(password: str):
|
|
# Hash the password
|
|
hashed_password = argon2.hash(password)
|
|
# Argon2 includes the salt in the hashed output
|
|
return hashed_password
|
|
|
|
|
|
|
|
# Database variables
|
|
db_host = os.environ.get("DB_HOST", "127.0.0.1")
|
|
db_port = os.environ.get("DB_PORT", "5432")
|
|
db_user = os.environ.get("DB_USER", "postgres")
|
|
db_password = os.environ.get("DB_PASSWORD", "password")
|
|
db_name = os.environ.get("DB_NAME", "pypods_database")
|
|
|
|
# Function to create the database if it doesn't exist
|
|
def create_database_if_not_exists():
|
|
try:
|
|
# Connect to the default 'postgres' database
|
|
with psycopg.connect(
|
|
host=db_host,
|
|
port=db_port,
|
|
user=db_user,
|
|
password=db_password,
|
|
dbname='postgres'
|
|
) as conn:
|
|
conn.autocommit = True
|
|
with conn.cursor() as cur:
|
|
# Check if the database exists
|
|
cur.execute("SELECT 1 FROM pg_database WHERE datname = %s", (db_name,))
|
|
exists = cur.fetchone()
|
|
if not exists:
|
|
logging.info(f"Database {db_name} does not exist. Creating...")
|
|
print(f"Database {db_name} does not exist. Creating...")
|
|
cur.execute(f"CREATE DATABASE {db_name}")
|
|
logging.info(f"Database {db_name} created successfully.")
|
|
else:
|
|
logging.info(f"Database {db_name} already exists.")
|
|
except Exception as e:
|
|
logging.error(f"Error creating database: {e}")
|
|
raise
|
|
|
|
# Create the database if it doesn't exist
|
|
create_database_if_not_exists()
|
|
|
|
# Create database connector
|
|
cnx = psycopg.connect(
|
|
host=db_host,
|
|
port=db_port,
|
|
user=db_user,
|
|
password=db_password,
|
|
dbname=db_name
|
|
)
|
|
|
|
# create a cursor to execute SQL statements
|
|
cursor = cnx.cursor()
|
|
|
|
def ensure_usernames_lowercase(cnx):
|
|
with cnx.cursor() as cursor:
|
|
cursor.execute('SELECT UserID, Username FROM "Users"')
|
|
users = cursor.fetchall()
|
|
for user_id, username in users:
|
|
if username != username.lower():
|
|
cursor.execute('UPDATE "Users" SET Username = %s WHERE UserID = %s', (username.lower(), user_id))
|
|
print(f"Updated Username for UserID {user_id} to lowercase")
|
|
|
|
def add_column_if_not_exists(cursor, table_name, column_name, column_definition):
|
|
cursor.execute(f"""
|
|
SELECT COUNT(*)
|
|
FROM information_schema.columns
|
|
WHERE table_name='{table_name}'
|
|
AND column_name='{column_name}';
|
|
""")
|
|
if cursor.fetchone()[0] == 0:
|
|
cursor.execute(f"""
|
|
ALTER TABLE "{table_name}"
|
|
ADD COLUMN {column_name} {column_definition};
|
|
""")
|
|
print(f"Column '{column_name}' added to table '{table_name}'")
|
|
else:
|
|
return
|
|
|
|
# Create Users table first
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "Users" (
|
|
UserID SERIAL PRIMARY KEY,
|
|
Fullname VARCHAR(255),
|
|
Username VARCHAR(255) UNIQUE,
|
|
Email VARCHAR(255),
|
|
Hashed_PW VARCHAR(500),
|
|
IsAdmin BOOLEAN,
|
|
Reset_Code TEXT,
|
|
Reset_Expiry TIMESTAMP,
|
|
MFA_Secret VARCHAR(70),
|
|
TimeZone VARCHAR(50) DEFAULT 'UTC',
|
|
TimeFormat INT DEFAULT 24,
|
|
DateFormat VARCHAR(3) DEFAULT 'ISO',
|
|
FirstLogin BOOLEAN DEFAULT false,
|
|
GpodderUrl VARCHAR(255) DEFAULT '',
|
|
Pod_Sync_Type VARCHAR(50) DEFAULT 'None',
|
|
GpodderLoginName VARCHAR(255) DEFAULT '',
|
|
GpodderToken VARCHAR(255) DEFAULT '',
|
|
EnableRSSFeeds BOOLEAN DEFAULT FALSE,
|
|
PlaybackSpeed NUMERIC(2,1) DEFAULT 1.0
|
|
)
|
|
""")
|
|
cnx.commit()
|
|
|
|
def add_playbackspeed_if_not_exist_users(cursor, cnx):
|
|
try:
|
|
cursor.execute("""
|
|
SELECT column_name
|
|
FROM information_schema.columns
|
|
WHERE table_name='Users'
|
|
AND column_name = 'playbackspeed'
|
|
""")
|
|
existing_column = cursor.fetchone()
|
|
if not existing_column:
|
|
cursor.execute("""
|
|
ALTER TABLE "Users"
|
|
ADD COLUMN PlaybackSpeed NUMERIC(2,1) DEFAULT 1.0
|
|
""")
|
|
print("Added 'PlaybackSpeed' column to 'Users' table.")
|
|
cnx.commit()
|
|
else:
|
|
print("Column 'PlaybackSpeed' already exists in 'Users' table.")
|
|
except Exception as e:
|
|
print(f"Error checking PlaybackSpeed column in Users table: {e}")
|
|
# Important: Don't try to commit here, as the transaction is already aborted
|
|
|
|
# Usage - should be called during app startup
|
|
add_playbackspeed_if_not_exist_users(cursor, cnx)
|
|
|
|
# Create OIDCProviders next
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "OIDCProviders" (
|
|
ProviderID SERIAL PRIMARY KEY,
|
|
ProviderName VARCHAR(255) NOT NULL,
|
|
ClientID VARCHAR(255) NOT NULL,
|
|
ClientSecret VARCHAR(500) NOT NULL,
|
|
AuthorizationURL VARCHAR(255) NOT NULL,
|
|
TokenURL VARCHAR(255) NOT NULL,
|
|
UserInfoURL VARCHAR(255) NOT NULL,
|
|
Scope VARCHAR(255) DEFAULT 'openid email profile',
|
|
ButtonColor VARCHAR(50) DEFAULT '#000000',
|
|
ButtonText VARCHAR(255) NOT NULL,
|
|
ButtonTextColor VARCHAR(50) DEFAULT '#000000',
|
|
IconSVG TEXT,
|
|
Enabled BOOLEAN DEFAULT true,
|
|
Created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
Modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
""")
|
|
cnx.commit()
|
|
|
|
# Now add all columns
|
|
add_column_if_not_exists(cursor, 'Users', 'auth_type', 'VARCHAR(50) DEFAULT \'standard\'')
|
|
add_column_if_not_exists(cursor, 'Users', 'oidc_provider_id', 'INT')
|
|
add_column_if_not_exists(cursor, 'Users', 'oidc_subject', 'VARCHAR(255)')
|
|
cnx.commit()
|
|
|
|
# Now add foreign key
|
|
cursor.execute("""
|
|
SELECT COUNT(*)
|
|
FROM information_schema.table_constraints
|
|
WHERE constraint_name = 'fk_oidc_provider'
|
|
AND table_name = 'Users';
|
|
""")
|
|
if cursor.fetchone()[0] == 0:
|
|
cursor.execute("""
|
|
ALTER TABLE "Users"
|
|
ADD CONSTRAINT fk_oidc_provider
|
|
FOREIGN KEY (oidc_provider_id)
|
|
REFERENCES "OIDCProviders"(ProviderID);
|
|
""")
|
|
print("Foreign key constraint 'fk_oidc_provider' added")
|
|
cnx.commit()
|
|
|
|
# Create API Keys table last
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "APIKeys" (
|
|
APIKeyID SERIAL PRIMARY KEY,
|
|
UserID INT,
|
|
APIKey TEXT,
|
|
Created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (UserID) REFERENCES "Users"(UserID) ON DELETE CASCADE
|
|
)
|
|
""")
|
|
cnx.commit()
|
|
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "RssKeys" (
|
|
RssKeyID SERIAL PRIMARY KEY,
|
|
UserID INT,
|
|
RssKey TEXT,
|
|
Created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (UserID) REFERENCES "Users"(UserID) ON DELETE CASCADE
|
|
)
|
|
""")
|
|
cnx.commit()
|
|
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "RssKeyMap" (
|
|
RssKeyID INT,
|
|
PodcastID INT,
|
|
FOREIGN KEY (RssKeyID) REFERENCES "RssKeys"(RssKeyID) ON DELETE CASCADE
|
|
)
|
|
""")
|
|
cnx.commit()
|
|
|
|
cursor.execute("""
|
|
SELECT COUNT(*)
|
|
FROM information_schema.table_constraints
|
|
WHERE constraint_name = 'PlaybackSpeed'
|
|
AND table_name = 'Users';
|
|
""")
|
|
if cursor.fetchone()[0] == 0:
|
|
cursor.execute("""
|
|
ALTER TABLE "Users" ADD COLUMN PlaybackSpeed NUMERIC(2,1) DEFAULT 1.0;
|
|
""")
|
|
print("Column 'PlaybackSpeed' added to Users table")
|
|
cnx.commit()
|
|
|
|
# Now add foreign key
|
|
cursor.execute("""
|
|
SELECT COUNT(*)
|
|
FROM information_schema.table_constraints
|
|
WHERE constraint_name = 'PlaybackSpeed'
|
|
AND table_name = 'Podcasts';
|
|
""")
|
|
if cursor.fetchone()[0] == 0:
|
|
cursor.execute("""
|
|
ALTER TABLE "Podcasts" ADD COLUMN PlaybackSpeed NUMERIC(2,1) DEFAULT 1.0;
|
|
""")
|
|
print("Column 'PlaybackSpeed' added to Podcasts table")
|
|
cnx.commit()
|
|
|
|
ensure_usernames_lowercase(cnx)
|
|
|
|
|
|
try:
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "GpodderDevices" (
|
|
DeviceID SERIAL PRIMARY KEY,
|
|
UserID INT NOT NULL,
|
|
DeviceName VARCHAR(255) NOT NULL,
|
|
DeviceType VARCHAR(50) DEFAULT 'desktop',
|
|
DeviceCaption VARCHAR(255),
|
|
IsDefault BOOLEAN DEFAULT FALSE,
|
|
LastSync TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
IsActive BOOLEAN DEFAULT TRUE,
|
|
FOREIGN KEY (UserID) REFERENCES "Users"(UserID) ON DELETE CASCADE,
|
|
UNIQUE(UserID, DeviceName)
|
|
)
|
|
""")
|
|
cnx.commit()
|
|
print("Created GpodderDevices table")
|
|
|
|
# Create index for faster lookups
|
|
cursor.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_gpodder_devices_userid
|
|
ON "GpodderDevices"(UserID)
|
|
""")
|
|
cnx.commit()
|
|
|
|
# Create a table for subscription history/sync state
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "GpodderSyncState" (
|
|
SyncStateID SERIAL PRIMARY KEY,
|
|
UserID INT NOT NULL,
|
|
DeviceID INT NOT NULL,
|
|
LastTimestamp BIGINT DEFAULT 0,
|
|
EpisodesTimestamp BIGINT DEFAULT 0,
|
|
FOREIGN KEY (UserID) REFERENCES "Users"(UserID) ON DELETE CASCADE,
|
|
FOREIGN KEY (DeviceID) REFERENCES "GpodderDevices"(DeviceID) ON DELETE CASCADE,
|
|
UNIQUE(UserID, DeviceID)
|
|
)
|
|
""")
|
|
cnx.commit()
|
|
print("Created GpodderSyncState table")
|
|
except Exception as e:
|
|
print(f"Error creating GPodder tables: {e}")
|
|
|
|
cursor.execute("""CREATE TABLE IF NOT EXISTS "UserStats" (
|
|
UserStatsID SERIAL PRIMARY KEY,
|
|
UserID INT UNIQUE,
|
|
UserCreated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
PodcastsPlayed INT DEFAULT 0,
|
|
TimeListened INT DEFAULT 0,
|
|
PodcastsAdded INT DEFAULT 0,
|
|
EpisodesSaved INT DEFAULT 0,
|
|
EpisodesDownloaded INT DEFAULT 0,
|
|
FOREIGN KEY (UserID) REFERENCES "Users"(UserID)
|
|
)""")
|
|
|
|
|
|
# Generate a key
|
|
key = Fernet.generate_key()
|
|
|
|
# Create the AppSettings table
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "AppSettings" (
|
|
AppSettingsID SERIAL PRIMARY KEY,
|
|
SelfServiceUser BOOLEAN DEFAULT false,
|
|
DownloadEnabled BOOLEAN DEFAULT true,
|
|
EncryptionKey BYTEA, -- Set the data type to BYTEA for binary data
|
|
NewsFeedSubscribed BOOLEAN DEFAULT false
|
|
)
|
|
""")
|
|
|
|
cursor.execute('SELECT COUNT(*) FROM "AppSettings" WHERE AppSettingsID = 1')
|
|
count = cursor.fetchone()[0]
|
|
|
|
if count == 0:
|
|
cursor.execute("""
|
|
INSERT INTO "AppSettings" (SelfServiceUser, DownloadEnabled, EncryptionKey)
|
|
VALUES (false, true, %s)
|
|
""", (key,))
|
|
|
|
try:
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "EmailSettings" (
|
|
EmailSettingsID SERIAL PRIMARY KEY,
|
|
Server_Name VARCHAR(255),
|
|
Server_Port INT,
|
|
From_Email VARCHAR(255),
|
|
Send_Mode VARCHAR(255),
|
|
Encryption VARCHAR(255),
|
|
Auth_Required BOOLEAN,
|
|
Username VARCHAR(255),
|
|
Password VARCHAR(255)
|
|
)
|
|
""")
|
|
except Exception as e:
|
|
logging.error(f"Failed to create EmailSettings table: {e}")
|
|
|
|
try:
|
|
cursor.execute("""
|
|
SELECT COUNT(*) FROM "EmailSettings"
|
|
""")
|
|
rows = cursor.fetchone()
|
|
|
|
|
|
if rows[0] == 0:
|
|
cursor.execute("""
|
|
INSERT INTO "EmailSettings" (Server_Name, Server_Port, From_Email, Send_Mode, Encryption, Auth_Required, Username, Password)
|
|
VALUES ('default_server', 587, 'default_email@domain.com', 'default_mode', 'default_encryption', true, 'default_username', 'default_password')
|
|
""")
|
|
except Exception as e:
|
|
print(f"Error setting default email data: {e}")
|
|
|
|
def user_exists(cursor, username):
|
|
cursor.execute("""
|
|
SELECT 1 FROM "Users" WHERE Username = %s
|
|
""", (username,))
|
|
return cursor.fetchone() is not None
|
|
|
|
# Insert or update the user in the database
|
|
def insert_or_update_user(cursor, hashed_password):
|
|
try:
|
|
if user_exists(cursor, 'guest'):
|
|
cursor.execute("""
|
|
UPDATE "Users"
|
|
SET Fullname = %s, Username = %s, Email = %s, Hashed_PW = %s, IsAdmin = %s
|
|
WHERE Username = %s
|
|
""", ('Background Tasks', 'background_tasks', 'inactive', hashed_password, False, 'guest'))
|
|
logging.info("Updated existing 'guest' user to 'background_tasks' user.")
|
|
elif user_exists(cursor, 'bt'):
|
|
cursor.execute("""
|
|
UPDATE "Users"
|
|
SET Fullname = %s, Username = %s, Email = %s, Hashed_PW = %s, IsAdmin = %s
|
|
WHERE Username = %s
|
|
""", ('Background Tasks', 'background_tasks', 'inactive', hashed_password, False, 'bt'))
|
|
logging.info("Updated existing 'guest' user to 'background_tasks' user.")
|
|
else:
|
|
cursor.execute("""
|
|
INSERT INTO "Users" (Fullname, Username, Email, Hashed_PW, IsAdmin)
|
|
VALUES (%s, %s, %s, %s, %s)
|
|
ON CONFLICT (Username) DO NOTHING
|
|
""", ('Background Tasks', 'background_tasks', 'inactive', hashed_password, False))
|
|
except Exception as e:
|
|
print(f"Error inserting or updating user: {e}")
|
|
logging.error("Error inserting or updating user: %s", e)
|
|
|
|
|
|
try:
|
|
# Generate and hash the password
|
|
random_password = generate_random_password()
|
|
hashed_password = hash_password(random_password)
|
|
|
|
if hashed_password:
|
|
insert_or_update_user(cursor, hashed_password)
|
|
|
|
|
|
|
|
except Exception as e:
|
|
print(f"Error setting default Background Task User: {e}")
|
|
logging.error("Error setting default Background Task User: %s", e)
|
|
|
|
|
|
try:
|
|
# Check if API key exists for user_id
|
|
cursor.execute('SELECT apikey FROM "APIKeys" WHERE userid = %s', (1,))
|
|
|
|
result = cursor.fetchone()
|
|
|
|
if result:
|
|
api_key = result[0]
|
|
else:
|
|
import secrets
|
|
import string
|
|
alphabet = string.ascii_letters + string.digits
|
|
api_key = ''.join(secrets.choice(alphabet) for _ in range(64))
|
|
|
|
# Insert the new API key into the database using a parameterized query
|
|
cursor.execute('INSERT INTO "APIKeys" (UserID, APIKey) VALUES (%s, %s)', (1, api_key))
|
|
|
|
cnx.commit()
|
|
|
|
with open("/tmp/web_api_key.txt", "w") as f:
|
|
f.write(api_key)
|
|
except Exception as e:
|
|
print(f"Error creating web key: {e}")
|
|
|
|
try:
|
|
# First check if the table exists - use lowercase in the check since lower() is applied
|
|
cursor.execute("""
|
|
SELECT EXISTS (
|
|
SELECT FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
AND lower(table_name) = 'usersettings'
|
|
);
|
|
""")
|
|
table_exists = cursor.fetchone()[0]
|
|
|
|
if not table_exists:
|
|
# Fresh install - create the table with all columns
|
|
# Important: Notice we're referencing "Users" (capital U) here
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "UserSettings" (
|
|
usersettingid SERIAL PRIMARY KEY,
|
|
userid INT UNIQUE,
|
|
theme VARCHAR(255) DEFAULT 'Nordic',
|
|
startpage VARCHAR(255) DEFAULT 'home',
|
|
FOREIGN KEY (userid) REFERENCES "Users"(userid)
|
|
)
|
|
""")
|
|
print("UserSettings table created with startpage column included")
|
|
else:
|
|
# Get the actual table name (might be mixed case)
|
|
cursor.execute("""
|
|
SELECT table_name
|
|
FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
AND lower(table_name) = 'usersettings'
|
|
""")
|
|
actual_table_name = cursor.fetchone()[0]
|
|
print(f"Found existing UserSettings table as: {actual_table_name}")
|
|
|
|
# Get all column names with their actual case
|
|
cursor.execute(f"""
|
|
SELECT column_name
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'public'
|
|
AND lower(table_name) = 'usersettings'
|
|
""")
|
|
columns = [col[0] for col in cursor.fetchall()]
|
|
print(f"Existing columns: {columns}")
|
|
|
|
# Check if any variation of 'startpage' exists (case-insensitive)
|
|
startpage_column_exists = False
|
|
startpage_column_name = None
|
|
for col in columns:
|
|
if col.lower() == 'startpage':
|
|
startpage_column_exists = True
|
|
startpage_column_name = col
|
|
break
|
|
|
|
if not startpage_column_exists:
|
|
# The column doesn't exist, add it
|
|
cursor.execute(f"""
|
|
ALTER TABLE "{actual_table_name}"
|
|
ADD COLUMN startpage VARCHAR(255) DEFAULT 'home'
|
|
""")
|
|
print("startpage column added to existing UserSettings table")
|
|
else:
|
|
print(f"startpage column exists as: {startpage_column_name}")
|
|
|
|
# IMPORTANT: The API is trying to access 'startpage' but the column is 'StartPage'
|
|
# Check if we need to fix this by checking the error from the log
|
|
cursor.execute(f"""
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'public'
|
|
AND table_name = lower('{actual_table_name}')
|
|
AND column_name = 'startpage'
|
|
""")
|
|
lowercase_exists = cursor.fetchone()
|
|
|
|
if not lowercase_exists and startpage_column_name != 'startpage':
|
|
print(f"Column exists as {startpage_column_name} but code tries to access 'startpage'. Adding alias column...")
|
|
try:
|
|
# Add a new column and copy data from the existing one
|
|
cursor.execute(f"""
|
|
ALTER TABLE "{actual_table_name}"
|
|
ADD COLUMN startpage VARCHAR(255) DEFAULT 'home'
|
|
""")
|
|
# Fixed the column name reference in the UPDATE statement
|
|
cursor.execute(f"""
|
|
UPDATE "{actual_table_name}"
|
|
SET startpage = "{startpage_column_name}"
|
|
""")
|
|
print("Added lowercase startpage column for compatibility")
|
|
except Exception as e:
|
|
print(f"Error adding compatibility column: {e}")
|
|
|
|
# Always commit the transaction
|
|
cnx.commit()
|
|
except Exception as e:
|
|
# Log the general error and rollback
|
|
print(f"Error handling usersettings table: {e}")
|
|
cnx.rollback()
|
|
|
|
admin_created = False
|
|
try:
|
|
admin_fullname = os.environ.get("FULLNAME")
|
|
admin_username = os.environ.get("USERNAME")
|
|
admin_email = os.environ.get("EMAIL")
|
|
admin_pw = os.environ.get("PASSWORD")
|
|
|
|
if all([admin_fullname, admin_username, admin_email, admin_pw]):
|
|
hashed_pw = hash_password(admin_pw).strip()
|
|
admin_insert_query = """
|
|
INSERT INTO "Users" (Fullname, Username, Email, Hashed_PW, IsAdmin)
|
|
VALUES (%s, %s, %s, %s, %s::boolean)
|
|
ON CONFLICT (Username) DO NOTHING
|
|
RETURNING UserID
|
|
"""
|
|
cursor.execute(admin_insert_query, (admin_fullname, admin_username, admin_email, hashed_pw, True))
|
|
admin_created = cursor.fetchone() is not None
|
|
cnx.commit()
|
|
except Exception as e:
|
|
print(f"Error creating default admin: {e}")
|
|
|
|
# Now handle UserStats and UserSettings
|
|
try:
|
|
# Background tasks user stats
|
|
cursor.execute("""
|
|
INSERT INTO "UserStats" (UserID) VALUES (1)
|
|
ON CONFLICT (UserID) DO NOTHING
|
|
""")
|
|
if admin_created:
|
|
cursor.execute("""
|
|
INSERT INTO "UserStats" (UserID) VALUES (2)
|
|
ON CONFLICT (UserID) DO NOTHING
|
|
""")
|
|
cursor.execute("""
|
|
INSERT INTO "UserSettings" (UserID, Theme) VALUES (2, 'Nordic')
|
|
ON CONFLICT (UserID) DO NOTHING
|
|
""")
|
|
cnx.commit()
|
|
except Exception as e:
|
|
print(f"Error creating user stats/settings: {e}")
|
|
|
|
cursor.execute("""INSERT INTO "UserSettings" (UserID, Theme) VALUES ('1', 'Nordic') ON CONFLICT (UserID) DO NOTHING""")
|
|
if admin_created:
|
|
cursor.execute("""INSERT INTO "UserSettings" (UserID, Theme) VALUES ('2', 'Nordic') ON CONFLICT (UserID) DO NOTHING""")
|
|
|
|
|
|
try:
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "Podcasts" (
|
|
PodcastID SERIAL PRIMARY KEY,
|
|
PodcastIndexID INT,
|
|
PodcastName TEXT,
|
|
ArtworkURL TEXT,
|
|
Author TEXT,
|
|
Categories TEXT,
|
|
Description TEXT,
|
|
EpisodeCount INT,
|
|
FeedURL TEXT,
|
|
WebsiteURL TEXT,
|
|
Explicit BOOLEAN,
|
|
UserID INT,
|
|
AutoDownload BOOLEAN DEFAULT FALSE,
|
|
StartSkip INT DEFAULT 0,
|
|
EndSkip INT DEFAULT 0,
|
|
Username TEXT,
|
|
Password TEXT,
|
|
IsYouTubeChannel BOOLEAN DEFAULT FALSE,
|
|
NotificationsEnabled BOOLEAN DEFAULT FALSE,
|
|
FeedCutoffDays INT DEFAULT 0,
|
|
PlaybackSpeed NUMERIC(2,1) DEFAULT 1.0,
|
|
PlaybackSpeedCustomized BOOLEAN DEFAULT FALSE,
|
|
FOREIGN KEY (UserID) REFERENCES "Users"(UserID)
|
|
)
|
|
""")
|
|
cnx.commit() # Ensure changes are committed
|
|
except Exception as e:
|
|
print(f"Error adding Podcasts table: {e}")
|
|
|
|
try:
|
|
# Add unique constraint on UserID and FeedURL to fix the ON CONFLICT error
|
|
cursor.execute("""
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM pg_constraint
|
|
WHERE conname = 'podcasts_userid_feedurl_key'
|
|
) THEN
|
|
-- Add the constraint if it doesn't exist
|
|
ALTER TABLE "Podcasts"
|
|
ADD CONSTRAINT podcasts_userid_feedurl_key
|
|
UNIQUE (UserID, FeedURL);
|
|
END IF;
|
|
END
|
|
$$;
|
|
""")
|
|
cnx.commit()
|
|
print("Added unique constraint on UserID and FeedURL to Podcasts table")
|
|
except Exception as e:
|
|
print(f"Error adding unique constraint to Podcasts table: {e}")
|
|
|
|
def add_youtube_column_if_not_exist(cursor, cnx):
|
|
try:
|
|
cursor.execute("""
|
|
SELECT column_name
|
|
FROM information_schema.columns
|
|
WHERE table_name='Podcasts'
|
|
AND column_name = 'isyoutubechannel'
|
|
""")
|
|
existing_column = cursor.fetchone()
|
|
|
|
if not existing_column:
|
|
cursor.execute("""
|
|
ALTER TABLE "Podcasts"
|
|
ADD COLUMN "isyoutubechannel" BOOLEAN DEFAULT FALSE
|
|
""")
|
|
print("Added 'IsYouTubeChannel' column to 'Podcasts' table.")
|
|
cnx.commit()
|
|
else:
|
|
print('IsYouTubeChannel column already exists')
|
|
except Exception as e:
|
|
print(f"Error adding IsYouTubeChannel column to Podcasts table: {e}")
|
|
|
|
# Usage
|
|
add_youtube_column_if_not_exist(cursor, cnx)
|
|
|
|
def add_playbackspeed_if_not_exist_podcasts(cursor, cnx):
|
|
try:
|
|
cursor.execute("""
|
|
SELECT column_name
|
|
FROM information_schema.columns
|
|
WHERE table_name='Podcasts'
|
|
AND column_name = 'playbackspeed'
|
|
""")
|
|
existing_column = cursor.fetchone()
|
|
if not existing_column:
|
|
cursor.execute("""
|
|
ALTER TABLE "Podcasts"
|
|
ADD COLUMN PlaybackSpeed NUMERIC(2,1) DEFAULT 1.0
|
|
""")
|
|
print("Added 'PlaybackSpeed' column to 'Podcasts' table.")
|
|
cnx.commit()
|
|
else:
|
|
print("Column 'PlaybackSpeed' already exists in 'Podcasts' table. ")
|
|
except Exception as e:
|
|
print(f"Error checking PlaybackSpeed column in Podcasts table: {e}")
|
|
# No commit or rollback here, just like in your working example
|
|
|
|
# Usage - should be called during app startup
|
|
add_playbackspeed_if_not_exist_podcasts(cursor, cnx)
|
|
|
|
def add_playbackspeed_customized_if_not_exist(cursor, cnx):
|
|
try:
|
|
cursor.execute("""
|
|
SELECT column_name
|
|
FROM information_schema.columns
|
|
WHERE table_name='Podcasts'
|
|
AND column_name = 'playbackspeedcustomized'
|
|
""")
|
|
existing_column = cursor.fetchone()
|
|
if not existing_column:
|
|
cursor.execute("""
|
|
ALTER TABLE "Podcasts"
|
|
ADD COLUMN PlaybackSpeedCustomized BOOLEAN DEFAULT FALSE
|
|
""")
|
|
print("Added 'PlaybackSpeedCustomized' column to 'Podcasts' table.")
|
|
cnx.commit()
|
|
else:
|
|
print('PlaybackSpeedCustomized column already exists')
|
|
except Exception as e:
|
|
print(f"Error adding PlaybackSpeedCustomized column to Podcasts table: {e}")
|
|
|
|
# Usage - should be called during app startup
|
|
add_playbackspeed_customized_if_not_exist(cursor, cnx)
|
|
|
|
def add_feed_cutoff_column_if_not_exist(cursor, cnx):
|
|
try:
|
|
cursor.execute("""
|
|
SELECT column_name
|
|
FROM information_schema.columns
|
|
WHERE table_name='Podcasts'
|
|
AND column_name = 'feedcutoffdays'
|
|
""")
|
|
existing_column = cursor.fetchone()
|
|
|
|
if not existing_column:
|
|
cursor.execute("""
|
|
ALTER TABLE "Podcasts"
|
|
ADD COLUMN "feedcutoffdays" INT DEFAULT 0
|
|
""")
|
|
print("Added 'feedcutoffdays' column to 'Podcasts' table.")
|
|
cnx.commit()
|
|
except Exception as e:
|
|
print(f"Error adding feedcutoffdays column to Podcasts table: {e}")
|
|
|
|
add_feed_cutoff_column_if_not_exist(cursor, cnx)
|
|
|
|
cursor.execute("SELECT to_regclass('public.\"Podcasts\"')")
|
|
|
|
try:
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "Episodes" (
|
|
EpisodeID SERIAL PRIMARY KEY,
|
|
PodcastID INT,
|
|
EpisodeTitle TEXT,
|
|
EpisodeDescription TEXT,
|
|
EpisodeURL TEXT,
|
|
EpisodeArtwork TEXT,
|
|
EpisodePubDate TIMESTAMP,
|
|
EpisodeDuration INT,
|
|
Completed BOOLEAN DEFAULT FALSE,
|
|
FOREIGN KEY (PodcastID) REFERENCES "Podcasts"(PodcastID)
|
|
)
|
|
""")
|
|
|
|
cnx.commit() # Ensure changes are committed
|
|
except Exception as e:
|
|
print(f"Error adding Episodes table: {e}")
|
|
|
|
try:
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "YouTubeVideos" (
|
|
VideoID SERIAL PRIMARY KEY,
|
|
PodcastID INT,
|
|
VideoTitle TEXT,
|
|
VideoDescription TEXT,
|
|
VideoURL TEXT,
|
|
ThumbnailURL TEXT,
|
|
PublishedAt TIMESTAMP,
|
|
Duration INT,
|
|
YouTubeVideoID TEXT,
|
|
Completed BOOLEAN DEFAULT FALSE,
|
|
ListenPosition INT DEFAULT 0,
|
|
FOREIGN KEY (PodcastID) REFERENCES "Podcasts"(PodcastID)
|
|
)
|
|
""")
|
|
|
|
cnx.commit() # Ensure changes are committed
|
|
except Exception as e:
|
|
print(f"Error adding YoutubeVideos table: {e}")
|
|
|
|
def create_index_if_not_exists(cursor, index_name, table_name, column_name):
|
|
cursor.execute(f"""
|
|
SELECT 1
|
|
FROM pg_indexes
|
|
WHERE lower(indexname) = lower('{index_name}') AND tablename = '{table_name}'
|
|
""")
|
|
if not cursor.fetchone():
|
|
cursor.execute(f'CREATE INDEX {index_name} ON "{table_name}"({column_name})')
|
|
|
|
create_index_if_not_exists(cursor, "idx_podcasts_userid", "Podcasts", "UserID")
|
|
create_index_if_not_exists(cursor, "idx_episodes_podcastid", "Episodes", "PodcastID")
|
|
create_index_if_not_exists(cursor, "idx_episodes_episodepubdate", "Episodes", "EpisodePubDate")
|
|
|
|
try:
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "People" (
|
|
PersonID SERIAL PRIMARY KEY,
|
|
Name TEXT,
|
|
PersonImg TEXT,
|
|
PeopleDBID INT,
|
|
AssociatedPodcasts TEXT,
|
|
UserID INT,
|
|
FOREIGN KEY (UserID) REFERENCES "Users"(UserID)
|
|
);
|
|
|
|
""")
|
|
cnx.commit()
|
|
except Exception as e:
|
|
print(f"Error creating People table: {e}")
|
|
|
|
try:
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "PeopleEpisodes" (
|
|
EpisodeID SERIAL PRIMARY KEY,
|
|
PersonID INT,
|
|
PodcastID INT,
|
|
EpisodeTitle TEXT,
|
|
EpisodeDescription TEXT,
|
|
EpisodeURL TEXT,
|
|
EpisodeArtwork TEXT,
|
|
EpisodePubDate TIMESTAMP,
|
|
EpisodeDuration INT,
|
|
AddedDate TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (PersonID) REFERENCES "People"(PersonID),
|
|
FOREIGN KEY (PodcastID) REFERENCES "Podcasts"(PodcastID)
|
|
);
|
|
|
|
""")
|
|
cnx.commit()
|
|
except Exception as e:
|
|
print(f"Error creating People table: {e}")
|
|
|
|
create_index_if_not_exists(cursor, "idx_people_episodes_person", "PeopleEpisodes", "PersonID")
|
|
create_index_if_not_exists(cursor, "idx_people_episodes_podcast", "PeopleEpisodes", "PodcastID")
|
|
|
|
|
|
try:
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "SharedEpisodes" (
|
|
SharedEpisodeID SERIAL PRIMARY KEY,
|
|
EpisodeID INT,
|
|
UrlKey TEXT,
|
|
ExpirationDate TIMESTAMP,
|
|
FOREIGN KEY (EpisodeID) REFERENCES "Episodes"(EpisodeID)
|
|
)
|
|
""")
|
|
cnx.commit()
|
|
except Exception as e:
|
|
print(f"Error creating SharedEpisodes table: {e}")
|
|
|
|
|
|
cursor.execute("""CREATE TABLE IF NOT EXISTS "UserEpisodeHistory" (
|
|
UserEpisodeHistoryID SERIAL PRIMARY KEY,
|
|
UserID INT,
|
|
EpisodeID INT,
|
|
ListenDate TIMESTAMP,
|
|
ListenDuration INT,
|
|
FOREIGN KEY (UserID) REFERENCES "Users"(UserID),
|
|
FOREIGN KEY (EpisodeID) REFERENCES "Episodes"(EpisodeID)
|
|
)""")
|
|
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "UserVideoHistory" (
|
|
UserVideoHistoryID SERIAL PRIMARY KEY,
|
|
UserID INT,
|
|
VideoID INT,
|
|
ListenDate TIMESTAMP,
|
|
ListenDuration INT DEFAULT 0,
|
|
FOREIGN KEY (UserID) REFERENCES "Users"(UserID),
|
|
FOREIGN KEY (VideoID) REFERENCES "YouTubeVideos"(VideoID)
|
|
)
|
|
""")
|
|
|
|
def add_history_constraints_if_not_exists(cursor, cnx):
|
|
try:
|
|
# Check/add constraint for UserEpisodeHistory
|
|
cursor.execute("""
|
|
SELECT constraint_name
|
|
FROM information_schema.table_constraints
|
|
WHERE table_name = 'UserEpisodeHistory'
|
|
AND constraint_type = 'UNIQUE'
|
|
AND constraint_name = 'user_episode_unique'
|
|
""")
|
|
|
|
if not cursor.fetchone():
|
|
cursor.execute("""
|
|
ALTER TABLE "UserEpisodeHistory"
|
|
ADD CONSTRAINT user_episode_unique
|
|
UNIQUE (UserID, EpisodeID)
|
|
""")
|
|
print("Added unique constraint to UserEpisodeHistory table.")
|
|
cnx.commit()
|
|
|
|
# Check/add constraint for UserVideoHistory
|
|
cursor.execute("""
|
|
SELECT constraint_name
|
|
FROM information_schema.table_constraints
|
|
WHERE table_name = 'UserVideoHistory'
|
|
AND constraint_type = 'UNIQUE'
|
|
AND constraint_name = 'user_video_unique'
|
|
""")
|
|
|
|
if not cursor.fetchone():
|
|
cursor.execute("""
|
|
ALTER TABLE "UserVideoHistory"
|
|
ADD CONSTRAINT user_video_unique
|
|
UNIQUE (UserID, VideoID)
|
|
""")
|
|
print("Added unique constraint to UserVideoHistory table.")
|
|
cnx.commit()
|
|
except Exception as e:
|
|
print(f"Error adding unique constraints to history tables: {e}")
|
|
|
|
# Call this after creating both history tables
|
|
add_history_constraints_if_not_exists(cursor, cnx)
|
|
|
|
cursor.execute("""CREATE TABLE IF NOT EXISTS "SavedEpisodes" (
|
|
SaveID SERIAL PRIMARY KEY,
|
|
UserID INT,
|
|
EpisodeID INT,
|
|
SaveDate TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (UserID) REFERENCES "Users"(UserID),
|
|
FOREIGN KEY (EpisodeID) REFERENCES "Episodes"(EpisodeID)
|
|
)""")
|
|
|
|
cursor.execute("""CREATE TABLE IF NOT EXISTS "SavedVideos" (
|
|
SaveID SERIAL PRIMARY KEY,
|
|
UserID INT,
|
|
VideoID INT,
|
|
SaveDate TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (UserID) REFERENCES "Users"(UserID),
|
|
FOREIGN KEY (VideoID) REFERENCES "YouTubeVideos"(VideoID)
|
|
)""")
|
|
|
|
|
|
# Create the DownloadedEpisodes table
|
|
cursor.execute("""CREATE TABLE IF NOT EXISTS "DownloadedEpisodes" (
|
|
DownloadID SERIAL PRIMARY KEY,
|
|
UserID INT,
|
|
EpisodeID INT,
|
|
DownloadedDate TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
DownloadedSize INT,
|
|
DownloadedLocation VARCHAR(255),
|
|
FOREIGN KEY (UserID) REFERENCES "Users"(UserID),
|
|
FOREIGN KEY (EpisodeID) REFERENCES "Episodes"(EpisodeID)
|
|
)""")
|
|
|
|
cursor.execute("""CREATE TABLE IF NOT EXISTS "DownloadedVideos" (
|
|
DownloadID SERIAL PRIMARY KEY,
|
|
UserID INT,
|
|
VideoID INT,
|
|
DownloadedDate TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
DownloadedSize INT,
|
|
DownloadedLocation VARCHAR(255),
|
|
FOREIGN KEY (UserID) REFERENCES "Users"(UserID),
|
|
FOREIGN KEY (VideoID) REFERENCES "YouTubeVideos"(VideoID)
|
|
)""")
|
|
|
|
# Create the EpisodeQueue table
|
|
cursor.execute("""CREATE TABLE IF NOT EXISTS "EpisodeQueue" (
|
|
QueueID SERIAL PRIMARY KEY,
|
|
QueueDate TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
UserID INT,
|
|
EpisodeID INT,
|
|
QueuePosition INT NOT NULL DEFAULT 0,
|
|
is_youtube BOOLEAN DEFAULT FALSE,
|
|
FOREIGN KEY (UserID) REFERENCES "Users"(UserID)
|
|
)""")
|
|
|
|
def remove_episode_queue_constraint(cursor, cnx):
|
|
try:
|
|
# First check if the constraint exists
|
|
check_constraint_query = """
|
|
SELECT constraint_name
|
|
FROM information_schema.table_constraints
|
|
WHERE table_name = 'EpisodeQueue'
|
|
AND constraint_name = 'EpisodeQueue_episodeid_fkey'
|
|
AND constraint_type = 'FOREIGN KEY'
|
|
"""
|
|
cursor.execute(check_constraint_query)
|
|
constraint = cursor.fetchone()
|
|
|
|
if constraint:
|
|
# If it exists, drop it
|
|
cursor.execute('ALTER TABLE "EpisodeQueue" DROP CONSTRAINT "EpisodeQueue_episodeid_fkey"')
|
|
cnx.commit()
|
|
print("Removed EpisodeQueue foreign key constraint")
|
|
else:
|
|
print("EpisodeQueue foreign key constraint not found - no action needed")
|
|
|
|
except Exception as e:
|
|
print(f"Error managing EpisodeQueue constraint: {e}")
|
|
cnx.rollback()
|
|
|
|
remove_episode_queue_constraint(cursor, cnx)
|
|
|
|
def add_queue_youtube_column_if_not_exist(cursor, cnx):
|
|
try:
|
|
# Check if column exists using PostgreSQL's system catalog
|
|
cursor.execute("""
|
|
SELECT EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'EpisodeQueue'
|
|
AND column_name = 'is_youtube'
|
|
)
|
|
""")
|
|
column_exists = cursor.fetchone()[0]
|
|
|
|
if not column_exists:
|
|
cursor.execute("""
|
|
ALTER TABLE "EpisodeQueue"
|
|
ADD COLUMN is_youtube BOOLEAN DEFAULT FALSE
|
|
""")
|
|
cnx.commit()
|
|
print("Added 'is_youtube' column to 'EpisodeQueue' table.")
|
|
else:
|
|
print("Column 'is_youtube' already exists in 'EpisodeQueue' table.")
|
|
|
|
except Exception as e:
|
|
cnx.rollback()
|
|
print(f"Error managing is_youtube column: {e}")
|
|
|
|
add_queue_youtube_column_if_not_exist(cursor, cnx)
|
|
|
|
# Create the Sessions table
|
|
cursor.execute("""CREATE TABLE IF NOT EXISTS "Sessions" (
|
|
SessionID SERIAL PRIMARY KEY,
|
|
UserID INT,
|
|
value TEXT,
|
|
expire TIMESTAMP NOT NULL,
|
|
FOREIGN KEY (UserID) REFERENCES "Users"(UserID)
|
|
)""")
|
|
cnx.commit()
|
|
|
|
# First let's define our functions to check and add columns/tables
|
|
def add_notification_column_if_not_exists(cursor, cnx):
|
|
try:
|
|
cursor.execute("""
|
|
SELECT EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'Podcasts'
|
|
AND column_name = 'notificationsenabled'
|
|
)
|
|
""")
|
|
column_exists = cursor.fetchone()[0]
|
|
|
|
if not column_exists:
|
|
cursor.execute("""
|
|
ALTER TABLE "Podcasts"
|
|
ADD COLUMN NotificationsEnabled BOOLEAN DEFAULT FALSE
|
|
""")
|
|
cnx.commit()
|
|
print("Added 'NotificationsEnabled' column to 'Podcasts' table.")
|
|
else:
|
|
print("Column 'NotificationsEnabled' already exists in 'Podcasts' table.")
|
|
except Exception as e:
|
|
cnx.rollback()
|
|
print(f"Error managing NotificationsEnabled column: {e}")
|
|
|
|
add_notification_column_if_not_exists(cursor, cnx)
|
|
|
|
# Now create the notification settings table if it doesn't exist
|
|
try:
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "UserNotificationSettings" (
|
|
SettingID SERIAL PRIMARY KEY,
|
|
UserID INT,
|
|
Platform VARCHAR(50) NOT NULL,
|
|
Enabled BOOLEAN DEFAULT TRUE,
|
|
NtfyTopic VARCHAR(255),
|
|
NtfyServerUrl VARCHAR(255) DEFAULT 'https://ntfy.sh',
|
|
GotifyUrl VARCHAR(255),
|
|
GotifyToken VARCHAR(255),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (UserID) REFERENCES "Users"(UserID),
|
|
UNIQUE(UserID, platform)
|
|
)
|
|
""")
|
|
cnx.commit()
|
|
print("Checked/Created UserNotificationSettings table")
|
|
except Exception as e:
|
|
print(f"Error creating UserNotificationSettings table: {e}")
|
|
|
|
|
|
try:
|
|
# Create Playlists table with the unique constraint
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "Playlists" (
|
|
PlaylistID SERIAL PRIMARY KEY,
|
|
UserID INT NOT NULL,
|
|
Name VARCHAR(255) NOT NULL,
|
|
Description TEXT,
|
|
IsSystemPlaylist BOOLEAN NOT NULL DEFAULT FALSE,
|
|
PodcastIDs INTEGER[], -- Can be NULL to mean "all podcasts"
|
|
IncludeUnplayed BOOLEAN NOT NULL DEFAULT TRUE,
|
|
IncludePartiallyPlayed BOOLEAN NOT NULL DEFAULT TRUE,
|
|
IncludePlayed BOOLEAN NOT NULL DEFAULT FALSE,
|
|
MinDuration INTEGER, -- NULL means no minimum
|
|
MaxDuration INTEGER, -- NULL means no maximum
|
|
SortOrder VARCHAR(50) NOT NULL DEFAULT 'date_desc'
|
|
CHECK (SortOrder IN ('date_asc', 'date_desc',
|
|
'duration_asc', 'duration_desc',
|
|
'listen_progress', 'completion')),
|
|
GroupByPodcast BOOLEAN NOT NULL DEFAULT FALSE,
|
|
MaxEpisodes INTEGER, -- NULL means no limit
|
|
PlayProgressMin FLOAT, -- NULL means no minimum progress requirement
|
|
PlayProgressMax FLOAT, -- NULL means no maximum progress limit
|
|
TimeFilterHours INTEGER, -- NULL means no time filter
|
|
LastUpdated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
Created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
IconName VARCHAR(50) NOT NULL DEFAULT 'ph-playlist',
|
|
FOREIGN KEY (UserID) REFERENCES "Users"(UserID) ON DELETE CASCADE,
|
|
UNIQUE(UserID, Name),
|
|
CHECK (PlayProgressMin IS NULL OR (PlayProgressMin >= 0 AND PlayProgressMin <= 100)),
|
|
CHECK (PlayProgressMax IS NULL OR (PlayProgressMax >= 0 AND PlayProgressMax <= 100)),
|
|
CHECK (PlayProgressMin IS NULL OR PlayProgressMax IS NULL OR PlayProgressMin <= PlayProgressMax),
|
|
CHECK (MinDuration IS NULL OR MinDuration >= 0),
|
|
CHECK (MaxDuration IS NULL OR MaxDuration >= 0),
|
|
CHECK (MinDuration IS NULL OR MaxDuration IS NULL OR MinDuration <= MaxDuration),
|
|
CHECK (TimeFilterHours IS NULL OR TimeFilterHours > 0),
|
|
CHECK (MaxEpisodes IS NULL OR MaxEpisodes > 0)
|
|
)
|
|
""")
|
|
cnx.commit()
|
|
|
|
# First add the unique constraint if it doesn't exist
|
|
cursor.execute("""
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM pg_constraint
|
|
WHERE conname = 'playlists_userid_name_key'
|
|
) THEN
|
|
ALTER TABLE "Playlists"
|
|
ADD CONSTRAINT playlists_userid_name_key UNIQUE(UserID, Name);
|
|
END IF;
|
|
END $$;
|
|
""")
|
|
cnx.commit()
|
|
|
|
# Create PlaylistContents table
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS "PlaylistContents" (
|
|
PlaylistContentID SERIAL PRIMARY KEY,
|
|
PlaylistID INT,
|
|
EpisodeID INT,
|
|
VideoID INT,
|
|
Position INT,
|
|
DateAdded TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (PlaylistID) REFERENCES "Playlists"(PlaylistID) ON DELETE CASCADE,
|
|
FOREIGN KEY (EpisodeID) REFERENCES "Episodes"(EpisodeID) ON DELETE CASCADE,
|
|
FOREIGN KEY (VideoID) REFERENCES "YouTubeVideos"(VideoID) ON DELETE CASCADE,
|
|
CHECK ((EpisodeID IS NOT NULL AND VideoID IS NULL) OR (EpisodeID IS NULL AND VideoID IS NOT NULL))
|
|
)
|
|
""")
|
|
cnx.commit()
|
|
|
|
# Create indexes
|
|
cursor.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_playlists_userid ON "Playlists"(UserID);
|
|
CREATE INDEX IF NOT EXISTS idx_playlist_contents_playlistid ON "PlaylistContents"(PlaylistID);
|
|
CREATE INDEX IF NOT EXISTS idx_playlist_contents_episodeid ON "PlaylistContents"(EpisodeID);
|
|
CREATE INDEX IF NOT EXISTS idx_playlist_contents_videoid ON "PlaylistContents"(VideoID);
|
|
""")
|
|
cnx.commit()
|
|
|
|
# Define system playlists
|
|
system_playlists = [
|
|
{
|
|
'name': 'Quick Listens',
|
|
'description': 'Short episodes under 15 minutes, perfect for quick breaks',
|
|
'min_duration': None,
|
|
'max_duration': 900, # 15 minutes
|
|
'sort_order': 'duration_asc',
|
|
'icon_name': 'ph-fast-forward'
|
|
},
|
|
{
|
|
'name': 'Longform',
|
|
'description': 'Extended episodes over 1 hour, ideal for long drives or deep dives',
|
|
'min_duration': 3600, # 1 hour
|
|
'max_duration': None,
|
|
'sort_order': 'duration_desc',
|
|
'icon_name': 'ph-car'
|
|
},
|
|
{
|
|
'name': 'Currently Listening',
|
|
'description': 'Episodes you\'ve started but haven\'t finished',
|
|
'min_duration': None,
|
|
'max_duration': None,
|
|
'sort_order': 'date_desc',
|
|
'include_unplayed': False,
|
|
'include_partially_played': True,
|
|
'include_played': False,
|
|
'icon_name': 'ph-play'
|
|
},
|
|
{
|
|
'name': 'Fresh Releases',
|
|
'description': 'Latest episodes from the last 24 hours',
|
|
'min_duration': None,
|
|
'max_duration': None,
|
|
'sort_order': 'date_desc',
|
|
'include_unplayed': True,
|
|
'include_partially_played': False,
|
|
'include_played': False,
|
|
'time_filter_hours': 24,
|
|
'icon_name': 'ph-sparkle'
|
|
},
|
|
{
|
|
'name': 'Weekend Marathon',
|
|
'description': 'Longer episodes (30+ minutes) perfect for weekend listening',
|
|
'min_duration': 1800, # 30 minutes
|
|
'max_duration': None,
|
|
'sort_order': 'duration_desc',
|
|
'group_by_podcast': True,
|
|
'icon_name': 'ph-couch'
|
|
},
|
|
{
|
|
'name': 'Commuter Mix',
|
|
'description': 'Episodes between 20-40 minutes, ideal for average commute times',
|
|
'min_duration': 1200, # 20 minutes
|
|
'max_duration': 2400, # 40 minutes
|
|
'sort_order': 'date_desc',
|
|
'icon_name': 'ph-train'
|
|
},
|
|
{
|
|
'name': 'Almost Done',
|
|
'description': 'Episodes you\'re close to finishing (75%+ complete)',
|
|
'min_duration': None,
|
|
'max_duration': None,
|
|
'sort_order': 'date_asc',
|
|
'include_unplayed': False,
|
|
'include_partially_played': True,
|
|
'include_played': False,
|
|
'play_progress_min': 75.0, # Add this
|
|
'play_progress_max': None, # Can add this too
|
|
'icon_name': 'ph-hourglass'
|
|
}
|
|
]
|
|
|
|
# Insert system playlists
|
|
for playlist in system_playlists:
|
|
try:
|
|
# First check if this playlist already exists
|
|
cursor.execute("""
|
|
SELECT COUNT(*)
|
|
FROM "Playlists"
|
|
WHERE UserID = 1 AND Name = %s AND IsSystemPlaylist = TRUE
|
|
""", (playlist['name'],))
|
|
|
|
if cursor.fetchone()[0] == 0:
|
|
cursor.execute("""
|
|
INSERT INTO "Playlists" (
|
|
UserID,
|
|
Name,
|
|
Description,
|
|
IsSystemPlaylist,
|
|
MinDuration,
|
|
MaxDuration,
|
|
SortOrder,
|
|
GroupByPodcast,
|
|
IncludeUnplayed,
|
|
IncludePartiallyPlayed,
|
|
IncludePlayed,
|
|
IconName,
|
|
TimeFilterHours,
|
|
PlayProgressMin,
|
|
PlayProgressMax
|
|
) VALUES (
|
|
1,
|
|
%s,
|
|
%s,
|
|
TRUE,
|
|
%s,
|
|
%s,
|
|
%s,
|
|
%s,
|
|
%s,
|
|
%s,
|
|
%s,
|
|
%s,
|
|
%s,
|
|
%s,
|
|
%s
|
|
)
|
|
""", (
|
|
playlist['name'],
|
|
playlist['description'],
|
|
playlist.get('min_duration'),
|
|
playlist.get('max_duration'),
|
|
playlist.get('sort_order', 'date_asc'),
|
|
playlist.get('group_by_podcast', False),
|
|
playlist.get('include_unplayed', True),
|
|
playlist.get('include_partially_played', True),
|
|
playlist.get('include_played', False),
|
|
playlist.get('icon_name', 'ph-playlist'),
|
|
playlist.get('time_filter_hours'),
|
|
playlist.get('play_progress_min'),
|
|
playlist.get('play_progress_max')
|
|
))
|
|
cnx.commit()
|
|
print(f"Successfully added system playlist: {playlist['name']}")
|
|
else:
|
|
print(f"System playlist already exists: {playlist['name']}")
|
|
|
|
except Exception as e:
|
|
print(f"Error handling system playlist {playlist['name']}: {e}")
|
|
continue
|
|
|
|
print("Checked/Created Playlist Tables")
|
|
|
|
except psycopg.Error as err:
|
|
logging.error(f"Database error: {err}")
|
|
except Exception as e:
|
|
logging.error(f"General error: {e}")
|
|
|
|
|
|
except psycopg.Error as err:
|
|
logging.error(f"Database error: {err}")
|
|
except Exception as e:
|
|
logging.error(f"General error: {e}")
|
|
|
|
# Ensure to close the cursor and connection
|
|
finally:
|
|
if 'cursor' in locals():
|
|
cursor.close()
|
|
if 'cnx' in locals():
|
|
cnx.close()
|