Files
PinePods-nix/PinePods-0.8.2/startup/setuppostgresdatabase_legacy.py
2026-03-03 10:57:43 -05:00

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()