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

1326 lines
52 KiB
Python

import mysql.connector
import os
import sys
from cryptography.fernet import Fernet
import string
import secrets
import logging
import random
from argon2 import PasswordHasher
from argon2.exceptions import HashingError
from passlib.hash import argon2
# 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
# Retrieve database connection details from environment variables
db_host = os.environ.get("DB_HOST", "127.0.0.1")
db_port = os.environ.get("DB_PORT", "3306")
db_user = os.environ.get("DB_USER", "root")
db_password = os.environ.get("DB_PASSWORD", "password")
db_name = os.environ.get("DB_NAME", "pypods_database")
# Attempt to create a database connector
cnx = mysql.connector.connect(
host=db_host,
port=db_port,
user=db_user,
password=db_password,
database=db_name,
charset='utf8mb4',
collation="utf8mb4_general_ci"
)
# Create a cursor to execute SQL statements
cursor = cnx.cursor()
# Function to ensure all usernames are lowercase
def ensure_usernames_lowercase(cnx):
cursor = cnx.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")
cnx.commit()
cursor.close()
# Function to check and add columns if they don't exist
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}'
AND table_schema=DATABASE();
""")
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 if it doesn't exist (your existing code)
cursor.execute("""
CREATE TABLE IF NOT EXISTS Users (
UserID INT AUTO_INCREMENT PRIMARY KEY,
Fullname VARCHAR(255),
Username VARCHAR(255),
Email VARCHAR(255),
Hashed_PW CHAR(255),
IsAdmin TINYINT(1),
Reset_Code TEXT,
Reset_Expiry DATETIME,
MFA_Secret VARCHAR(70),
TimeZone VARCHAR(50) DEFAULT 'UTC',
TimeFormat INT DEFAULT 24,
DateFormat VARCHAR(3) DEFAULT 'ISO',
FirstLogin TINYINT(1) DEFAULT 0,
GpodderUrl VARCHAR(255) DEFAULT '',
Pod_Sync_Type VARCHAR(50) DEFAULT 'None',
GpodderLoginName VARCHAR(255) DEFAULT '',
GpodderToken VARCHAR(255) DEFAULT '',
EnableRSSFeeds TINYINT(1) DEFAULT 0,
auth_type VARCHAR(50) DEFAULT 'standard',
oidc_provider_id INT,
oidc_subject VARCHAR(255),
PlaybackSpeed DECIMAL(2,1) UNSIGNED DEFAULT 1.0,
UNIQUE (Username)
)
""")
# Create OIDCProviders table if it doesn't exist (MySQL version)
cursor.execute("""
CREATE TABLE IF NOT EXISTS OIDCProviders (
ProviderID INT AUTO_INCREMENT 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 TINYINT(1) DEFAULT 1,
Created DATETIME DEFAULT CURRENT_TIMESTAMP,
Modified DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)
""")
cnx.commit()
# Function to add PlaybackSpeed to Users table for MySQL
def add_playbackspeed_if_not_exist_users_mysql(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 DECIMAL(2,1) UNSIGNED 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}")
add_playbackspeed_if_not_exist_users_mysql(cursor, cnx)
# Add new columns to Users table if they don't exist
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)')
# Check if foreign key exists before adding it
cursor.execute("""
SELECT COUNT(*)
FROM information_schema.table_constraints
WHERE constraint_name = 'fk_oidc_provider'
AND table_name = 'Users'
AND table_schema = DATABASE();
""")
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")
# Add EnableRSSFeeds column if it doesn't exist
cursor.execute("""
SELECT COUNT(*)
FROM information_schema.columns
WHERE table_name = 'Users'
AND column_name = 'EnableRSSFeeds'
""")
if cursor.fetchone()[0] == 0:
cursor.execute("ALTER TABLE Users ADD COLUMN EnableRSSFeeds TINYINT(1) DEFAULT 0")
cursor.execute("""
SELECT COUNT(*)
FROM information_schema.columns
WHERE table_name = 'Users'
AND column_name = 'PlaybackSpeed'
""")
if cursor.fetchone()[0] == 0:
cursor.execute("ALTER TABLE Users ADD COLUMN PlaybackSpeed DECIMAL(2,1) UNSIGNED DEFAULT 1.0")
# Add EnableRSSFeeds column if it doesn't exist
cursor.execute("""
SELECT COUNT(*)
FROM information_schema.columns
WHERE table_name = 'Podcasts'
AND column_name = 'PlaybackSpeed'
""")
if cursor.fetchone()[0] == 0:
cursor.execute("ALTER TABLE Podcasts ADD COLUMN PlaybackSpeed DECIMAL(2,1) UNSIGNED DEFAULT 1.0")
ensure_usernames_lowercase(cnx)
def add_pod_sync_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
add_pod_sync_if_not_exists(cursor, 'Users', 'Pod_Sync_Type', 'VARCHAR(50) DEFAULT \'None\'')
cursor.execute("""CREATE TABLE IF NOT EXISTS APIKeys (
APIKeyID INT AUTO_INCREMENT 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 INT AUTO_INCREMENT 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()
try:
cursor.execute("""
CREATE TABLE IF NOT EXISTS GpodderDevices (
DeviceID INT AUTO_INCREMENT 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
# Check if index exists before creating it
cursor.execute("""
SELECT COUNT(1) IndexExists FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_schema = DATABASE()
AND table_name = 'GpodderDevices'
AND index_name = 'idx_gpodder_devices_userid'
""")
index_exists = cursor.fetchone()[0]
if index_exists == 0:
# Create index only if it doesn't exist
cursor.execute("""
CREATE INDEX idx_gpodder_devices_userid
ON GpodderDevices(UserID)
""")
cnx.commit()
print("Created index idx_gpodder_devices_userid")
else:
print("Index idx_gpodder_devices_userid already exists")
# Create a table for subscription history/sync state
cursor.execute("""
CREATE TABLE IF NOT EXISTS GpodderSyncState (
SyncStateID INT AUTO_INCREMENT 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 INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
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 INT AUTO_INCREMENT PRIMARY KEY,
SelfServiceUser TINYINT(1) DEFAULT 0,
DownloadEnabled TINYINT(1) DEFAULT 1,
EncryptionKey BINARY(44), -- Set the data type to BINARY(32) to hold the 32-byte key
NewsFeedSubscribed TINYINT(1) DEFAULT 0
)
""")
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 (0, 1, %s)
""", (key,))
cursor.execute("""
CREATE TABLE IF NOT EXISTS EmailSettings (
EmailSettingsID INT AUTO_INCREMENT PRIMARY KEY,
Server_Name VARCHAR(255),
Server_Port INT,
From_Email VARCHAR(255),
Send_Mode VARCHAR(255),
Encryption VARCHAR(255),
Auth_Required TINYINT(1),
Username VARCHAR(255),
Password VARCHAR(255)
)
""")
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', 1, 'default_username', 'default_password')
""")
# 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
# Check if a user with the username 'guest' exists
def user_exists(cursor, username):
cursor.execute("""
SELECT 1 FROM Users WHERE Username = %s
""", (username,))
return cursor.fetchone() is not None
def insert_or_update_user(cursor, hashed_password):
try:
# First, check if 'background_tasks' user exists
cursor.execute("SELECT * FROM Users WHERE Username = %s", ('background_tasks',))
existing_user = cursor.fetchone()
if existing_user:
# Update existing 'background_tasks' user
cursor.execute("""
UPDATE Users
SET Fullname = %s, Email = %s, Hashed_PW = %s, IsAdmin = %s
WHERE Username = %s
""", ('Background Tasks', 'inactive', hashed_password, False, 'background_tasks'))
logging.info("Updated existing 'background_tasks' user.")
else:
# Check for 'guest' or 'bt' users to update
cursor.execute("SELECT Username FROM Users WHERE Username IN ('guest', 'bt')")
old_user = cursor.fetchone()
if old_user:
# Update old user to 'background_tasks'
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, old_user[0]))
logging.info(f"Updated existing '{old_user[0]}' user to 'background_tasks' user.")
else:
# Insert new 'background_tasks' user
cursor.execute("""
INSERT INTO Users (Fullname, Username, Email, Hashed_PW, IsAdmin)
VALUES (%s, %s, %s, %s, %s)
""", ('Background Tasks', 'background_tasks', 'inactive', hashed_password, False))
logging.info("Inserted new 'background_tasks' user.")
except Exception as e:
print(f"Error inserting or updating user: {e}")
logging.error("Error inserting or updating user: %s", e)
# Rollback the transaction in case of error
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)
# Create the web Key
def create_api_key(cnx, user_id=1):
cursor_key = cnx.cursor()
# Check if API key exists for user_id
query = f"SELECT APIKey FROM APIKeys WHERE UserID = {user_id}"
cursor_key.execute(query)
result = cursor_key.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))
# Note the quotes around {api_key}
query = f"INSERT INTO APIKeys (UserID, APIKey) VALUES ({user_id}, '{api_key}')"
cursor_key.execute(query)
cnx.commit()
cursor_key.close()
return api_key
web_api_key = create_api_key(cnx)
with open("/tmp/web_api_key.txt", "w") as f:
f.write(web_api_key)
# Check if admin environment variables are set
admin_fullname = os.environ.get("FULLNAME")
admin_username = os.environ.get("USERNAME")
admin_email = os.environ.get("EMAIL")
admin_pw = os.environ.get("PASSWORD")
admin_created = False
if all([admin_fullname, admin_username, admin_email, admin_pw]):
# Hash the admin password
hashed_pw = hash_password(admin_pw)
admin_insert_query = """INSERT IGNORE INTO Users (Fullname, Username, Email, Hashed_PW, IsAdmin)
VALUES (%s, %s, %s, %s, %s)"""
# Execute the INSERT statement without a separate salt
cursor.execute(admin_insert_query, (admin_fullname, admin_username, admin_email, hashed_pw, 1))
admin_created = True
# Always create stats for background_tasks user
cursor.execute("""INSERT IGNORE INTO UserStats (UserID) VALUES (1)""")
# Only create stats for admin if we created the admin user
if admin_created:
cursor.execute("""INSERT IGNORE INTO UserStats (UserID) VALUES (2)""")
# Create the Podcasts table if it doesn't exist
cursor.execute("""CREATE TABLE IF NOT EXISTS Podcasts (
PodcastID INT AUTO_INCREMENT PRIMARY KEY,
PodcastIndexID INT,
PodcastName TEXT,
ArtworkURL TEXT,
Author TEXT,
Categories TEXT,
Description TEXT,
EpisodeCount INT,
FeedURL TEXT,
WebsiteURL TEXT,
Explicit TINYINT(1),
UserID INT,
AutoDownload TINYINT(1) DEFAULT 0,
StartSkip INT DEFAULT 0,
EndSkip INT DEFAULT 0,
Username TEXT,
Password TEXT,
IsYouTubeChannel TINYINT(1) DEFAULT 0,
NotificationsEnabled TINYINT(1) DEFAULT 0,
FeedCutoffDays INT DEFAULT 0,
PlaybackSpeed DECIMAL(2,1) UNSIGNED DEFAULT 1.0,
PlaybackSpeedCustomized TINYINT(1) DEFAULT 0,
FOREIGN KEY (UserID) REFERENCES Users(UserID)
)""")
def add_youtube_column_if_not_exist(cursor, cnx):
try:
# Check if column exists in MySQL
cursor.execute("""
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'Podcasts'
AND COLUMN_NAME = 'IsYouTubeChannel'
AND TABLE_SCHEMA = DATABASE()
""")
existing_column = cursor.fetchone()
if not existing_column:
cursor.execute("""
ALTER TABLE Podcasts
ADD COLUMN IsYouTubeChannel TINYINT(1) DEFAULT 0
""")
print("Added 'IsYouTubeChannel' column to 'Podcasts' table.")
cnx.commit()
except Exception as e:
print(f"Error adding IsYouTubeChannel column to Podcasts table: {e}")
add_youtube_column_if_not_exist(cursor, cnx)
# Function to add PlaybackSpeed to Podcasts table for MySQL
def add_playbackspeed_if_not_exist_podcasts_mysql(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 DECIMAL(2,1) UNSIGNED 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}")
# Function to add PlaybackSpeedCustomized to Podcasts table for MySQL
def add_playbackspeed_customized_if_not_exist_mysql(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 TINYINT(1) DEFAULT 0
""")
print("Added 'PlaybackSpeedCustomized' column to 'Podcasts' table.")
cnx.commit()
else:
print("Column 'PlaybackSpeedCustomized' already exists in 'Podcasts' table.")
except Exception as e:
print(f"Error checking PlaybackSpeedCustomized column in Podcasts table: {e}")
add_playbackspeed_if_not_exist_podcasts_mysql(cursor, cnx)
add_playbackspeed_customized_if_not_exist_mysql(cursor, cnx)
def add_feed_cutoff_column_if_not_exist(cursor, cnx):
try:
# Check if column exists in MySQL
cursor.execute("""
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'Podcasts'
AND COLUMN_NAME = 'FeedCutoffDays'
AND TABLE_SCHEMA = DATABASE()
""")
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)
def add_user_pass_columns_if_not_exist(cursor, cnx):
try:
# Check if the columns exist
cursor.execute("""
SELECT column_name
FROM information_schema.columns
WHERE table_name='Podcasts'
AND column_name IN ('Username', 'Password')
""")
existing_columns = cursor.fetchall()
existing_columns = [col[0] for col in existing_columns]
# Add Username column if it doesn't exist
if 'Username' not in existing_columns:
cursor.execute("""
ALTER TABLE Podcasts
ADD COLUMN Username TEXT
""")
print("Added 'Username' column to 'Podcasts' table.")
# Add Password column if it doesn't exist
if 'Password' not in existing_columns:
cursor.execute("""
ALTER TABLE Podcasts
ADD COLUMN Password TEXT
""")
print("Added 'Password' column to 'Podcasts' table.")
cnx.commit() # Ensure changes are committed
except Exception as e:
print(f"Error adding columns to Podcasts table: {e}")
# Usage
add_user_pass_columns_if_not_exist(cursor, cnx)
# Check if the new columns exist, and add them if they don't
cursor.execute("SHOW COLUMNS FROM Podcasts LIKE 'AutoDownload'")
result = cursor.fetchone()
if not result:
cursor.execute("""
ALTER TABLE Podcasts
ADD COLUMN AutoDownload TINYINT(1) DEFAULT 0,
ADD COLUMN StartSkip INT DEFAULT 0,
ADD COLUMN EndSkip INT DEFAULT 0
""")
cursor.execute("""CREATE TABLE IF NOT EXISTS Episodes (
EpisodeID INT AUTO_INCREMENT PRIMARY KEY,
PodcastID INT,
EpisodeTitle TEXT,
EpisodeDescription TEXT,
EpisodeURL TEXT,
EpisodeArtwork TEXT,
EpisodePubDate DATETIME,
EpisodeDuration INT,
Completed TINYINT(1) DEFAULT 0,
FOREIGN KEY (PodcastID) REFERENCES Podcasts(PodcastID)
)""")
# Check if the Completed column exists, and add it if it doesn't
cursor.execute("SHOW COLUMNS FROM Episodes LIKE 'Completed'")
result = cursor.fetchone()
if not result:
cursor.execute("""
ALTER TABLE Episodes
ADD COLUMN Completed TINYINT(1) DEFAULT 0
""")
try:
# YouTubeVideos table
cursor.execute("""
CREATE TABLE IF NOT EXISTS YouTubeVideos (
VideoID INT AUTO_INCREMENT PRIMARY KEY,
PodcastID INT,
VideoTitle TEXT,
VideoDescription TEXT,
VideoURL TEXT,
ThumbnailURL TEXT,
PublishedAt TIMESTAMP,
Duration INT,
YouTubeVideoID TEXT,
Completed TINYINT(1) DEFAULT 0,
ListenPosition INT DEFAULT 0,
FOREIGN KEY (PodcastID) REFERENCES Podcasts(PodcastID)
)
""")
cnx.commit()
except Exception as e:
print(f"Error creating YoutubeVideos Table: {e}")
def create_index_if_not_exists(cursor, index_name, table_name, column_name):
cursor.execute(f"SELECT COUNT(1) IndexIsThere FROM INFORMATION_SCHEMA.STATISTICS WHERE table_schema = DATABASE() AND index_name = '{index_name}'")
if cursor.fetchone()[0] == 0:
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 INT AUTO_INCREMENT 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 INT AUTO_INCREMENT PRIMARY KEY,
PersonID INT,
PodcastID INT,
EpisodeTitle TEXT,
EpisodeDescription TEXT,
EpisodeURL TEXT,
EpisodeArtwork TEXT,
EpisodePubDate DATETIME,
EpisodeDuration INT,
AddedDate DATETIME 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 PeopleEpisodes 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 INT AUTO_INCREMENT PRIMARY KEY,
EpisodeID INT,
UrlKey TEXT,
ExpirationDate DATETIME,
FOREIGN KEY (EpisodeID) REFERENCES Episodes(EpisodeID)
)
""")
cnx.commit()
except Exception as e:
print(f"Error creating SharedEpisodes table: {e}")
try:
cursor.execute("""CREATE TABLE IF NOT EXISTS UserSettings (
UserSettingID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT UNIQUE,
Theme VARCHAR(255) DEFAULT 'Nordic',
StartPage VARCHAR(255) DEFAULT 'home',
FOREIGN KEY (UserID) REFERENCES Users(UserID)
)""")
except Exception as e:
print(f"Error adding UserSettings table: {e}")
def add_startpage_column():
try:
# Check if the column exists
cursor.execute("""
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME='UserSettings'
AND COLUMN_NAME='StartPage'
AND TABLE_SCHEMA=DATABASE();
""")
# If the column doesn't exist (no rows returned), add it
if not cursor.fetchone():
cursor.execute("""
ALTER TABLE UserSettings
ADD COLUMN StartPage VARCHAR(255) DEFAULT 'home';
""")
print("Successfully added StartPage column to UserSettings table")
else:
print("StartPage column already exists in UserSettings table")
except Exception as e:
print(f"Error adding StartPage column: {e}")
# Call the function to ensure the column exists
add_startpage_column()
cursor.execute("""INSERT IGNORE INTO UserSettings (UserID, Theme) VALUES ('1', 'Nordic')""")
cursor.execute("""INSERT IGNORE INTO UserSettings (UserID, Theme) VALUES ('2', 'Nordic')""")
cursor.execute("""CREATE TABLE IF NOT EXISTS UserEpisodeHistory (
UserEpisodeHistoryID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
EpisodeID INT,
ListenDate DATETIME,
ListenDuration INT,
FOREIGN KEY (UserID) REFERENCES Users(UserID),
FOREIGN KEY (EpisodeID) REFERENCES Episodes(EpisodeID)
)""")
try:
# UserVideoHistory table
cursor.execute("""
CREATE TABLE IF NOT EXISTS UserVideoHistory (
UserVideoHistoryID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
VideoID INT,
ListenDate TIMESTAMP,
ListenDuration INT DEFAULT 0,
FOREIGN KEY (UserID) REFERENCES Users(UserID),
FOREIGN KEY (VideoID) REFERENCES YouTubeVideos(VideoID)
)
""")
cnx.commit()
except Exception as e:
print(f"Error creating UserVideoHistory table: {e}")
cursor.execute("""CREATE TABLE IF NOT EXISTS SavedEpisodes (
SaveID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
EpisodeID INT,
SaveDate TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES Users(UserID),
FOREIGN KEY (EpisodeID) REFERENCES Episodes(EpisodeID)
)""")
try:
# SavedVideos table
cursor.execute("""
CREATE TABLE IF NOT EXISTS SavedVideos (
SaveID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
VideoID INT,
SaveDate TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES Users(UserID),
FOREIGN KEY (VideoID) REFERENCES YouTubeVideos(VideoID)
)
""")
cnx.commit()
except Exception as e:
print(f"Error creating SavedVideos table: {e}")
# Create the DownloadedEpisodes table
cursor.execute("""CREATE TABLE IF NOT EXISTS DownloadedEpisodes (
DownloadID INT AUTO_INCREMENT 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)
)""")
try:
# DownloadedVideos table
cursor.execute("""
CREATE TABLE IF NOT EXISTS DownloadedVideos (
DownloadID INT AUTO_INCREMENT 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)
)
""")
cnx.commit()
except Exception as e:
print(f"Error creating DownloadedVideos table: {e}")
# Create the EpisodeQueue table
cursor.execute("""CREATE TABLE IF NOT EXISTS EpisodeQueue (
QueueID INT AUTO_INCREMENT PRIMARY KEY,
QueueDate TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UserID INT,
EpisodeID INT,
QueuePosition INT NOT NULL DEFAULT 0,
is_youtube TINYINT(1) DEFAULT 0,
FOREIGN KEY (UserID) REFERENCES Users(UserID),
FOREIGN KEY (EpisodeID) REFERENCES Episodes(EpisodeID)
)""")
def add_queue_youtube_column_if_not_exist(cursor, cnx):
try:
# Check if column exists in MySQL
cursor.execute("""
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'EpisodeQueue'
AND COLUMN_NAME = 'is_youtube'
AND TABLE_SCHEMA = DATABASE()
""")
existing_column = cursor.fetchone()
if not existing_column:
try:
# Add the is_youtube column
cursor.execute("""
ALTER TABLE EpisodeQueue
ADD COLUMN is_youtube TINYINT(1) DEFAULT 0
""")
cnx.commit()
print("Added 'is_youtube' column to 'EpisodeQueue' table.")
except Exception as e:
cnx.rollback()
if 'Duplicate column name' not in str(e): # MySQL specific error message
print(f"Error adding is_youtube column to EpisodeQueue table: {e}")
else:
cnx.commit() # Commit transaction even if column exists
except Exception as e:
cnx.rollback()
print(f"Error checking for is_youtube column: {e}")
add_queue_youtube_column_if_not_exist(cursor, cnx)
def add_rssonly_column_if_not_exists(cursor, cnx):
try:
# Check if column exists in MySQL
cursor.execute("""
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'APIKeys'
AND COLUMN_NAME = 'RssOnly'
AND TABLE_SCHEMA = DATABASE()
""")
existing_column = cursor.fetchone()
if not existing_column:
try:
# Add the is_youtube column
cursor.execute("""
ALTER TABLE APIKeys
ADD COLUMN RssOnly TINYINT(1) DEFAULT 0
""")
cnx.commit()
print("Added 'RssOnly' column to 'APIKeys' table.")
except Exception as e:
cnx.rollback()
if 'Duplicate column name' not in str(e): # MySQL specific error message
print(f"Error adding RssOnly column to APIKeys table: {e}")
else:
cnx.commit() # Commit transaction even if column exists
except Exception as e:
cnx.rollback()
print(f"Error checking for is_youtube column: {e}")
add_rssonly_column_if_not_exists(cursor, cnx)
# Create the Sessions table
cursor.execute("""CREATE TABLE IF NOT EXISTS Sessions (
SessionID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
value TEXT,
expire DATETIME NOT NULL,
FOREIGN KEY (UserID) REFERENCES Users(UserID)
)""")
def add_notification_column_if_not_exists(cursor, cnx):
try:
# First check if the column exists
cursor.execute("""
SELECT COUNT(*) as column_exists
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'Podcasts'
AND COLUMN_NAME = 'NotificationsEnabled'
AND TABLE_SCHEMA = DATABASE()
""")
result = cursor.fetchone()
column_exists = result[0] > 0 if isinstance(result, tuple) else result.get('column_exists', 0) > 0
# Only attempt to add the column if it doesn't exist
if not column_exists:
try:
cursor.execute("""
ALTER TABLE Podcasts
ADD COLUMN NotificationsEnabled TINYINT(1) DEFAULT 0
""")
print("Added NotificationsEnabled column to Podcasts table.")
cnx.commit()
except Exception as alter_err:
# Check if the error is because the column already exists
# (This can happen in race conditions or if the schema check was outdated)
if "Duplicate column name" in str(alter_err) or "column already exists" in str(alter_err).lower():
print("Column NotificationsEnabled already exists in Podcasts table.")
else:
# It's a different error, so re-raise it
raise alter_err
else:
print("Column NotificationsEnabled already exists in Podcasts table.")
except Exception as e:
print(f"Error checking/adding NotificationsEnabled column to Podcasts table: {e}")
# Only rollback if we're in a transaction that needs rolling back
try:
cnx.rollback()
except:
pass # If rollback fails, we're not in a transaction
# Create the notification settings table
try:
cursor.execute("""
CREATE TABLE IF NOT EXISTS UserNotificationSettings (
SettingID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
Platform VARCHAR(50) NOT NULL,
Enabled TINYINT(1) DEFAULT 1,
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)
)
""")
print("Checked/Created UserNotificationSettings table")
cnx.commit()
except Exception as e:
print(f"Error creating UserNotificationSettings table: {e}")
cnx.rollback()
# Call our function to add the new column
add_notification_column_if_not_exists(cursor, cnx)
try:
# Create Playlists table with the unique constraint
cursor.execute("""
CREATE TABLE IF NOT EXISTS Playlists (
PlaylistID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT NOT NULL,
Name VARCHAR(255) NOT NULL,
Description TEXT,
IsSystemPlaylist TINYINT(1) NOT NULL DEFAULT 0,
PodcastIDs TEXT, -- Storing as JSON array in MySQL
IncludeUnplayed TINYINT(1) NOT NULL DEFAULT 1,
IncludePartiallyPlayed TINYINT(1) NOT NULL DEFAULT 1,
IncludePlayed TINYINT(1) NOT NULL DEFAULT 0,
MinDuration INT, -- NULL means no minimum
MaxDuration INT, -- NULL means no maximum
SortOrder VARCHAR(50) NOT NULL DEFAULT 'date_desc',
GroupByPodcast TINYINT(1) NOT NULL DEFAULT 0,
MaxEpisodes INT, -- NULL means no limit
PlayProgressMin FLOAT, -- NULL means no minimum progress requirement
PlayProgressMax FLOAT, -- NULL means no maximum progress limit
TimeFilterHours INT, -- NULL means no time filter
LastUpdated DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
Created DATETIME 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),
CHECK (SortOrder IN ('date_asc', 'date_desc',
'duration_asc', 'duration_desc',
'listen_progress', 'completion'))
)
""")
cnx.commit()
# Create PlaylistContents table
cursor.execute("""
CREATE TABLE IF NOT EXISTS PlaylistContents (
PlaylistContentID INT AUTO_INCREMENT PRIMARY KEY,
PlaylistID INT,
EpisodeID INT,
VideoID INT,
Position INT,
DateAdded DATETIME 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 - check if they exist first
# Index 1: idx_playlists_userid
cursor.execute("""
SELECT COUNT(1) IndexExists FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_schema = DATABASE()
AND table_name = 'Playlists'
AND index_name = 'idx_playlists_userid'
""")
if cursor.fetchone()[0] == 0:
cursor.execute("CREATE INDEX idx_playlists_userid ON Playlists(UserID)")
cnx.commit()
print("Created index idx_playlists_userid")
# Index 2: idx_playlist_contents_playlistid
cursor.execute("""
SELECT COUNT(1) IndexExists FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_schema = DATABASE()
AND table_name = 'PlaylistContents'
AND index_name = 'idx_playlist_contents_playlistid'
""")
if cursor.fetchone()[0] == 0:
cursor.execute("CREATE INDEX idx_playlist_contents_playlistid ON PlaylistContents(PlaylistID)")
cnx.commit()
print("Created index idx_playlist_contents_playlistid")
# Index 3: idx_playlist_contents_episodeid
cursor.execute("""
SELECT COUNT(1) IndexExists FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_schema = DATABASE()
AND table_name = 'PlaylistContents'
AND index_name = 'idx_playlist_contents_episodeid'
""")
if cursor.fetchone()[0] == 0:
cursor.execute("CREATE INDEX idx_playlist_contents_episodeid ON PlaylistContents(EpisodeID)")
cnx.commit()
print("Created index idx_playlist_contents_episodeid")
# Index 4: idx_playlist_contents_videoid
cursor.execute("""
SELECT COUNT(1) IndexExists FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_schema = DATABASE()
AND table_name = 'PlaylistContents'
AND index_name = 'idx_playlist_contents_videoid'
""")
if cursor.fetchone()[0] == 0:
cursor.execute("CREATE INDEX idx_playlist_contents_videoid ON PlaylistContents(VideoID)")
cnx.commit()
print("Created index idx_playlist_contents_videoid")
# 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,
'play_progress_max': None,
'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 = 1
""", (playlist['name'],))
if cursor.fetchone()[0] == 0:
cursor.execute("""
INSERT INTO Playlists (
UserID,
Name,
Description,
IsSystemPlaylist,
MinDuration,
MaxDuration,
SortOrder,
GroupByPodcast,
IncludeUnplayed,
IncludePartiallyPlayed,
IncludePlayed,
IconName,
PlayProgressMin,
PlayProgressMax,
TimeFilterHours
) VALUES (
1,
%s,
%s,
1,
%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'),
1 if playlist.get('group_by_podcast', False) else 0,
1 if playlist.get('include_unplayed', True) else 0,
1 if playlist.get('include_partially_played', True) else 0,
1 if playlist.get('include_played', False) else 0,
playlist.get('icon_name', 'ph-playlist'),
playlist.get('play_progress_min'),
playlist.get('play_progress_max'),
playlist.get('time_filter_hours')
))
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
except Exception as e:
print(f"Error setting up platlists: {e}")
print("Checked/Created Playlist Tables")
except mysql.connector.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()