use sqlx::{MySql, Pool, Postgres, Row};
use std::time::Duration;
use crate::{config::{Config, OIDCConfig}, error::{AppError, AppResult}};
use chrono::{DateTime, Utc};
use chrono_tz::Tz;
use std::collections::HashMap;
use bigdecimal::ToPrimitive;
use std::sync::{Arc, Mutex};
use lazy_static::lazy_static;
use base64;

// Global temporary MFA secrets storage (matches Python temp_mfa_secrets)
lazy_static! {
    static ref TEMP_MFA_SECRETS: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
}

#[derive(Clone)]
pub enum DatabasePool {
    Postgres(Pool<Postgres>),
    MySQL(Pool<MySql>),
}

impl DatabasePool {
    pub async fn new(config: &Config) -> AppResult<Self> {
        let database_url = config.database_url();
        
        match config.database.db_type.as_str() {
            "postgresql" => {
                let pool = sqlx::postgres::PgPoolOptions::new()
                    .max_connections(config.database.max_connections)
                    .min_connections(config.database.min_connections)
                    .acquire_timeout(Duration::from_secs(30))
                    .connect(&database_url)
                    .await?;

                // Skip migrations for now - database already exists
                
                Ok(DatabasePool::Postgres(pool))
            }
            _ => {
                let pool = sqlx::mysql::MySqlPoolOptions::new()
                    .max_connections(config.database.max_connections)
                    .min_connections(config.database.min_connections)
                    .acquire_timeout(Duration::from_secs(30))
                    .connect(&database_url)
                    .await?;

                // Skip migrations for now - database already exists
                
                Ok(DatabasePool::MySQL(pool))
            }
        }
    }

    pub async fn health_check(&self) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query("SELECT 1 as health")
                    .fetch_one(pool)
                    .await?;
                let health: i32 = row.try_get("health")?;
                Ok(health == 1)
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT 1 as health")
                    .fetch_one(pool)
                    .await?;
                let health: i32 = row.try_get("health")?;
                Ok(health == 1)
            }
        }
    }

    // Helper methods for database operations

    // Verify API key - matches Python verify_api_key function
    pub async fn verify_api_key(&self, api_key: &str) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT * FROM "APIKeys" WHERE apikey = $1"#)
                    .bind(api_key)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(row.is_some())
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT * FROM APIKeys WHERE APIKey = ?")
                    .bind(api_key)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(row.is_some())
            }
        }
    }

    // Verify password - matches Python verify_password function
    pub async fn verify_password(&self, username: &str, password: &str) -> AppResult<bool> {
        use crate::services::auth::verify_password;
        
        let stored_hash = match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT hashed_pw FROM "Users" WHERE username = $1"#)
                    .bind(username)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    row.try_get::<String, _>("hashed_pw")?
                } else {
                    return Ok(false);
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT Hashed_PW FROM Users WHERE Username = ?")
                    .bind(username)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    row.try_get::<String, _>("Hashed_PW")?
                } else {
                    return Ok(false);
                }
            }
        };

        verify_password(password, &stored_hash)
    }

    // Get API key for user - matches Python get_api_key function  
    pub async fn get_api_key(&self, username: &str) -> AppResult<String> {
        match self {
            DatabasePool::Postgres(pool) => {
                // First get UserID
                let user_row = sqlx::query(r#"SELECT userid FROM "Users" WHERE username = $1"#)
                    .bind(username)
                    .fetch_optional(pool)
                    .await?;
                
                let user_id: i32 = match user_row {
                    Some(row) => row.try_get("userid")?,
                    None => return Err(AppError::not_found("User not found")),
                };
                
                // Then get API key
                let api_row = sqlx::query(r#"SELECT apikey FROM "APIKeys" WHERE userid = $1 LIMIT 1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                match api_row {
                    Some(row) => Ok(row.try_get("apikey")?),
                    None => Err(AppError::not_found("API key not found for user")),
                }
            }
            DatabasePool::MySQL(pool) => {
                // First get UserID
                let user_row = sqlx::query("SELECT UserID FROM Users WHERE Username = ?")
                    .bind(username)
                    .fetch_optional(pool)
                    .await?;
                
                let user_id: i32 = match user_row {
                    Some(row) => row.try_get("UserID")?,
                    None => return Err(AppError::not_found("User not found")),
                };
                
                // Then get API key
                let api_row = sqlx::query("SELECT APIKey FROM APIKeys WHERE UserID = ? LIMIT 1")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                match api_row {
                    Some(row) => Ok(row.try_get("APIKey")?),
                    None => Err(AppError::not_found("API key not found for user")),
                }
            }
        }
    }

    // Get user ID from API key - matches Python get_api_user function
    pub async fn get_user_id_from_api_key(&self, api_key: &str) -> AppResult<i32> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT userid FROM "APIKeys" WHERE apikey = $1 LIMIT 1"#)
                    .bind(api_key)
                    .fetch_one(pool)
                    .await?;
                
                Ok(row.try_get("userid")?)
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT UserID FROM APIKeys WHERE APIKey = ? LIMIT 1")
                    .bind(api_key)
                    .fetch_one(pool)
                    .await?;
                
                Ok(row.try_get("UserID")?)
            }
        }
    }

    // Get user ID from username - for login and key creation
    pub async fn get_user_id_from_username(&self, username: &str) -> AppResult<i32> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT userid FROM "Users" WHERE username = $1"#)
                    .bind(username)
                    .fetch_optional(pool)
                    .await?;
                
                match row {
                    Some(row) => Ok(row.try_get("userid")?),
                    None => Err(AppError::not_found("User not found")),
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT UserID FROM Users WHERE Username = ?")
                    .bind(username)
                    .fetch_optional(pool)
                    .await?;
                
                match row {
                    Some(row) => Ok(row.try_get("UserID")?),
                    None => Err(AppError::not_found("User not found")),
                }
            }
        }
    }

    // Get user details by ID - matches Python get_user_details_id function
    pub async fn get_user_details_by_id(&self, user_id: i32) -> AppResult<crate::handlers::auth::UserDetails> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT * FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                
                Ok(crate::handlers::auth::UserDetails {
                    UserID: row.try_get("userid")?,
                    Fullname: row.try_get("fullname").ok(),
                    Username: row.try_get("username").ok(),
                    Email: row.try_get("email").ok(),
                    Hashed_PW: row.try_get("hashed_pw").ok(),
                    Salt: row.try_get("salt").ok(),
                })
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT * FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                
                Ok(crate::handlers::auth::UserDetails {
                    UserID: row.try_get("UserID")?,
                    Fullname: row.try_get("Fullname").ok(),
                    Username: row.try_get("Username").ok(),
                    Email: row.try_get("Email").ok(),
                    Hashed_PW: row.try_get("Hashed_PW").ok(),
                    Salt: row.try_get("Salt").ok(),
                })
            }
        }
    }

    pub async fn get_user_by_credentials(&self, username: &str) -> AppResult<Option<UserCredentials>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(
                    r#"SELECT userid as user_id, username as username, hashed_pw as hashed_password, email as email
                     FROM "Users" WHERE username = $1"#
                )
                .bind(username)
                .fetch_optional(pool)
                .await?;
                
                if let Some(row) = row {
                    Ok(Some(UserCredentials {
                        user_id: row.try_get("user_id")?,
                        username: row.try_get("username")?,
                        hashed_password: row.try_get("hashed_password")?,
                        email: row.try_get("email")?,
                    }))
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query(
                    "SELECT UserID as user_id, Username as username, Hashed_PW as hashed_password, Email as email
                     FROM Users WHERE Username = ?"
                )
                .bind(username)
                .fetch_optional(pool)
                .await?;
                
                if let Some(row) = row {
                    Ok(Some(UserCredentials {
                        user_id: row.try_get("user_id")?,
                        username: row.try_get("username")?,
                        hashed_password: row.try_get("hashed_password")?,
                        email: row.try_get("email")?,
                    }))
                } else {
                    Ok(None)
                }
            }
        }
    }

    pub async fn get_user_settings_by_user_id(&self, user_id: i32) -> AppResult<Option<UserSettings>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(
                    r#"SELECT us.userid, ak.apikey as api_key, us.theme, 
                              COALESCE(us.auto_download_episodes, false) as auto_download_episodes,
                              COALESCE(us.auto_delete_episodes, false) as auto_delete_episodes
                       FROM "UserSettings" us 
                       LEFT JOIN "APIKeys" ak ON us.userid = ak.userid
                       WHERE us.userid = $1"#
                )
                .bind(user_id)
                .fetch_optional(pool)
                .await?;
                
                if let Some(row) = row {
                    Ok(Some(UserSettings {
                        user_id: row.try_get("userid")?,
                        api_key: row.try_get("api_key")?,
                        theme: row.try_get("theme")?,
                        auto_download_episodes: row.try_get("auto_download_episodes")?,
                        auto_delete_episodes: row.try_get("auto_delete_episodes")?,
                    }))
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query(
                    "SELECT user_id, api_key, theme, auto_download_episodes, auto_delete_episodes
                     FROM UserSettings WHERE user_id = ?"
                )
                .bind(user_id)
                .fetch_optional(pool)
                .await?;
                
                if let Some(row) = row {
                    Ok(Some(UserSettings {
                        user_id: row.try_get("user_id")?,
                        api_key: row.try_get("api_key")?,
                        theme: row.try_get("theme")?,
                        auto_download_episodes: row.try_get("auto_download_episodes")?,
                        auto_delete_episodes: row.try_get("auto_delete_episodes")?,
                    }))
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Get user ID by API key - matches Python get_api_user function
    pub async fn get_api_user(&self, api_key: &str) -> AppResult<i32> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT userid FROM "APIKeys" WHERE apikey = $1"#)
                    .bind(api_key)
                    .fetch_one(pool)
                    .await?;
                
                Ok(row.try_get("userid")?)
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT UserID FROM APIKeys WHERE APIKey = ?")
                    .bind(api_key)
                    .fetch_one(pool)
                    .await?;
                
                Ok(row.try_get("UserID")?)
            }
        }
    }

    // Get episodes for user - matches Python return_episodes function
    pub async fn return_episodes(&self, user_id: i32) -> AppResult<Vec<crate::handlers::podcasts::Episode>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT * FROM (
                        SELECT
                            "Podcasts".podcastname as podcastname,
                            "Episodes".episodetitle as episodetitle,
                            "Episodes".episodepubdate as episodepubdate,
                            "Episodes".episodedescription as episodedescription,
                            CASE 
                                WHEN "Podcasts".usepodcastcoverscustomized = TRUE AND "Podcasts".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                WHEN "Users".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                ELSE "Episodes".episodeartwork
                            END as episodeartwork,
                            "Episodes".episodeurl as episodeurl,
                            "Episodes".episodeduration as episodeduration,
                            "UserEpisodeHistory".listenduration as listenduration,
                            "Episodes".episodeid as episodeid,
                            "Episodes".completed as completed,
                            CASE WHEN "SavedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN "EpisodeQueue".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN "DownloadedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            FALSE as is_youtube
                        FROM "Episodes"
                        INNER JOIN "Podcasts" ON "Episodes".podcastid = "Podcasts".podcastid
                        LEFT JOIN "Users" ON "Podcasts".userid = "Users".userid
                        LEFT JOIN "UserEpisodeHistory" ON
                            "Episodes".episodeid = "UserEpisodeHistory".episodeid
                            AND "UserEpisodeHistory".userid = $1
                        LEFT JOIN "SavedEpisodes" ON
                            "Episodes".episodeid = "SavedEpisodes".episodeid
                            AND "SavedEpisodes".userid = $1
                        LEFT JOIN "EpisodeQueue" ON
                            "Episodes".episodeid = "EpisodeQueue".episodeid
                            AND "EpisodeQueue".userid = $1
                            AND "EpisodeQueue".is_youtube = FALSE
                        LEFT JOIN "DownloadedEpisodes" ON
                            "Episodes".episodeid = "DownloadedEpisodes".episodeid
                            AND "DownloadedEpisodes".userid = $1
                        WHERE "Episodes".episodepubdate >= NOW() - INTERVAL '30 days'
                        AND "Podcasts".userid = $1

                        UNION ALL

                        SELECT
                            "Podcasts".podcastname as podcastname,
                            "YouTubeVideos".videotitle as episodetitle,
                            "YouTubeVideos".publishedat as episodepubdate,
                            "YouTubeVideos".videodescription as episodedescription,
                            CASE 
                                WHEN "Podcasts".usepodcastcoverscustomized = TRUE AND "Podcasts".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                WHEN "Users".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                ELSE "YouTubeVideos".thumbnailurl
                            END as episodeartwork,
                            "YouTubeVideos".videourl as episodeurl,
                            "YouTubeVideos".duration as episodeduration,
                            "YouTubeVideos".listenposition as listenduration,
                            "YouTubeVideos".videoid as episodeid,
                            "YouTubeVideos".completed as completed,
                            CASE WHEN "SavedVideos".videoid IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN "EpisodeQueue".episodeid IS NOT NULL AND "EpisodeQueue".is_youtube = TRUE THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN "DownloadedVideos".videoid IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            TRUE as is_youtube
                        FROM "YouTubeVideos"
                        INNER JOIN "Podcasts" ON "YouTubeVideos".podcastid = "Podcasts".podcastid
                        LEFT JOIN "Users" ON "Podcasts".userid = "Users".userid
                        LEFT JOIN "SavedVideos" ON
                            "YouTubeVideos".videoid = "SavedVideos".videoid
                            AND "SavedVideos".userid = $2
                        LEFT JOIN "EpisodeQueue" ON
                            "YouTubeVideos".videoid = "EpisodeQueue".episodeid
                            AND "EpisodeQueue".userid = $3
                            AND "EpisodeQueue".is_youtube = TRUE
                        LEFT JOIN "DownloadedVideos" ON
                            "YouTubeVideos".videoid = "DownloadedVideos".videoid
                            AND "DownloadedVideos".userid = $4
                        WHERE "YouTubeVideos".publishedat >= NOW() - INTERVAL '30 days'
                        AND "Podcasts".userid = $5
                    ) combined
                    ORDER BY episodepubdate DESC"#
                )
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .fetch_all(pool)
                .await?;
                
                let mut episodes = Vec::new();
                for row in rows {
                    episodes.push(crate::handlers::podcasts::Episode {
                        podcastname: row.try_get("podcastname")?,
                        episodetitle: row.try_get("episodetitle")?,
                        episodepubdate: {
                            let naive = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                            naive.format("%Y-%m-%dT%H:%M:%S").to_string()
                        },
                        episodedescription: row.try_get("episodedescription")?,
                        episodeartwork: row.try_get("episodeartwork")?,
                        episodeurl: row.try_get("episodeurl")?,
                        episodeduration: row.try_get("episodeduration")?,
                        listenduration: row.try_get("listenduration").ok(),
                        episodeid: row.try_get("episodeid")?,
                        completed: row.try_get("completed")?,
                        saved: row.try_get("saved")?,
                        queued: row.try_get("queued")?,
                        downloaded: row.try_get("downloaded")?,
                        is_youtube: row.try_get("is_youtube")?,
                    });
                }
                Ok(episodes)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(
                    "SELECT * FROM (
                        SELECT
                            Podcasts.PodcastName as podcastname,
                            Episodes.EpisodeTitle as episodetitle,
                            Episodes.EpisodePubDate as episodepubdate,
                            Episodes.EpisodeDescription as episodedescription,
                            CASE 
                                WHEN Podcasts.UsePodcastCoversCustomized = 1 AND Podcasts.UsePodcastCovers = 1 THEN Podcasts.ArtworkURL
                                WHEN Users.UsePodcastCovers = 1 THEN Podcasts.ArtworkURL
                                ELSE Episodes.EpisodeArtwork
                            END as episodeartwork,
                            Episodes.EpisodeURL as episodeurl,
                            Episodes.EpisodeDuration as episodeduration,
                            UserEpisodeHistory.ListenDuration as listenduration,
                            Episodes.EpisodeID as episodeid,
                            Episodes.Completed as completed,
                            CASE WHEN SavedEpisodes.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN EpisodeQueue.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN DownloadedEpisodes.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            FALSE as is_youtube
                        FROM Episodes
                        INNER JOIN Podcasts ON Episodes.PodcastID = Podcasts.PodcastID
                        LEFT JOIN Users ON Podcasts.UserID = Users.UserID
                        LEFT JOIN UserEpisodeHistory ON
                            Episodes.EpisodeID = UserEpisodeHistory.EpisodeID
                            AND UserEpisodeHistory.UserID = ?
                        LEFT JOIN SavedEpisodes ON
                            Episodes.EpisodeID = SavedEpisodes.EpisodeID
                            AND SavedEpisodes.UserID = ?
                        LEFT JOIN EpisodeQueue ON
                            Episodes.EpisodeID = EpisodeQueue.EpisodeID
                            AND EpisodeQueue.UserID = ?
                            AND EpisodeQueue.is_youtube = FALSE
                        LEFT JOIN DownloadedEpisodes ON
                            Episodes.EpisodeID = DownloadedEpisodes.EpisodeID
                            AND DownloadedEpisodes.UserID = ?
                        WHERE Episodes.EpisodePubDate >= DATE_SUB(NOW(), INTERVAL 30 DAY)
                        AND Podcasts.UserID = ?

                        UNION ALL

                        SELECT
                            Podcasts.PodcastName as podcastname,
                            YouTubeVideos.VideoTitle as episodetitle,
                            YouTubeVideos.PublishedAt as episodepubdate,
                            YouTubeVideos.VideoDescription as episodedescription,
                            CASE 
                                WHEN Podcasts.UsePodcastCoversCustomized = 1 AND Podcasts.UsePodcastCovers = 1 THEN Podcasts.ArtworkURL
                                WHEN Users.UsePodcastCovers = 1 THEN Podcasts.ArtworkURL
                                ELSE YouTubeVideos.ThumbnailURL
                            END as episodeartwork,
                            YouTubeVideos.VideoURL as episodeurl,
                            YouTubeVideos.Duration as episodeduration,
                            YouTubeVideos.ListenPosition as listenduration,
                            YouTubeVideos.VideoID as episodeid,
                            YouTubeVideos.Completed as completed,
                            CASE WHEN SavedVideos.VideoID IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN EpisodeQueue.EpisodeID IS NOT NULL AND EpisodeQueue.is_youtube = TRUE THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN DownloadedVideos.VideoID IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            TRUE as is_youtube
                        FROM YouTubeVideos
                        INNER JOIN Podcasts ON YouTubeVideos.PodcastID = Podcasts.PodcastID
                        LEFT JOIN Users ON Podcasts.UserID = Users.UserID
                        LEFT JOIN SavedVideos ON
                            YouTubeVideos.VideoID = SavedVideos.VideoID
                            AND SavedVideos.UserID = ?
                        LEFT JOIN EpisodeQueue ON
                            YouTubeVideos.VideoID = EpisodeQueue.EpisodeID
                            AND EpisodeQueue.UserID = ?
                            AND EpisodeQueue.is_youtube = TRUE
                        LEFT JOIN DownloadedVideos ON
                            YouTubeVideos.VideoID = DownloadedVideos.VideoID
                            AND DownloadedVideos.UserID = ?
                        WHERE YouTubeVideos.PublishedAt >= DATE_SUB(NOW(), INTERVAL 30 DAY)
                        AND Podcasts.UserID = ?
                    ) combined
                    ORDER BY episodepubdate DESC"
                )
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .fetch_all(pool)
                .await?;
                
                let mut episodes = Vec::new();
                for row in rows {
                    episodes.push(crate::handlers::podcasts::Episode {
                        podcastname: row.try_get("podcastname")?,
                        episodetitle: row.try_get("episodetitle")?,
                        episodepubdate: {
                            let naive = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                            naive.format("%Y-%m-%dT%H:%M:%S").to_string()
                        },
                        episodedescription: row.try_get("episodedescription")?,
                        episodeartwork: row.try_get("episodeartwork")?,
                        episodeurl: row.try_get("episodeurl")?,
                        episodeduration: row.try_get("episodeduration")?,
                        listenduration: row.try_get("listenduration").ok(),
                        episodeid: row.try_get("episodeid")?,
                        completed: row.try_get("completed")?,
                        saved: row.try_get("saved")?,
                        queued: row.try_get("queued")?,
                        downloaded: row.try_get("downloaded")?,
                        is_youtube: row.try_get("is_youtube")?,
                    });
                }
                Ok(episodes)
            }
        }
    }

    // Add podcast - matches Python add_podcast function exactly
    pub async fn add_podcast(
        &self,
        podcast_values: &crate::handlers::podcasts::PodcastValues,
        podcast_index_id: i64,
        username: Option<&str>,
        password: Option<&str>,
    ) -> AppResult<(i32, Option<i32>)> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Check if podcast already exists
                let existing = sqlx::query(r#"SELECT podcastid, podcastname, feedurl FROM "Podcasts" WHERE feedurl = $1 AND userid = $2"#)
                    .bind(&podcast_values.pod_feed_url)
                    .bind(podcast_values.user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = existing {
                    let podcast_id: i32 = row.try_get("podcastid")?;
                    // Check if there are episodes
                    let episode_count = sqlx::query(r#"SELECT COUNT(*) as count FROM "Episodes" WHERE podcastid = $1"#)
                        .bind(podcast_id)
                        .fetch_one(pool)
                        .await?;
                    
                    let count: i64 = episode_count.try_get("count")?;
                    if count == 0 {
                        // No episodes, add them
                        let first_episode_id = self.add_episodes(podcast_id, &podcast_values.pod_feed_url, 
                                                                  &podcast_values.pod_artwork, false, 
                                                                  username, password).await?;
                        return Ok((podcast_id, first_episode_id));
                    } else {
                        return Ok((podcast_id, None));
                    }
                }
                
                // Convert categories to string
                let category_list = serde_json::to_string(&podcast_values.categories)?;
                
                // Insert new podcast
                let row = sqlx::query(
                    r#"INSERT INTO "Podcasts" 
                       (podcastname, artworkurl, author, categories, description, episodecount, 
                        feedurl, websiteurl, explicit, userid, feedcutoffdays, username, password, podcastindexid)
                       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
                       RETURNING podcastid"#
                )
                .bind(&podcast_values.pod_title)
                .bind(&podcast_values.pod_artwork)
                .bind(&podcast_values.pod_author)
                .bind(&category_list)
                .bind(&podcast_values.pod_description)
                .bind(0) // EpisodeCount starts at 0
                .bind(&podcast_values.pod_feed_url)
                .bind(&podcast_values.pod_website)
                .bind(podcast_values.pod_explicit)
                .bind(podcast_values.user_id)
                .bind(30) // Default feed cutoff days
                .bind(username)
                .bind(password)
                .bind(podcast_index_id)
                .fetch_one(pool)
                .await?;
                
                let podcast_id: i32 = row.try_get("podcastid")?;
                
                // Update UserStats table
                sqlx::query(r#"UPDATE "UserStats" SET podcastsadded = podcastsadded + 1 WHERE userid = $1"#)
                    .bind(podcast_values.user_id)
                    .execute(pool)
                    .await?;
                
                // Add episodes
                let first_episode_id = self.add_episodes(podcast_id, &podcast_values.pod_feed_url, 
                                                          &podcast_values.pod_artwork, false, 
                                                          username, password).await?;
                
                // Count episodes for logging
                let episode_count: i64 = sqlx::query_scalar(r#"SELECT COUNT(*) FROM "Episodes" WHERE podcastid = $1"#)
                    .bind(podcast_id)
                    .fetch_one(pool)
                    .await?;
                
                println!("✅ Added podcast '{}' for user {} with {} episodes", 
                    podcast_values.pod_title, podcast_values.user_id, episode_count);
                
                Ok((podcast_id, first_episode_id))
            }
            DatabasePool::MySQL(pool) => {
                // Check if podcast already exists
                let existing = sqlx::query("SELECT PodcastID, PodcastName, FeedURL FROM Podcasts WHERE FeedURL = ? AND UserID = ?")
                    .bind(&podcast_values.pod_feed_url)
                    .bind(podcast_values.user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = existing {
                    let podcast_id: i32 = row.try_get("PodcastID")?;
                    // Check if there are episodes
                    let episode_count = sqlx::query("SELECT COUNT(*) as count FROM Episodes WHERE PodcastID = ?")
                        .bind(podcast_id)
                        .fetch_one(pool)
                        .await?;
                    
                    let count: i64 = episode_count.try_get("count")?;
                    if count == 0 {
                        // No episodes, add them
                        let first_episode_id = self.add_episodes(podcast_id, &podcast_values.pod_feed_url, 
                                                                  &podcast_values.pod_artwork, false, 
                                                                  username, password).await?;
                        return Ok((podcast_id, first_episode_id));
                    } else {
                        return Ok((podcast_id, None));
                    }
                }
                
                // Convert categories to string
                let category_list = serde_json::to_string(&podcast_values.categories)?;
                
                // Insert new podcast
                let result = sqlx::query(
                    "INSERT INTO Podcasts 
                     (PodcastName, ArtworkURL, Author, Categories, Description, EpisodeCount, 
                      FeedURL, WebsiteURL, Explicit, UserID, FeedCutoffDays, Username, Password, PodcastIndexID)
                     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
                )
                .bind(&podcast_values.pod_title)
                .bind(&podcast_values.pod_artwork)
                .bind(&podcast_values.pod_author)
                .bind(&category_list)
                .bind(&podcast_values.pod_description)
                .bind(0) // EpisodeCount starts at 0
                .bind(&podcast_values.pod_feed_url)
                .bind(&podcast_values.pod_website)
                .bind(if podcast_values.pod_explicit { 1 } else { 0 })
                .bind(podcast_values.user_id)
                .bind(30) // Default feed cutoff days
                .bind(username)
                .bind(password)
                .bind(podcast_index_id)
                .execute(pool)
                .await?;
                
                let podcast_id = result.last_insert_id() as i32;
                
                // Update UserStats table
                sqlx::query("UPDATE UserStats SET PodcastsAdded = PodcastsAdded + 1 WHERE UserID = ?")
                    .bind(podcast_values.user_id)
                    .execute(pool)
                    .await?;
                
                // Add episodes
                let first_episode_id = self.add_episodes(podcast_id, &podcast_values.pod_feed_url, 
                                                          &podcast_values.pod_artwork, false, 
                                                          username, password).await?;
                
                // Count episodes for logging
                let episode_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM Episodes WHERE PodcastID = ?")
                    .bind(podcast_id)
                    .fetch_one(pool)
                    .await?;
                
                println!("✅ Added podcast '{}' for user {} with {} episodes", 
                    podcast_values.pod_title, podcast_values.user_id, episode_count);
                
                Ok((podcast_id, first_episode_id))
            }
        }
    }

    // Add podcast without episodes - for background episode processing
    pub async fn add_podcast_without_episodes(
        &self,
        podcast_values: &crate::handlers::podcasts::PodcastValues,
        podcast_index_id: i64,
        username: Option<&str>,
        password: Option<&str>,
    ) -> AppResult<i32> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Check if podcast already exists
                let existing = sqlx::query(r#"SELECT podcastid, podcastname, feedurl FROM "Podcasts" WHERE feedurl = $1 AND userid = $2"#)
                    .bind(&podcast_values.pod_feed_url)
                    .bind(podcast_values.user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = existing {
                    let podcast_id: i32 = row.try_get("podcastid")?;
                    return Ok(podcast_id);
                }
                
                // Convert categories to string
                let category_list = serde_json::to_string(&podcast_values.categories)?;
                
                // Insert new podcast without episodes
                let row = sqlx::query(
                    r#"INSERT INTO "Podcasts" 
                       (podcastname, artworkurl, author, categories, description, episodecount, 
                        feedurl, websiteurl, explicit, userid, feedcutoffdays, username, password, podcastindexid)
                       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
                       RETURNING podcastid"#
                )
                .bind(&podcast_values.pod_title)
                .bind(&podcast_values.pod_artwork)
                .bind(&podcast_values.pod_author)
                .bind(&category_list)
                .bind(&podcast_values.pod_description)
                .bind(0) // EpisodeCount starts at 0
                .bind(&podcast_values.pod_feed_url)
                .bind(&podcast_values.pod_website)
                .bind(podcast_values.pod_explicit)
                .bind(podcast_values.user_id)
                .bind(30) // Default feed cutoff days
                .bind(username)
                .bind(password)
                .bind(podcast_index_id)
                .fetch_one(pool)
                .await?;
                
                let podcast_id: i32 = row.try_get("podcastid")?;
                
                // Update UserStats table
                sqlx::query(r#"UPDATE "UserStats" SET podcastsadded = podcastsadded + 1 WHERE userid = $1"#)
                    .bind(podcast_values.user_id)
                    .execute(pool)
                    .await?;
                
                println!("✅ Added podcast '{}' for user {} (episodes will be processed in background)", 
                    podcast_values.pod_title, podcast_values.user_id);
                
                Ok(podcast_id)
            }
            DatabasePool::MySQL(pool) => {
                // Check if podcast already exists
                let existing = sqlx::query("SELECT PodcastID, PodcastName, FeedURL FROM Podcasts WHERE FeedURL = ? AND UserID = ?")
                    .bind(&podcast_values.pod_feed_url)
                    .bind(podcast_values.user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = existing {
                    let podcast_id: i32 = row.try_get("PodcastID")?;
                    return Ok(podcast_id);
                }
                
                // Convert categories to string
                let category_list = serde_json::to_string(&podcast_values.categories)?;
                
                // Insert new podcast without episodes
                let result = sqlx::query(
                    "INSERT INTO Podcasts 
                     (PodcastName, ArtworkURL, Author, Categories, Description, EpisodeCount, 
                      FeedURL, WebsiteURL, Explicit, UserID, FeedCutoffDays, Username, Password, PodcastIndexID)
                     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
                )
                .bind(&podcast_values.pod_title)
                .bind(&podcast_values.pod_artwork)
                .bind(&podcast_values.pod_author)
                .bind(&category_list)
                .bind(&podcast_values.pod_description)
                .bind(0) // EpisodeCount starts at 0
                .bind(&podcast_values.pod_feed_url)
                .bind(&podcast_values.pod_website)
                .bind(podcast_values.pod_explicit)
                .bind(podcast_values.user_id)
                .bind(30) // Default feed cutoff days
                .bind(username)
                .bind(password)
                .bind(podcast_index_id)
                .execute(pool)
                .await?;
                
                let podcast_id = result.last_insert_id() as i32;
                
                // Update UserStats table
                sqlx::query("UPDATE UserStats SET PodcastsAdded = PodcastsAdded + 1 WHERE UserID = ?")
                    .bind(podcast_values.user_id)
                    .execute(pool)
                    .await?;
                
                println!("✅ Added podcast '{}' for user {} (episodes will be processed in background)", 
                    podcast_values.pod_title, podcast_values.user_id);
                
                Ok(podcast_id)
            }
        }
    }

    // Remove podcast - matches Python remove_podcast function
    pub async fn remove_podcast(
        &self,
        podcast_name: &str,
        podcast_url: &str,
        user_id: i32,
    ) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                // First get the podcast ID to cascade delete properly
                let podcast_row = sqlx::query(
                    r#"SELECT podcastid FROM "Podcasts" 
                       WHERE podcastname = $1 AND feedurl = $2 AND userid = $3"#
                )
                .bind(podcast_name)
                .bind(podcast_url)
                .bind(user_id)
                .fetch_optional(pool)
                .await?;
                
                if let Some(row) = podcast_row {
                    let podcast_id: i32 = row.try_get("podcastid")?;
                    
                    // Delete in the proper order to handle foreign key constraints
                    // 1. PlaylistContents first
                    sqlx::query(r#"DELETE FROM "PlaylistContents" WHERE episodeid IN (SELECT episodeid FROM "Episodes" WHERE podcastid = $1)"#)
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 2. UserEpisodeHistory
                    sqlx::query(r#"DELETE FROM "UserEpisodeHistory" WHERE episodeid IN (SELECT episodeid FROM "Episodes" WHERE podcastid = $1)"#)
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 3. DownloadedEpisodes
                    sqlx::query(r#"DELETE FROM "DownloadedEpisodes" WHERE episodeid IN (SELECT episodeid FROM "Episodes" WHERE podcastid = $1)"#)
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 4. SavedEpisodes
                    sqlx::query(r#"DELETE FROM "SavedEpisodes" WHERE episodeid IN (SELECT episodeid FROM "Episodes" WHERE podcastid = $1)"#)
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 5. EpisodeQueue
                    sqlx::query(r#"DELETE FROM "EpisodeQueue" WHERE episodeid IN (SELECT episodeid FROM "Episodes" WHERE podcastid = $1)"#)
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 6. Episodes
                    sqlx::query(r#"DELETE FROM "Episodes" WHERE podcastid = $1"#)
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 7. Finally delete the podcast itself
                    sqlx::query(r#"DELETE FROM "Podcasts" WHERE podcastid = $1"#)
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // Update user stats
                    sqlx::query(r#"UPDATE "UserStats" SET podcastsadded = podcastsadded - 1 WHERE userid = $1"#)
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                }
                Ok(())
            }
            DatabasePool::MySQL(pool) => {
                // First get the podcast ID to cascade delete properly
                let podcast_row = sqlx::query(
                    "SELECT PodcastID FROM Podcasts 
                     WHERE PodcastName = ? AND FeedURL = ? AND UserID = ?"
                )
                .bind(podcast_name)
                .bind(podcast_url)
                .bind(user_id)
                .fetch_optional(pool)
                .await?;
                
                if let Some(row) = podcast_row {
                    let podcast_id: i32 = row.try_get("PodcastID")?;
                    
                    // Delete in the proper order to handle foreign key constraints
                    // 1. PlaylistContents first
                    sqlx::query("DELETE FROM PlaylistContents WHERE EpisodeID IN (SELECT EpisodeID FROM Episodes WHERE PodcastID = ?)")
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 2. UserEpisodeHistory
                    sqlx::query("DELETE FROM UserEpisodeHistory WHERE EpisodeID IN (SELECT EpisodeID FROM Episodes WHERE PodcastID = ?)")
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 3. DownloadedEpisodes
                    sqlx::query("DELETE FROM DownloadedEpisodes WHERE EpisodeID IN (SELECT EpisodeID FROM Episodes WHERE PodcastID = ?)")
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 4. SavedEpisodes
                    sqlx::query("DELETE FROM SavedEpisodes WHERE EpisodeID IN (SELECT EpisodeID FROM Episodes WHERE PodcastID = ?)")
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 5. EpisodeQueue
                    sqlx::query("DELETE FROM EpisodeQueue WHERE EpisodeID IN (SELECT EpisodeID FROM Episodes WHERE PodcastID = ?)")
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 6. Episodes
                    sqlx::query("DELETE FROM Episodes WHERE PodcastID = ?")
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 7. Finally delete the podcast itself
                    sqlx::query("DELETE FROM Podcasts WHERE PodcastID = ?")
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // Update user stats
                    sqlx::query("UPDATE UserStats SET PodcastsAdded = PodcastsAdded - 1 WHERE UserID = ?")
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                }
                Ok(())
            }
        }
    }

    // Remove podcast by ID - matches Python remove_podcast_id function
    pub async fn remove_podcast_id(&self, podcast_id: i32, user_id: i32) -> AppResult<()> {
        if podcast_id == 0 {
            return Err(AppError::bad_request("Invalid podcast ID"));
        }

        match self {
            DatabasePool::Postgres(pool) => {
                // Delete in the proper order to handle foreign key constraints
                // 1. PlaylistContents first
                sqlx::query(r#"DELETE FROM "PlaylistContents" WHERE episodeid IN (SELECT episodeid FROM "Episodes" WHERE podcastid = $1)"#)
                    .bind(podcast_id)
                    .execute(pool)
                    .await?;
                
                // 2. UserEpisodeHistory
                sqlx::query(r#"DELETE FROM "UserEpisodeHistory" WHERE episodeid IN (SELECT episodeid FROM "Episodes" WHERE podcastid = $1)"#)
                    .bind(podcast_id)
                    .execute(pool)
                    .await?;
                
                // 3. DownloadedEpisodes
                sqlx::query(r#"DELETE FROM "DownloadedEpisodes" WHERE episodeid IN (SELECT episodeid FROM "Episodes" WHERE podcastid = $1)"#)
                    .bind(podcast_id)
                    .execute(pool)
                    .await?;
                
                // 4. SavedEpisodes
                sqlx::query(r#"DELETE FROM "SavedEpisodes" WHERE episodeid IN (SELECT episodeid FROM "Episodes" WHERE podcastid = $1)"#)
                    .bind(podcast_id)
                    .execute(pool)
                    .await?;
                
                // 5. EpisodeQueue
                sqlx::query(r#"DELETE FROM "EpisodeQueue" WHERE episodeid IN (SELECT episodeid FROM "Episodes" WHERE podcastid = $1)"#)
                    .bind(podcast_id)
                    .execute(pool)
                    .await?;
                
                // 6. Episodes
                sqlx::query(r#"DELETE FROM "Episodes" WHERE podcastid = $1"#)
                    .bind(podcast_id)
                    .execute(pool)
                    .await?;
                
                // 7. Finally delete the podcast itself
                sqlx::query(r#"DELETE FROM "Podcasts" WHERE podcastid = $1"#)
                    .bind(podcast_id)
                    .execute(pool)
                    .await?;
                
                // Update user stats
                sqlx::query(r#"UPDATE "UserStats" SET podcastsadded = podcastsadded - 1 WHERE userid = $1"#)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(())
            }
            DatabasePool::MySQL(pool) => {
                // Delete in the proper order to handle foreign key constraints
                // 1. PlaylistContents first
                sqlx::query("DELETE FROM PlaylistContents WHERE EpisodeID IN (SELECT EpisodeID FROM Episodes WHERE PodcastID = ?)")
                    .bind(podcast_id)
                    .execute(pool)
                    .await?;
                
                // 2. UserEpisodeHistory
                sqlx::query("DELETE FROM UserEpisodeHistory WHERE EpisodeID IN (SELECT EpisodeID FROM Episodes WHERE PodcastID = ?)")
                    .bind(podcast_id)
                    .execute(pool)
                    .await?;
                
                // 3. DownloadedEpisodes
                sqlx::query("DELETE FROM DownloadedEpisodes WHERE EpisodeID IN (SELECT EpisodeID FROM Episodes WHERE PodcastID = ?)")
                    .bind(podcast_id)
                    .execute(pool)
                    .await?;
                
                // 4. SavedEpisodes
                sqlx::query("DELETE FROM SavedEpisodes WHERE EpisodeID IN (SELECT EpisodeID FROM Episodes WHERE PodcastID = ?)")
                    .bind(podcast_id)
                    .execute(pool)
                    .await?;
                
                // 5. EpisodeQueue
                sqlx::query("DELETE FROM EpisodeQueue WHERE EpisodeID IN (SELECT EpisodeID FROM Episodes WHERE PodcastID = ?)")
                    .bind(podcast_id)
                    .execute(pool)
                    .await?;
                
                // 6. Episodes
                sqlx::query("DELETE FROM Episodes WHERE PodcastID = ?")
                    .bind(podcast_id)
                    .execute(pool)
                    .await?;
                
                // 7. Finally delete the podcast itself
                sqlx::query("DELETE FROM Podcasts WHERE PodcastID = ?")
                    .bind(podcast_id)
                    .execute(pool)
                    .await?;
                
                // Update user stats
                sqlx::query("UPDATE UserStats SET PodcastsAdded = PodcastsAdded - 1 WHERE UserID = ?")
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(())
            }
        }
    }

    // Get user podcast count - for refresh progress tracking
    pub async fn get_user_podcast_count(&self, user_id: i32) -> AppResult<u32> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT COUNT(*) as count FROM "Podcasts" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                
                Ok(row.try_get::<i64, _>("count")? as u32)
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT COUNT(*) as count FROM Podcasts WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                
                Ok(row.try_get::<i64, _>("count")? as u32)
            }
        }
    }

    // Get user podcasts for refresh - matches Python refresh logic
    pub async fn get_user_podcasts_for_refresh(&self, user_id: i32) -> AppResult<Vec<crate::handlers::refresh::PodcastForRefresh>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT 
                        podcastid as id,
                        podcastname as name,
                        feedurl as feed_url,
                        artworkurl as artwork_url,
                        isyoutubechannel as is_youtube,
                        autodownload as auto_download,
                        username as username,
                        password as password,
                        feedcutoffdays as feed_cutoff_days,
                        userid as user_id
                    FROM "Podcasts" 
                    WHERE userid = $1"#
                )
                .bind(user_id)
                .fetch_all(pool)
                .await?;
                
                let mut podcasts = Vec::new();
                for row in rows {
                    podcasts.push(crate::handlers::refresh::PodcastForRefresh {
                        id: row.try_get("id")?,
                        name: row.try_get("name")?,
                        feed_url: row.try_get("feed_url")?,
                        artwork_url: row.try_get("artwork_url").unwrap_or_default(),
                        is_youtube: row.try_get("is_youtube")?,
                        auto_download: row.try_get("auto_download")?,
                        username: row.try_get("username").ok(),
                        password: row.try_get("password").ok(),
                        feed_cutoff_days: row.try_get("feed_cutoff_days").ok(),
                        user_id: row.try_get("user_id")?,
                    });
                }
                Ok(podcasts)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(
                    "SELECT 
                        PodcastID as id,
                        PodcastName as name,
                        FeedURL as feed_url,
                        ArtworkURL as artwork_url,
                        IsYouTubeChannel as is_youtube,
                        AutoDownload as auto_download,
                        Username as username,
                        Password as password,
                        FeedCutoffDays as feed_cutoff_days,
                        UserID as user_id
                    FROM Podcasts 
                    WHERE UserID = ?"
                )
                .bind(user_id)
                .fetch_all(pool)
                .await?;
                
                let mut podcasts = Vec::new();
                for row in rows {
                    podcasts.push(crate::handlers::refresh::PodcastForRefresh {
                        id: row.try_get("id")?,
                        name: row.try_get("name")?,
                        feed_url: row.try_get("feed_url")?,
                        artwork_url: row.try_get("artwork_url").unwrap_or_default(),
                        is_youtube: row.try_get("is_youtube")?,
                        auto_download: row.try_get("auto_download")?,
                        username: row.try_get("username").ok(),
                        password: row.try_get("password").ok(),
                        feed_cutoff_days: row.try_get("feed_cutoff_days").ok(),
                        user_id: row.try_get("user_id")?,
                    });
                }
                Ok(podcasts)
            }
        }
    }

    // Remove podcast by name and URL - matches Python remove_podcast function
    pub async fn remove_podcast_by_name_url(
        &self,
        podcast_name: &str,
        podcast_url: &str,
        user_id: i32,
    ) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                // First get the podcast ID to cascade delete properly
                let podcast_row = sqlx::query(
                    r#"SELECT podcastid FROM "Podcasts" 
                       WHERE podcastname = $1 AND feedurl = $2 AND userid = $3"#
                )
                .bind(podcast_name)
                .bind(podcast_url)
                .bind(user_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = podcast_row {
                    let podcast_id: i32 = row.try_get("podcastid")?;
                    
                    // Delete in the proper order to handle foreign key constraints
                    // 1. PlaylistContents first
                    sqlx::query(r#"DELETE FROM "PlaylistContents" WHERE "EpisodeID" IN (SELECT "EpisodeID" FROM "Episodes" WHERE "PodcastID" = $1)"#)
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 2. UserEpisodeHistory
                    sqlx::query(r#"DELETE FROM "UserEpisodeHistory" WHERE "EpisodeID" IN (SELECT "EpisodeID" FROM "Episodes" WHERE "PodcastID" = $1)"#)
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 3. DownloadedEpisodes
                    sqlx::query(r#"DELETE FROM "DownloadedEpisodes" WHERE "EpisodeID" IN (SELECT "EpisodeID" FROM "Episodes" WHERE "PodcastID" = $1)"#)
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 4. SavedEpisodes
                    sqlx::query(r#"DELETE FROM "SavedEpisodes" WHERE "EpisodeID" IN (SELECT "EpisodeID" FROM "Episodes" WHERE "PodcastID" = $1)"#)
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 5. QueuedEpisodes (EpisodeQueue in Python)
                    sqlx::query(r#"DELETE FROM "QueuedEpisodes" WHERE "EpisodeID" IN (SELECT "EpisodeID" FROM "Episodes" WHERE "PodcastID" = $1)"#)
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 6. Episodes
                    sqlx::query(r#"DELETE FROM "Episodes" WHERE "PodcastID" = $1"#)
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 7. Finally delete the podcast
                    sqlx::query(r#"DELETE FROM "Podcasts" WHERE "PodcastID" = $1"#)
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    // 8. Update UserStats - decrement PodcastsAdded
                    sqlx::query(r#"UPDATE "UserStats" SET podcastsadded = podcastsadded - 1 WHERE userid = $1"#)
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                }
                
                Ok(())
            }
            DatabasePool::MySQL(pool) => {
                // First get the podcast ID to cascade delete properly
                let podcast_row = sqlx::query(
                    "SELECT PodcastID FROM Podcasts 
                     WHERE PodcastName = ? AND FeedURL = ? AND UserID = ?"
                )
                .bind(podcast_name)
                .bind(podcast_url)
                .bind(user_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = podcast_row {
                    let podcast_id: i32 = row.try_get("podcastid")?;
                    
                    // Delete in the proper order to handle foreign key constraints
                    sqlx::query("DELETE FROM PlaylistContents WHERE EpisodeID IN (SELECT EpisodeID FROM Episodes WHERE PodcastID = ?)")
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    sqlx::query("DELETE FROM UserEpisodeHistory WHERE EpisodeID IN (SELECT EpisodeID FROM Episodes WHERE PodcastID = ?)")
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    sqlx::query("DELETE FROM DownloadedEpisodes WHERE EpisodeID IN (SELECT EpisodeID FROM Episodes WHERE PodcastID = ?)")
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    sqlx::query("DELETE FROM SavedEpisodes WHERE EpisodeID IN (SELECT EpisodeID FROM Episodes WHERE PodcastID = ?)")
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    sqlx::query("DELETE FROM QueuedEpisodes WHERE EpisodeID IN (SELECT EpisodeID FROM Episodes WHERE PodcastID = ?)")
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    sqlx::query("DELETE FROM Episodes WHERE PodcastID = ?")
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    sqlx::query("DELETE FROM Podcasts WHERE PodcastID = ?")
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                    
                    sqlx::query("UPDATE UserStats SET PodcastsAdded = PodcastsAdded - 1 WHERE UserID = ?")
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                }
                
                Ok(())
            }
        }
    }

    // Return podcasts basic - matches Python return_pods function
    pub async fn return_pods(&self, user_id: i32) -> AppResult<Vec<crate::models::PodcastResponse>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT 
                        podcastid as podcastid,
                        COALESCE(podcastname, 'Unknown Podcast') as podcastname,
                        CASE 
                            WHEN artworkurl IS NULL OR artworkurl = '' 
                            THEN '/static/assets/default-podcast.png'
                            ELSE artworkurl
                        END as artworkurl,
                        COALESCE(description, 'No description available') as description,
                        COALESCE(episodecount, 0) as episodecount,
                        COALESCE(websiteurl, '') as websiteurl,
                        COALESCE(feedurl, '') as feedurl,
                        COALESCE(author, 'Unknown Author') as author,
                        COALESCE(categories, '') as categories,
                        COALESCE(explicit, false) as explicit,
                        COALESCE(podcastindexid, 0) as podcastindexid
                    FROM "Podcasts"
                    WHERE userid = $1 AND COALESCE(displaypodcast, TRUE) = TRUE
                    ORDER BY podcastname"#
                )
                .bind(user_id)
                .fetch_all(pool)
                .await?;
                
                let mut podcasts = Vec::new();
                for row in rows {
                    podcasts.push(crate::models::PodcastResponse {
                        podcastid: row.try_get("podcastid")?,
                        podcastname: row.try_get("podcastname")?,
                        artworkurl: row.try_get("artworkurl").ok(),
                        description: row.try_get("description").ok(),
                        episodecount: row.try_get("episodecount").ok(),
                        websiteurl: row.try_get("websiteurl").ok(),
                        feedurl: row.try_get("feedurl")?,
                        author: row.try_get("author").ok(),
                        categories: {
                            let categories_str: String = row.try_get("categories")?;
                            self.parse_categories_json(&categories_str)
                        },
                        explicit: row.try_get("explicit")?,
                        podcastindexid: row.try_get::<i32, _>("podcastindexid").ok().map(|i| i as i64),
                    });
                }
                Ok(podcasts)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(
                    "SELECT 
                        PodcastID as podcastid,
                        COALESCE(PodcastName, 'Unknown Podcast') as podcastname,
                        CASE 
                            WHEN ArtworkURL IS NULL OR ArtworkURL = '' 
                            THEN '/static/assets/default-podcast.png'
                            ELSE ArtworkURL
                        END as artworkurl,
                        COALESCE(Description, 'No description available') as description,
                        COALESCE(EpisodeCount, 0) as episodecount,
                        COALESCE(WebsiteURL, '') as websiteurl,
                        COALESCE(FeedURL, '') as feedurl,
                        COALESCE(Author, 'Unknown Author') as author,
                        COALESCE(Categories, '') as categories,
                        COALESCE(Explicit, false) as explicit,
                        COALESCE(PodcastIndexID, 0) as podcastindexid
                    FROM Podcasts
                    WHERE UserID = ? AND COALESCE(DisplayPodcast, 1) = 1
                    ORDER BY PodcastName"
                )
                .bind(user_id)
                .fetch_all(pool)
                .await?;
                
                let mut podcasts = Vec::new();
                for row in rows {
                    podcasts.push(crate::models::PodcastResponse {
                        podcastid: row.try_get("podcastid")?,
                        podcastname: row.try_get("podcastname")?,
                        artworkurl: row.try_get("artworkurl").ok(),
                        description: row.try_get("description").ok(),
                        episodecount: row.try_get("episodecount").ok(),
                        websiteurl: row.try_get("websiteurl").ok(),
                        feedurl: row.try_get("feedurl")?,
                        author: row.try_get("author").ok(),
                        categories: {
                            let categories_str: String = row.try_get("categories")?;
                            self.parse_categories_json(&categories_str)
                        },
                        explicit: row.try_get("explicit")?,
                        podcastindexid: row.try_get::<i32, _>("podcastindexid").ok().map(|i| i as i64),
                    });
                }
                Ok(podcasts)
            }
        }
    }

    // Return podcasts with extra stats - matches Python return_pods with analytics
    pub async fn return_pods_extra(&self, user_id: i32) -> AppResult<Vec<crate::models::PodcastExtraResponse>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT 
                        p.podcastid as podcastid,
                        COALESCE(p.podcastname, 'Unknown Podcast') as podcastname,
                        CASE 
                            WHEN p.artworkurl IS NULL OR p.artworkurl = '' 
                            THEN '/static/assets/default-podcast.png'
                            ELSE p.artworkurl
                        END as artworkurl,
                        COALESCE(p.description, 'No description available') as description,
                        COALESCE(p.episodecount, 0) as episodecount,
                        COALESCE(p.websiteurl, '') as websiteurl,
                        COALESCE(p.feedurl, '') as feedurl,
                        COALESCE(p.author, 'Unknown Author') as author,
                        COALESCE(p.categories, '') as categories,
                        COALESCE(p.explicit, false) as explicit,
                        COALESCE(p.podcastindexid, 0) as podcastindexid,
                        COUNT(ueh.userepisodehistoryid) as play_count,
                        COUNT(DISTINCT ueh.episodeid) as episodes_played,
                        MIN(e.episodepubdate) as oldest_episode_date,
                        COALESCE(p.isyoutube, false) as is_youtube
                    FROM "Podcasts" p
                    LEFT JOIN "Episodes" e ON p.podcastid = e.podcastid
                    LEFT JOIN "UserEpisodeHistory" ueh ON e.episodeid = ueh.episodeid AND ueh.userid = $1
                    WHERE p.userid = $1 AND COALESCE(p.displaypodcast, TRUE) = TRUE
                    GROUP BY p.podcastid, p.podcastname, p.artworkurl, p.description, 
                             p.episodecount, p.websiteurl, p.feedurl, p.author, 
                             p.categories, p.explicit, p.podcastindexid, p.isyoutube
                    ORDER BY p.podcastname"#
                )
                .bind(user_id)
                .fetch_all(pool)
                .await?;
                
                let mut podcasts = Vec::new();
                for row in rows {
                    let feed_url: String = row.try_get("feedurl")?;
                    let is_youtube = row.try_get("is_youtube").unwrap_or_else(|_| feed_url.contains("youtube.com"));
                    
                    podcasts.push(crate::models::PodcastExtraResponse {
                        podcastid: row.try_get("podcastid")?,
                        podcastname: row.try_get("podcastname")?,
                        artworkurl: row.try_get("artworkurl").ok(),
                        description: row.try_get("description").ok(),
                        episodecount: row.try_get("episodecount").ok(),
                        websiteurl: row.try_get("websiteurl").ok(),
                        feedurl: feed_url,
                        author: row.try_get("author").ok(),
                        categories: {
                            let categories_str: String = row.try_get("categories")?;
                            self.parse_categories_json(&categories_str)
                        },
                        explicit: row.try_get("explicit")?,
                        podcastindexid: row.try_get::<i32, _>("podcastindexid").ok().map(|i| i as i64),
                        play_count: row.try_get("play_count")?,
                        episodes_played: row.try_get("episodes_played")?,
                        oldest_episode_date: row.try_get("oldest_episode_date").ok(),
                        is_youtube,
                    });
                }
                Ok(podcasts)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(
                    "SELECT 
                        p.PodcastID as podcastid,
                        COALESCE(p.PodcastName, 'Unknown Podcast') as podcastname,
                        CASE 
                            WHEN p.ArtworkURL IS NULL OR p.ArtworkURL = '' 
                            THEN '/static/assets/default-podcast.png'
                            ELSE p.ArtworkURL
                        END as artworkurl,
                        COALESCE(p.Description, 'No description available') as description,
                        COALESCE(p.EpisodeCount, 0) as episodecount,
                        COALESCE(p.WebsiteURL, '') as websiteurl,
                        COALESCE(p.FeedURL, '') as feedurl,
                        COALESCE(p.Author, 'Unknown Author') as author,
                        COALESCE(p.Categories, '') as categories,
                        COALESCE(p.Explicit, false) as explicit,
                        COALESCE(p.PodcastIndexID, 0) as podcastindexid,
                        COUNT(ueh.UserEpisodeHistoryID) as play_count,
                        COUNT(DISTINCT ueh.EpisodeID) as episodes_played,
                        MIN(e.EpisodePubDate) as oldest_episode_date,
                        COALESCE(p.IsYouTubeChannel, false) as is_youtube
                    FROM Podcasts p
                    LEFT JOIN Episodes e ON p.PodcastID = e.PodcastID
                    LEFT JOIN UserEpisodeHistory ueh ON e.EpisodeID = ueh.EpisodeID AND ueh.UserID = ?
                    WHERE p.UserID = ? AND COALESCE(p.DisplayPodcast, 1) = 1
                    GROUP BY p.PodcastID, p.PodcastName, p.ArtworkURL, p.Description, 
                             p.EpisodeCount, p.WebsiteURL, p.FeedURL, p.Author, 
                             p.Categories, p.Explicit, p.PodcastIndexID, p.IsYouTubeChannel
                    ORDER BY p.PodcastName"
                )
                .bind(user_id)
                .bind(user_id)
                .fetch_all(pool)
                .await?;
                
                let mut podcasts = Vec::new();
                for row in rows {
                    let feed_url: String = row.try_get("feedurl")?;
                    let is_youtube = row.try_get("is_youtube").unwrap_or_else(|_| feed_url.contains("youtube.com"));
                    
                    podcasts.push(crate::models::PodcastExtraResponse {
                        podcastid: row.try_get("podcastid")?,
                        podcastname: row.try_get("podcastname")?,
                        artworkurl: row.try_get("artworkurl").ok(),
                        description: row.try_get("description").ok(),
                        episodecount: row.try_get("episodecount").ok(),
                        websiteurl: row.try_get("websiteurl").ok(),
                        feedurl: feed_url,
                        author: row.try_get("author").ok(),
                        categories: {
                            let categories_str: String = row.try_get("categories")?;
                            self.parse_categories_json(&categories_str)
                        },
                        explicit: row.try_get("explicit")?,
                        podcastindexid: row.try_get::<i32, _>("podcastindexid").ok().map(|i| i as i64),
                        play_count: row.try_get("play_count")?,
                        episodes_played: row.try_get("episodes_played")?,
                        oldest_episode_date: row.try_get("oldest_episode_date").ok(),
                        is_youtube,
                    });
                }
                Ok(podcasts)
            }
        }
    }

    // Merge podcasts - set DisplayPodcast=FALSE for secondary podcasts and update primary with merged IDs
    pub async fn merge_podcasts(&self, primary_podcast_id: i32, secondary_podcast_ids: &[i32], user_id: i32) -> AppResult<()> {
        // Validate that all podcasts belong to the user
        for &podcast_id in std::iter::once(&primary_podcast_id).chain(secondary_podcast_ids.iter()) {
            let exists = self.verify_podcast_belongs_to_user(podcast_id, user_id).await?;
            if !exists {
                return Err(crate::error::AppError::forbidden("One or more podcasts do not belong to this user"));
            }
        }

        // Prevent circular merges - check if any secondary podcasts are already primary podcasts with merges
        for &secondary_id in secondary_podcast_ids {
            let existing_merges = self.get_merged_podcast_ids(secondary_id).await?;
            if !existing_merges.is_empty() {
                return Err(crate::error::AppError::bad_request("Cannot merge a podcast that is already a primary podcast with merged podcasts"));
            }
        }

        match self {
            DatabasePool::Postgres(pool) => {
                let mut tx = pool.begin().await?;
                
                // Set DisplayPodcast=FALSE for secondary podcasts
                for &secondary_id in secondary_podcast_ids {
                    sqlx::query(r#"UPDATE "Podcasts" SET displaypodcast = FALSE WHERE podcastid = $1"#)
                        .bind(secondary_id)
                        .execute(&mut *tx)
                        .await?;
                }
                
                // Get current merged IDs for primary podcast
                let current_merged_ids = self.get_merged_podcast_ids(primary_podcast_id).await?;
                let mut all_merged_ids = current_merged_ids;
                all_merged_ids.extend_from_slice(secondary_podcast_ids);
                
                // Update primary podcast with merged IDs
                let merged_json = serde_json::to_string(&all_merged_ids)?;
                sqlx::query(r#"UPDATE "Podcasts" SET mergedpodcastids = $1 WHERE podcastid = $2"#)
                    .bind(merged_json)
                    .bind(primary_podcast_id)
                    .execute(&mut *tx)
                    .await?;
                
                tx.commit().await?;
            }
            DatabasePool::MySQL(pool) => {
                let mut tx = pool.begin().await?;
                
                // Set DisplayPodcast=0 for secondary podcasts
                for &secondary_id in secondary_podcast_ids {
                    sqlx::query("UPDATE Podcasts SET DisplayPodcast = 0 WHERE PodcastID = ?")
                        .bind(secondary_id)
                        .execute(&mut *tx)
                        .await?;
                }
                
                // Get current merged IDs for primary podcast
                let current_merged_ids = self.get_merged_podcast_ids(primary_podcast_id).await?;
                let mut all_merged_ids = current_merged_ids;
                all_merged_ids.extend_from_slice(secondary_podcast_ids);
                
                // Update primary podcast with merged IDs
                let merged_json = serde_json::to_string(&all_merged_ids)?;
                sqlx::query("UPDATE Podcasts SET MergedPodcastIDs = ? WHERE PodcastID = ?")
                    .bind(merged_json)
                    .bind(primary_podcast_id)
                    .execute(&mut *tx)
                    .await?;
                
                tx.commit().await?;
            }
        }
        Ok(())
    }

    // Unmerge a specific podcast from a primary podcast
    pub async fn unmerge_podcast(&self, primary_podcast_id: i32, target_podcast_id: i32, user_id: i32) -> AppResult<()> {
        // Validate ownership
        let primary_exists = self.verify_podcast_belongs_to_user(primary_podcast_id, user_id).await?;
        let target_exists = self.verify_podcast_belongs_to_user(target_podcast_id, user_id).await?;
        
        if !primary_exists || !target_exists {
            return Err(crate::error::AppError::forbidden("One or more podcasts do not belong to this user"));
        }

        match self {
            DatabasePool::Postgres(pool) => {
                let mut tx = pool.begin().await?;
                
                // Get current merged IDs and remove the target
                let mut merged_ids = self.get_merged_podcast_ids(primary_podcast_id).await?;
                merged_ids.retain(|&id| id != target_podcast_id);
                
                // Update primary podcast
                let merged_json = if merged_ids.is_empty() {
                    None
                } else {
                    Some(serde_json::to_string(&merged_ids)?)
                };
                
                sqlx::query(r#"UPDATE "Podcasts" SET mergedpodcastids = $1 WHERE podcastid = $2"#)
                    .bind(merged_json)
                    .bind(primary_podcast_id)
                    .execute(&mut *tx)
                    .await?;
                
                // Set DisplayPodcast=TRUE for the unmerged podcast
                sqlx::query(r#"UPDATE "Podcasts" SET displaypodcast = TRUE WHERE podcastid = $1"#)
                    .bind(target_podcast_id)
                    .execute(&mut *tx)
                    .await?;
                
                tx.commit().await?;
            }
            DatabasePool::MySQL(pool) => {
                let mut tx = pool.begin().await?;
                
                // Get current merged IDs and remove the target
                let mut merged_ids = self.get_merged_podcast_ids(primary_podcast_id).await?;
                merged_ids.retain(|&id| id != target_podcast_id);
                
                // Update primary podcast
                let merged_json = if merged_ids.is_empty() {
                    None
                } else {
                    Some(serde_json::to_string(&merged_ids)?)
                };
                
                sqlx::query("UPDATE Podcasts SET MergedPodcastIDs = ? WHERE PodcastID = ?")
                    .bind(merged_json)
                    .bind(primary_podcast_id)
                    .execute(&mut *tx)
                    .await?;
                
                // Set DisplayPodcast=1 for the unmerged podcast
                sqlx::query("UPDATE Podcasts SET DisplayPodcast = 1 WHERE PodcastID = ?")
                    .bind(target_podcast_id)
                    .execute(&mut *tx)
                    .await?;
                
                tx.commit().await?;
            }
        }
        Ok(())
    }

    // Get merged podcast IDs for a primary podcast
    pub async fn get_merged_podcast_ids(&self, podcast_id: i32) -> AppResult<Vec<i32>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT mergedpodcastids FROM "Podcasts" WHERE podcastid = $1"#)
                    .bind(podcast_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    if let Some(merged_json) = row.try_get::<Option<String>, _>("mergedpodcastids")? {
                        let merged_ids: Vec<i32> = serde_json::from_str(&merged_json).unwrap_or_default();
                        Ok(merged_ids)
                    } else {
                        Ok(Vec::new())
                    }
                } else {
                    Ok(Vec::new())
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT MergedPodcastIDs FROM Podcasts WHERE PodcastID = ?")
                    .bind(podcast_id)
                    .fetch_optional(pool)
                    .await?;

                if let Some(row) = row {
                    if let Some(merged_json) = row.try_get::<Option<String>, _>("MergedPodcastIDs")? {
                        let merged_ids: Vec<i32> = serde_json::from_str(&merged_json).unwrap_or_default();
                        Ok(merged_ids)
                    } else {
                        Ok(Vec::new())
                    }
                } else {
                    Ok(Vec::new())
                }
            }
        }
    }

    // Helper function to verify a podcast belongs to a user
    pub async fn verify_podcast_belongs_to_user(&self, podcast_id: i32, user_id: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let count = sqlx::query(r#"SELECT COUNT(*) as count FROM "Podcasts" WHERE podcastid = $1 AND userid = $2"#)
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                
                Ok(count.try_get::<i64, _>("count")? > 0)
            }
            DatabasePool::MySQL(pool) => {
                let count = sqlx::query("SELECT COUNT(*) as count FROM Podcasts WHERE PodcastID = ? AND UserID = ?")
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                
                Ok(count.try_get::<i64, _>("count")? > 0)
            }
        }
    }

    // Get time info for user - matches Python get_time_info function
    pub async fn get_time_info(&self, user_id: i32) -> AppResult<crate::models::TimeInfoResponse> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(
                    r#"SELECT COALESCE(timezone, 'UTC') as timezone, 
                              COALESCE(timeformat, 12) as hour_pref,
                              dateformat as date_format
                       FROM "Users" WHERE userid = $1"#
                )
                .bind(user_id)
                .fetch_one(pool)
                .await?;
                
                Ok(crate::models::TimeInfoResponse {
                    timezone: row.try_get("timezone")?,
                    hour_pref: row.try_get("hour_pref")?,
                    date_format: row.try_get("date_format").ok(),
                })
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query(
                    "SELECT COALESCE(Timezone, 'UTC') as timezone, 
                            COALESCE(TimeFormat, 12) as hour_pref,
                            DateFormat as date_format
                     FROM Users WHERE UserID = ?"
                )
                .bind(user_id)
                .fetch_one(pool)
                .await?;
                
                Ok(crate::models::TimeInfoResponse {
                    timezone: row.try_get("timezone")?,
                    hour_pref: row.try_get("hour_pref")?,
                    date_format: row.try_get("date_format").ok(),
                })
            }
        }
    }

    // Check if podcast exists - matches Python check_podcast function
    pub async fn check_podcast(&self, user_id: i32, podcast_name: &str, podcast_url: &str) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(
                    r#"SELECT podcastid FROM "Podcasts" 
                       WHERE userid = $1 AND podcastname = $2 AND feedurl = $3"#
                )
                .bind(user_id)
                .bind(podcast_name)
                .bind(podcast_url)
                .fetch_optional(pool)
                .await?;
                
                Ok(row.is_some())
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query(
                    "SELECT PodcastID FROM Podcasts 
                     WHERE UserID = ? AND PodcastName = ? AND FeedURL = ?"
                )
                .bind(user_id)
                .bind(podcast_name)
                .bind(podcast_url)
                .fetch_optional(pool)
                .await?;
                
                Ok(row.is_some())
            }
        }
    }

    // Check if episode exists in database - matches Python check_episode_exists function
    pub async fn check_episode_exists(&self, user_id: i32, episode_title: &str, episode_url: &str) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(
                    r#"SELECT EXISTS(
                        SELECT 1 FROM "Episodes"
                        JOIN "Podcasts" ON "Episodes".podcastid = "Podcasts".podcastid
                        WHERE "Podcasts".userid = $1 
                          AND "Episodes".episodetitle = $2 
                          AND "Episodes".episodeurl = $3
                    ) as episode_exists"#
                )
                .bind(user_id)
                .bind(episode_title)
                .bind(episode_url)
                .fetch_one(pool)
                .await?;
                
                Ok(row.try_get("episode_exists")?)
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query(
                    "SELECT EXISTS(
                        SELECT 1 FROM Episodes
                        JOIN Podcasts ON Episodes.PodcastID = Podcasts.PodcastID
                        WHERE Podcasts.UserID = ? 
                          AND Episodes.EpisodeTitle = ? 
                          AND Episodes.EpisodeURL = ?
                    ) as episode_exists"
                )
                .bind(user_id)
                .bind(episode_title)
                .bind(episode_url)
                .fetch_one(pool)
                .await?;
                
                // MySQL returns integer (0 or 1) for EXISTS
                let exists_int: i32 = row.try_get("episode_exists")?;
                Ok(exists_int == 1)
            }
        }
    }

    // Queue episode - matches Python queue_pod function
    pub async fn queue_episode(&self, episode_id: i32, user_id: i32, is_youtube: bool) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                // First check if already queued
                let existing = sqlx::query(
                    r#"SELECT queueid FROM "EpisodeQueue" 
                       WHERE episodeid = $1 AND userid = $2 AND is_youtube = $3"#
                )
                .bind(episode_id)
                .bind(user_id) 
                .bind(is_youtube)
                .fetch_optional(pool)
                .await?;

                if existing.is_some() {
                    return Ok(()); // Already queued, don't duplicate
                }

                // Get max queue position for user
                let max_pos_row = sqlx::query(
                    r#"SELECT COALESCE(MAX(queueposition), 0) as max_pos FROM "EpisodeQueue" WHERE userid = $1"#
                )
                .bind(user_id)
                .fetch_one(pool)
                .await?;
                
                let max_pos: i32 = max_pos_row.try_get("max_pos")?;
                let new_position = max_pos + 1;

                // Insert new queued episode
                sqlx::query(
                    r#"INSERT INTO "EpisodeQueue" (episodeid, userid, queueposition, is_youtube) 
                       VALUES ($1, $2, $3, $4)"#
                )
                .bind(episode_id)
                .bind(user_id)
                .bind(new_position)
                .bind(is_youtube)
                .execute(pool)
                .await?;

                Ok(())
            }
            DatabasePool::MySQL(pool) => {
                // First check if already queued
                let existing = sqlx::query(
                    "SELECT QueueID FROM EpisodeQueue 
                     WHERE EpisodeID = ? AND UserID = ? AND is_youtube = ?"
                )
                .bind(episode_id)
                .bind(user_id)
                .bind(is_youtube)
                .fetch_optional(pool)
                .await?;

                if existing.is_some() {
                    return Ok(()); // Already queued, don't duplicate
                }

                // Get max queue position for user
                let max_pos_row = sqlx::query(
                    "SELECT COALESCE(MAX(QueuePosition), 0) as max_pos FROM EpisodeQueue WHERE UserID = ?"
                )
                .bind(user_id)
                .fetch_one(pool)
                .await?;
                
                let max_pos: i32 = max_pos_row.try_get("max_pos")?;
                let new_position = max_pos + 1;

                // Insert new queued episode
                sqlx::query(
                    "INSERT INTO EpisodeQueue (EpisodeID, UserID, QueuePosition, is_youtube) 
                     VALUES (?, ?, ?, ?)"
                )
                .bind(episode_id)
                .bind(user_id)
                .bind(new_position)
                .bind(is_youtube)
                .execute(pool)
                .await?;

                Ok(())
            }
        }
    }

    // Remove queued episode - matches Python remove_queued_pod function
    pub async fn remove_queued_episode(&self, episode_id: i32, user_id: i32, is_youtube: bool) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Get the queue position of the episode to be removed
                let position_row = sqlx::query(
                    r#"SELECT queueposition FROM "EpisodeQueue" 
                       WHERE episodeid = $1 AND userid = $2 AND is_youtube = $3"#
                )
                .bind(episode_id)
                .bind(user_id)
                .bind(is_youtube)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = position_row {
                    let removed_position: i32 = row.try_get("queueposition")?;

                    // Delete the episode from queue
                    sqlx::query(
                        r#"DELETE FROM "EpisodeQueue" 
                           WHERE episodeid = $1 AND userid = $2 AND is_youtube = $3"#
                    )
                    .bind(episode_id)
                    .bind(user_id)
                    .bind(is_youtube)
                    .execute(pool)
                    .await?;

                    // Update positions of all episodes that were after the removed one
                    sqlx::query(
                        r#"UPDATE "EpisodeQueue" SET queueposition = queueposition - 1 
                           WHERE userid = $1 AND queueposition > $2"#
                    )
                    .bind(user_id)
                    .bind(removed_position)
                    .execute(pool)
                    .await?;
                }

                Ok(())
            }
            DatabasePool::MySQL(pool) => {
                // Get the queue position of the episode to be removed
                let position_row = sqlx::query(
                    "SELECT QueuePosition FROM EpisodeQueue 
                     WHERE EpisodeID = ? AND UserID = ? AND is_youtube = ?"
                )
                .bind(episode_id)
                .bind(user_id)
                .bind(is_youtube)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = position_row {
                    let removed_position: i32 = row.try_get("QueuePosition")?;

                    // Delete the episode from queue
                    sqlx::query(
                        "DELETE FROM EpisodeQueue 
                         WHERE EpisodeID = ? AND UserID = ? AND is_youtube = ?"
                    )
                    .bind(episode_id)
                    .bind(user_id)
                    .bind(is_youtube)
                    .execute(pool)
                    .await?;

                    // Update positions of all episodes that were after the removed one
                    sqlx::query(
                        "UPDATE EpisodeQueue SET QueuePosition = QueuePosition - 1 
                         WHERE UserID = ? AND QueuePosition > ?"
                    )
                    .bind(user_id)
                    .bind(removed_position)
                    .execute(pool)
                    .await?;
                }

                Ok(())
            }
        }
    }

    // Get queued episodes - matches Python get_queued_episodes function
    pub async fn get_queued_episodes(&self, user_id: i32) -> AppResult<Vec<crate::models::QueuedEpisode>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT * FROM (
                        SELECT
                            "Episodes".episodetitle as episodetitle,
                            "Podcasts".podcastname as podcastname,
                            "Episodes".episodepubdate as episodepubdate,
                            "Episodes".episodedescription as episodedescription,
                            CASE 
                                WHEN "Podcasts".usepodcastcoverscustomized = TRUE AND "Podcasts".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                WHEN "Users".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                ELSE "Episodes".episodeartwork
                            END as episodeartwork,
                            "Episodes".episodeurl as episodeurl,
                            "EpisodeQueue".queueposition as queueposition,
                            "Episodes".episodeduration as episodeduration,
                            "EpisodeQueue".queuedate as queuedate,
                            "UserEpisodeHistory".listenduration as listenduration,
                            "Episodes".episodeid as episodeid,
                            "Episodes".completed as completed,
                            CASE WHEN "SavedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            TRUE as queued,
                            CASE WHEN "DownloadedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            FALSE as is_youtube
                        FROM "EpisodeQueue"
                        INNER JOIN "Episodes" ON "EpisodeQueue".episodeid = "Episodes".episodeid
                        INNER JOIN "Podcasts" ON "Episodes".podcastid = "Podcasts".podcastid
                        LEFT JOIN "Users" ON "Podcasts".userid = "Users".userid
                        LEFT JOIN "UserEpisodeHistory" ON
                            "EpisodeQueue".episodeid = "UserEpisodeHistory".episodeid
                            AND "EpisodeQueue".userid = "UserEpisodeHistory".userid
                        LEFT JOIN "SavedEpisodes" ON
                            "EpisodeQueue".episodeid = "SavedEpisodes".episodeid
                            AND "EpisodeQueue".userid = "SavedEpisodes".userid
                        LEFT JOIN "DownloadedEpisodes" ON
                            "EpisodeQueue".episodeid = "DownloadedEpisodes".episodeid
                            AND "EpisodeQueue".userid = "DownloadedEpisodes".userid
                        WHERE "EpisodeQueue".userid = $1 AND "EpisodeQueue".is_youtube = FALSE

                        UNION ALL

                        SELECT
                            "YouTubeVideos".videotitle as episodetitle,
                            "Podcasts".podcastname as podcastname,
                            "YouTubeVideos".publishedat as episodepubdate,
                            "YouTubeVideos".videodescription as episodedescription,
                            CASE 
                                WHEN "Podcasts".usepodcastcoverscustomized = TRUE AND "Podcasts".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                WHEN "Users".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                ELSE "YouTubeVideos".thumbnailurl
                            END as episodeartwork,
                            "YouTubeVideos".videourl as episodeurl,
                            "EpisodeQueue".queueposition as queueposition,
                            "YouTubeVideos".duration as episodeduration,
                            "EpisodeQueue".queuedate as queuedate,
                            "YouTubeVideos".listenposition as listenduration,
                            "YouTubeVideos".videoid as episodeid,
                            "YouTubeVideos".completed as completed,
                            CASE WHEN "SavedVideos".videoid IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            TRUE as queued,
                            CASE WHEN "DownloadedVideos".videoid IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            TRUE as is_youtube
                        FROM "EpisodeQueue"
                        INNER JOIN "YouTubeVideos" ON "EpisodeQueue".episodeid = "YouTubeVideos".videoid
                        INNER JOIN "Podcasts" ON "YouTubeVideos".podcastid = "Podcasts".podcastid
                        LEFT JOIN "Users" ON "Podcasts".userid = "Users".userid
                        LEFT JOIN "SavedVideos" ON
                            "EpisodeQueue".episodeid = "SavedVideos".videoid
                            AND "EpisodeQueue".userid = "SavedVideos".userid
                        LEFT JOIN "DownloadedVideos" ON
                            "EpisodeQueue".episodeid = "DownloadedVideos".videoid
                            AND "EpisodeQueue".userid = "DownloadedVideos".userid
                        WHERE "EpisodeQueue".userid = $2 AND "EpisodeQueue".is_youtube = TRUE
                    ) combined
                    ORDER BY queueposition ASC"#
                )
                .bind(user_id)
                .bind(user_id)
                .fetch_all(pool)
                .await?;

                let mut episodes = Vec::new();
                for row in rows {
                    episodes.push(crate::models::QueuedEpisode {
                        episodetitle: row.try_get("episodetitle")?,
                        podcastname: row.try_get("podcastname")?,
                        episodepubdate: {
                            let naive = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                            naive.format("%Y-%m-%dT%H:%M:%S").to_string()
                        },
                        episodedescription: row.try_get("episodedescription")?,
                        episodeartwork: row.try_get("episodeartwork")?,
                        episodeurl: row.try_get("episodeurl")?,
                        queueposition: row.try_get("queueposition").ok(),
                        episodeduration: row.try_get("episodeduration")?,
                        queuedate: {
                        let naive = row.try_get::<chrono::NaiveDateTime, _>("queuedate")?;
                        naive.format("%Y-%m-%dT%H:%M:%S").to_string()
                    },
                        listenduration: row.try_get("listenduration").ok(),
                        episodeid: row.try_get("episodeid")?,
                        completed: row.try_get("completed")?,
                        saved: row.try_get("saved")?,
                        queued: row.try_get("queued")?,
                        downloaded: row.try_get("downloaded")?,
                        is_youtube: row.try_get("is_youtube")?,
                    });
                }
                Ok(episodes)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(
                    "SELECT * FROM (
                        SELECT
                            Episodes.EpisodeTitle as episodetitle,
                            Podcasts.PodcastName as podcastname,
                            Episodes.EpisodePubDate as episodepubdate,
                            Episodes.EpisodeDescription as episodedescription,
                            CASE 
                                WHEN Podcasts.UsePodcastCoversCustomized = TRUE AND Podcasts.UsePodcastCovers = TRUE THEN Podcasts.ArtworkURL
                                WHEN Users.UsePodcastCovers = TRUE THEN Podcasts.ArtworkURL
                                ELSE Episodes.EpisodeArtwork
                            END as episodeartwork,
                            Episodes.EpisodeURL as episodeurl,
                            EpisodeQueue.QueuePosition as queueposition,
                            Episodes.EpisodeDuration as episodeduration,
                            EpisodeQueue.QueueDate as queuedate,
                            UserEpisodeHistory.ListenDuration as listenduration,
                            Episodes.EpisodeID as episodeid,
                            Episodes.Completed as completed,
                            CASE WHEN SavedEpisodes.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            TRUE as queued,
                            CASE WHEN DownloadedEpisodes.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            FALSE as is_youtube
                        FROM EpisodeQueue
                        INNER JOIN Episodes ON EpisodeQueue.EpisodeID = Episodes.EpisodeID
                        INNER JOIN Podcasts ON Episodes.PodcastID = Podcasts.PodcastID
                        LEFT JOIN Users ON Podcasts.UserID = Users.UserID
                        LEFT JOIN UserEpisodeHistory ON
                            EpisodeQueue.EpisodeID = UserEpisodeHistory.EpisodeID
                            AND EpisodeQueue.UserID = UserEpisodeHistory.UserID
                        LEFT JOIN SavedEpisodes ON
                            EpisodeQueue.EpisodeID = SavedEpisodes.EpisodeID
                            AND EpisodeQueue.UserID = SavedEpisodes.UserID
                        LEFT JOIN DownloadedEpisodes ON
                            EpisodeQueue.EpisodeID = DownloadedEpisodes.EpisodeID
                            AND EpisodeQueue.UserID = DownloadedEpisodes.UserID
                        WHERE EpisodeQueue.UserID = ? AND EpisodeQueue.is_youtube = FALSE

                        UNION ALL

                        SELECT
                            YouTubeVideos.VideoTitle as episodetitle,
                            Podcasts.PodcastName as podcastname,
                            YouTubeVideos.PublishedAt as episodepubdate,
                            YouTubeVideos.VideoDescription as episodedescription,
                            CASE 
                                WHEN Podcasts.UsePodcastCoversCustomized = TRUE AND Podcasts.UsePodcastCovers = TRUE THEN Podcasts.ArtworkURL
                                WHEN Users.UsePodcastCovers = TRUE THEN Podcasts.ArtworkURL
                                ELSE YouTubeVideos.ThumbnailURL
                            END as episodeartwork,
                            YouTubeVideos.VideoURL as episodeurl,
                            EpisodeQueue.QueuePosition as queueposition,
                            YouTubeVideos.Duration as episodeduration,
                            EpisodeQueue.QueueDate as queuedate,
                            YouTubeVideos.ListenPosition as listenduration,
                            YouTubeVideos.VideoID as episodeid,
                            YouTubeVideos.Completed as completed,
                            CASE WHEN SavedVideos.VideoID IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            TRUE as queued,
                            CASE WHEN DownloadedVideos.VideoID IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            TRUE as is_youtube
                        FROM EpisodeQueue
                        INNER JOIN YouTubeVideos ON EpisodeQueue.EpisodeID = YouTubeVideos.VideoID
                        INNER JOIN Podcasts ON YouTubeVideos.PodcastID = Podcasts.PodcastID
                        LEFT JOIN Users ON Podcasts.UserID = Users.UserID
                        LEFT JOIN SavedVideos ON
                            EpisodeQueue.EpisodeID = SavedVideos.VideoID
                            AND EpisodeQueue.UserID = SavedVideos.UserID
                        LEFT JOIN DownloadedVideos ON
                            EpisodeQueue.EpisodeID = DownloadedVideos.VideoID
                            AND EpisodeQueue.UserID = DownloadedVideos.UserID
                        WHERE EpisodeQueue.UserID = ? AND EpisodeQueue.is_youtube = TRUE
                    ) combined
                    ORDER BY queueposition ASC"
                )
                .bind(user_id)
                .bind(user_id)
                .fetch_all(pool)
                .await?;

                let mut episodes = Vec::new();
                for row in rows {
                    episodes.push(crate::models::QueuedEpisode {
                        episodetitle: row.try_get("episodetitle")?,
                        podcastname: row.try_get("podcastname")?,
                        episodepubdate: {
                            let dt = row.try_get::<sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>, _>("episodepubdate")?;
                            dt.format("%Y-%m-%dT%H:%M:%S").to_string()
                        },
                        episodedescription: row.try_get("episodedescription")?,
                        episodeartwork: row.try_get("episodeartwork")?,
                        episodeurl: row.try_get("episodeurl")?,
                        queueposition: row.try_get("queueposition").ok(),
                        episodeduration: row.try_get("episodeduration")?,
                        queuedate: {
                        let dt = row.try_get::<sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>, _>("queuedate")?;
                        dt.format("%Y-%m-%dT%H:%M:%S").to_string()
                    },
                        listenduration: row.try_get("listenduration").ok(),
                        episodeid: row.try_get("episodeid")?,
                        completed: row.try_get("completed")?,
                        saved: row.try_get("saved")?,
                        queued: row.try_get("queued")?,
                        downloaded: row.try_get("downloaded")?,
                        is_youtube: row.try_get("is_youtube")?,
                    });
                }
                Ok(episodes)
            }
        }
    }

    // Reorder queue - matches Python reorder_queued_episodes function
    pub async fn reorder_queue(&self, user_id: i32, episode_ids: Vec<i32>) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                let mut tx = pool.begin().await?;
                
                for (index, episode_id) in episode_ids.iter().enumerate() {
                    let new_position = (index + 1) as i32;
                    sqlx::query(
                        r#"UPDATE "EpisodeQueue" SET queueposition = $1 
                           WHERE episodeid = $2 AND userid = $3"#
                    )
                    .bind(new_position)
                    .bind(episode_id)
                    .bind(user_id)
                    .execute(&mut *tx)
                    .await?;
                }
                
                tx.commit().await?;
                Ok(())
            }
            DatabasePool::MySQL(pool) => {
                let mut tx = pool.begin().await?;
                
                for (index, episode_id) in episode_ids.iter().enumerate() {
                    let new_position = (index + 1) as i32;
                    sqlx::query(
                        "UPDATE EpisodeQueue SET QueuePosition = ? 
                         WHERE EpisodeID = ? AND UserID = ?"
                    )
                    .bind(new_position)
                    .bind(episode_id)
                    .bind(user_id)
                    .execute(&mut *tx)
                    .await?;
                }
                
                tx.commit().await?;
                Ok(())
            }
        }
    }

    // Save episode - matches Python save_episode function
    pub async fn save_episode(&self, episode_id: i32, user_id: i32, is_youtube: bool) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                if is_youtube {
                    // Check if already saved
                    let existing = sqlx::query(
                        r#"SELECT "SaveID" FROM "SavedVideos" WHERE "VideoID" = $1 AND "UserID" = $2"#
                    )
                    .bind(episode_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                    if existing.is_none() {
                        sqlx::query(
                            r#"INSERT INTO "SavedVideos" ("VideoID", "UserID") VALUES ($1, $2)"#
                        )
                        .bind(episode_id)
                        .bind(user_id)
                        .execute(pool)
                        .await?;

                        // Update UserStats table - increment EpisodesSaved count
                        sqlx::query(r#"UPDATE "UserStats" SET episodessaved = episodessaved + 1 WHERE userid = $1"#)
                            .bind(user_id)
                            .execute(pool)
                            .await?;
                    }
                } else {
                    // Check if already saved
                    let existing = sqlx::query(
                        r#"SELECT saveid FROM "SavedEpisodes" WHERE episodeid = $1 AND userid = $2"#
                    )
                    .bind(episode_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                    if existing.is_none() {
                        sqlx::query(
                            r#"INSERT INTO "SavedEpisodes" (episodeid, userid) VALUES ($1, $2)"#
                        )
                        .bind(episode_id)
                        .bind(user_id)
                        .execute(pool)
                        .await?;

                        // Update UserStats table - increment EpisodesSaved count
                        sqlx::query(r#"UPDATE "UserStats" SET episodessaved = episodessaved + 1 WHERE userid = $1"#)
                            .bind(user_id)
                            .execute(pool)
                            .await?;
                    }
                }
                Ok(())
            }
            DatabasePool::MySQL(pool) => {
                if is_youtube {
                    // Check if already saved
                    let existing = sqlx::query(
                        "SELECT SaveID FROM SavedVideos WHERE VideoID = ? AND UserID = ?"
                    )
                    .bind(episode_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                    if existing.is_none() {
                        sqlx::query(
                            "INSERT INTO SavedVideos (VideoID, UserID) VALUES (?, ?)"
                        )
                        .bind(episode_id)
                        .bind(user_id)
                        .execute(pool)
                        .await?;

                        // Update UserStats table - increment EpisodesSaved count
                        sqlx::query("UPDATE UserStats SET EpisodesSaved = EpisodesSaved + 1 WHERE UserID = ?")
                            .bind(user_id)
                            .execute(pool)
                            .await?;
                    }
                } else {
                    // Check if already saved
                    let existing = sqlx::query(
                        "SELECT SaveID FROM SavedEpisodes WHERE EpisodeID = ? AND UserID = ?"
                    )
                    .bind(episode_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                    if existing.is_none() {
                        sqlx::query(
                            "INSERT INTO SavedEpisodes (EpisodeID, UserID) VALUES (?, ?)"
                        )
                        .bind(episode_id)
                        .bind(user_id)
                        .execute(pool)
                        .await?;

                        // Update UserStats table - increment EpisodesSaved count
                        sqlx::query("UPDATE UserStats SET EpisodesSaved = EpisodesSaved + 1 WHERE UserID = ?")
                            .bind(user_id)
                            .execute(pool)
                            .await?;
                    }
                }
                Ok(())
            }
        }
    }

    // Remove saved episode - matches Python remove_saved_episode function
    pub async fn remove_saved_episode(&self, episode_id: i32, user_id: i32, is_youtube: bool) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                if is_youtube {
                    let result = sqlx::query(
                        r#"DELETE FROM "SavedVideos" WHERE "VideoID" = $1 AND "UserID" = $2"#
                    )
                    .bind(episode_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;

                    // Only update UserStats if a row was actually deleted
                    if result.rows_affected() > 0 {
                        sqlx::query(r#"UPDATE "UserStats" SET episodessaved = episodessaved - 1 WHERE userid = $1"#)
                            .bind(user_id)
                            .execute(pool)
                            .await?;
                    }
                } else {
                    let result = sqlx::query(
                        r#"DELETE FROM "SavedEpisodes" WHERE episodeid = $1 AND userid = $2"#
                    )
                    .bind(episode_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;

                    // Only update UserStats if a row was actually deleted
                    if result.rows_affected() > 0 {
                        sqlx::query(r#"UPDATE "UserStats" SET episodessaved = episodessaved - 1 WHERE userid = $1"#)
                            .bind(user_id)
                            .execute(pool)
                            .await?;
                    }
                }
                Ok(())
            }
            DatabasePool::MySQL(pool) => {
                if is_youtube {
                    let result = sqlx::query(
                        "DELETE FROM SavedVideos WHERE VideoID = ? AND UserID = ?"
                    )
                    .bind(episode_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;

                    // Only update UserStats if a row was actually deleted
                    if result.rows_affected() > 0 {
                        sqlx::query("UPDATE UserStats SET EpisodesSaved = EpisodesSaved - 1 WHERE UserID = ?")
                            .bind(user_id)
                            .execute(pool)
                            .await?;
                    }
                } else {
                    let result = sqlx::query(
                        "DELETE FROM SavedEpisodes WHERE EpisodeID = ? AND UserID = ?"
                    )
                    .bind(episode_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;

                    // Only update UserStats if a row was actually deleted
                    if result.rows_affected() > 0 {
                        sqlx::query("UPDATE UserStats SET EpisodesSaved = EpisodesSaved - 1 WHERE UserID = ?")
                            .bind(user_id)
                            .execute(pool)
                            .await?;
                    }
                }
                Ok(())
            }
        }
    }

    pub async fn update_episode_duration(&self, episode_id: i32, new_duration: i32, is_youtube: bool) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                if is_youtube {
                    sqlx::query(r#"UPDATE "YouTubeVideos" SET duration = $1 WHERE videoid = $2"#)
                        .bind(new_duration)
                        .bind(episode_id)
                        .execute(pool)
                        .await?;
                } else {
                    sqlx::query(r#"UPDATE "Episodes" SET episodeduration = $1 WHERE episodeid = $2"#)
                        .bind(new_duration)
                        .bind(episode_id)
                        .execute(pool)
                        .await?;
                }
            }
            DatabasePool::MySQL(pool) => {
                if is_youtube {
                    sqlx::query(r#"UPDATE YouTubeVideos SET duration = ? WHERE videoid = ?"#)
                        .bind(new_duration)
                        .bind(episode_id)
                        .execute(pool)
                        .await?;
                } else {
                    sqlx::query(r#"UPDATE Episodes SET episodeduration = ? WHERE episodeid = ?"#)
                        .bind(new_duration)
                        .bind(episode_id)
                        .execute(pool)
                        .await?;
                }
            }
        }
        Ok(())
    }

    // Mark episode as completed - matches Python mark_episode_completed function
    pub async fn mark_episode_completed(&self, episode_id: i32, user_id: i32, is_youtube: bool) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                if is_youtube {
                    // Get YouTube video duration
                    let duration_row = sqlx::query(
                        r#"SELECT duration FROM "YouTubeVideos" WHERE videoid = $1"#
                    )
                    .bind(episode_id)
                    .fetch_optional(pool)
                    .await?;

                    if let Some(row) = duration_row {
                        let duration: Option<i32> = row.try_get("duration").ok();
                        
                        if let Some(duration) = duration {
                            // Update completion status
                            sqlx::query(
                                r#"UPDATE "YouTubeVideos" SET completed = TRUE WHERE videoid = $1"#
                            )
                            .bind(episode_id)
                            .execute(pool)
                            .await?;

                            // Update history
                            sqlx::query(
                                r#"INSERT INTO "UserVideoHistory" (userid, videoid, listendate, listenduration)
                                   VALUES ($1, $2, NOW(), $3)
                                   ON CONFLICT (userid, videoid)
                                   DO UPDATE SET listenduration = $4, listendate = NOW()"#
                            )
                            .bind(user_id)
                            .bind(episode_id)
                            .bind(duration)
                            .bind(duration)
                            .execute(pool)
                            .await?;
                        }
                    }
                } else {
                    // Get episode duration
                    let duration_row = sqlx::query(
                        r#"SELECT episodeduration FROM "Episodes" WHERE episodeid = $1"#
                    )
                    .bind(episode_id)
                    .fetch_optional(pool)
                    .await?;

                    if let Some(row) = duration_row {
                        let duration: Option<i32> = row.try_get("episodeduration").ok();
                        
                        if let Some(duration) = duration {
                            // Update completion status
                            sqlx::query(
                                r#"UPDATE "Episodes" SET completed = TRUE WHERE episodeid = $1"#
                            )
                            .bind(episode_id)
                            .execute(pool)
                            .await?;

                            // Update history
                            sqlx::query(
                                r#"INSERT INTO "UserEpisodeHistory" (userid, episodeid, listendate, listenduration)
                                   VALUES ($1, $2, NOW(), $3)
                                   ON CONFLICT (userid, episodeid)
                                   DO UPDATE SET listenduration = $4, listendate = NOW()"#
                            )
                            .bind(user_id)
                            .bind(episode_id)
                            .bind(duration)
                            .bind(duration)
                            .execute(pool)
                            .await?;
                        }
                    }
                }
                Ok(())
            }
            DatabasePool::MySQL(pool) => {
                if is_youtube {
                    // Get YouTube video duration
                    let duration_row = sqlx::query(
                        "SELECT Duration FROM YouTubeVideos WHERE VideoID = ?"
                    )
                    .bind(episode_id)
                    .fetch_optional(pool)
                    .await?;

                    if let Some(row) = duration_row {
                        let duration: Option<i32> = row.try_get("Duration").ok();
                        
                        if let Some(duration) = duration {
                            // Update completion status
                            sqlx::query(
                                "UPDATE YouTubeVideos SET Completed = 1 WHERE VideoID = ?"
                            )
                            .bind(episode_id)
                            .execute(pool)
                            .await?;

                            // Update history
                            sqlx::query(
                                "INSERT INTO UserVideoHistory (UserID, VideoID, ListenDate, ListenDuration)
                                 VALUES (?, ?, NOW(), ?)
                                 ON DUPLICATE KEY UPDATE
                                     ListenDuration = ?,
                                     ListenDate = NOW()"
                            )
                            .bind(user_id)
                            .bind(episode_id)
                            .bind(duration)
                            .bind(duration)
                            .execute(pool)
                            .await?;
                        }
                    }
                } else {
                    // Get episode duration
                    let duration_row = sqlx::query(
                        "SELECT EpisodeDuration FROM Episodes WHERE EpisodeID = ?"
                    )
                    .bind(episode_id)
                    .fetch_optional(pool)
                    .await?;

                    if let Some(row) = duration_row {
                        let duration: Option<i32> = row.try_get("EpisodeDuration").ok();
                        
                        if let Some(duration) = duration {
                            // Update completion status
                            sqlx::query(
                                "UPDATE Episodes SET Completed = 1 WHERE EpisodeID = ?"
                            )
                            .bind(episode_id)
                            .execute(pool)
                            .await?;

                            // Update history
                            sqlx::query(
                                "INSERT INTO UserEpisodeHistory (UserID, EpisodeID, ListenDate, ListenDuration)
                                 VALUES (?, ?, NOW(), ?)
                                 ON DUPLICATE KEY UPDATE
                                     ListenDuration = ?,
                                     ListenDate = NOW()"
                            )
                            .bind(user_id)
                            .bind(episode_id)
                            .bind(duration)
                            .bind(duration)
                            .execute(pool)
                            .await?;
                        }
                    }
                }
                Ok(())
            }
        }
    }

    // Increment played count - matches Python increment_played function
    pub async fn increment_played(&self, user_id: i32) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(
                    r#"UPDATE "UserStats" SET podcastsplayed = podcastsplayed + 1 WHERE userid = $1"#
                )
                .bind(user_id)
                .execute(pool)
                .await?;
                Ok(())
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query(
                    "UPDATE UserStats SET PodcastsPlayed = PodcastsPlayed + 1 WHERE UserID = ?"
                )
                .bind(user_id)
                .execute(pool)
                .await?;
                Ok(())
            }
        }
    }

    // Get podcast ID from episode ID - matches Python get_podcast_id_from_episode function
    pub async fn get_podcast_id_from_episode(&self, episode_id: i32, user_id: i32, is_youtube: bool) -> AppResult<Option<i32>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let query = if is_youtube {
                    r#"SELECT "YouTubeVideos".podcastid
                       FROM "YouTubeVideos"
                       INNER JOIN "Podcasts" ON "YouTubeVideos".podcastid = "Podcasts".podcastid
                       WHERE "YouTubeVideos".videoid = $1 AND "Podcasts".userid = $2"#
                } else {
                    r#"SELECT "Episodes".podcastid
                       FROM "Episodes" 
                       INNER JOIN "Podcasts" ON "Episodes".podcastid = "Podcasts".podcastid
                       WHERE "Episodes".episodeid = $1 AND "Podcasts".userid = $2"#
                };

                // First try with provided user_id
                let row = sqlx::query(query)
                    .bind(episode_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                if let Some(row) = row {
                    return Ok(Some(row.try_get("podcastid")?));
                }

                // If not found, try with system user (1)
                let row = sqlx::query(query)
                    .bind(episode_id)
                    .bind(1)
                    .fetch_optional(pool)
                    .await?;

                if let Some(row) = row {
                    Ok(Some(row.try_get("podcastid")?))
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let query = if is_youtube {
                    "SELECT YouTubeVideos.PodcastID
                     FROM YouTubeVideos
                     INNER JOIN Podcasts ON YouTubeVideos.PodcastID = Podcasts.PodcastID
                     WHERE YouTubeVideos.VideoID = ? AND Podcasts.UserID = ?"
                } else {
                    "SELECT Episodes.PodcastID
                     FROM Episodes
                     INNER JOIN Podcasts ON Episodes.PodcastID = Podcasts.PodcastID
                     WHERE Episodes.EpisodeID = ? AND Podcasts.UserID = ?"
                };

                // First try with provided user_id
                let row = sqlx::query(query)
                    .bind(episode_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                if let Some(row) = row {
                    return Ok(Some(row.try_get("PodcastID")?));
                }

                // If not found, try with system user (1)
                let row = sqlx::query(query)
                    .bind(episode_id)
                    .bind(1)
                    .fetch_optional(pool)
                    .await?;

                if let Some(row) = row {
                    Ok(Some(row.try_get("PodcastID")?))
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Get episode ID from episode URL - public version of find_episode_by_url
    pub async fn get_episode_id_from_url(&self, episode_url: &str, user_id: i32) -> AppResult<Option<i32>> {
        self.find_episode_by_url(user_id, episode_url).await
    }

    // Get PinePods version - matches Python get_pinepods_version function
    pub async fn get_pinepods_version(&self) -> AppResult<String> {
        match std::fs::read_to_string("/pinepods/current_version") {
            Ok(version) => {
                let version = version.trim();
                if version.is_empty() {
                    Ok("dev_mode".to_string())
                } else {
                    Ok(version.to_string())
                }
            }
            Err(_) => Ok("Version file not found.".to_string()),
        }
    }

    // Get user stats - matches Python get_stats function
    pub async fn get_stats(&self, user_id: i32) -> AppResult<Option<serde_json::Value>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(
                    r#"SELECT usercreated, podcastsplayed, timelistened, podcastsadded 
                       FROM "UserStats" WHERE userid = $1"#
                )
                .bind(user_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    // Count saved episodes directly from SavedEpisodes table
                    let saved_count_row = sqlx::query(
                        r#"SELECT COUNT(*) as saved_count FROM "SavedEpisodes" WHERE userid = $1"#
                    )
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;

                    // Count downloaded episodes directly from DownloadedEpisodes table  
                    let downloaded_count_row = sqlx::query(
                        r#"SELECT COUNT(*) as downloaded_count FROM "DownloadedEpisodes" WHERE userid = $1"#
                    )
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;

                    let saved_count: i64 = saved_count_row.try_get("saved_count")?;
                    let downloaded_count: i64 = downloaded_count_row.try_get("downloaded_count")?;

                    let stats = serde_json::json!({
                        "UserCreated": row.try_get::<chrono::NaiveDateTime, _>("usercreated")?.format("%Y-%m-%dT%H:%M:%S%.f").to_string(),
                        "PodcastsPlayed": row.try_get::<i32, _>("podcastsplayed")?,
                        "TimeListened": row.try_get::<i32, _>("timelistened")?,
                        "PodcastsAdded": row.try_get::<i32, _>("podcastsadded")?,
                        "EpisodesSaved": saved_count as i32,
                        "EpisodesDownloaded": downloaded_count as i32,
                        "GpodderUrl": "http://localhost:8042",
                        "Pod_Sync_Type": "gpodder"
                    });

                    Ok(Some(stats))
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query(
                    "SELECT UserCreated, PodcastsPlayed, TimeListened, PodcastsAdded 
                     FROM UserStats WHERE UserID = ?"
                )
                .bind(user_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {

                    // Count saved episodes directly from SavedEpisodes table
                    let saved_count_row = sqlx::query(
                        "SELECT COUNT(*) as saved_count FROM SavedEpisodes WHERE UserID = ?"
                    )
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;

                    // Count downloaded episodes directly from DownloadedEpisodes table
                    let downloaded_count_row = sqlx::query(
                        "SELECT COUNT(*) as downloaded_count FROM DownloadedEpisodes WHERE UserID = ?"
                    )
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;

                    let saved_count: i64 = saved_count_row.try_get("saved_count")?;
                    let downloaded_count: i64 = downloaded_count_row.try_get("downloaded_count")?;

                    let stats = serde_json::json!({
                        "UserCreated": row.try_get::<sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>, _>("UserCreated")?.format("%Y-%m-%dT%H:%M:%S%.f").to_string(),
                        "PodcastsPlayed": row.try_get::<i32, _>("PodcastsPlayed")?,
                        "TimeListened": row.try_get::<i32, _>("TimeListened")?,
                        "PodcastsAdded": row.try_get::<i32, _>("PodcastsAdded")?,
                        "EpisodesSaved": saved_count as i32,
                        "EpisodesDownloaded": downloaded_count as i32,
                        "GpodderUrl": "http://localhost:8042",
                        "Pod_Sync_Type": "gpodder"
                    });

                    Ok(Some(stats))
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Search data - matches Python search_data function (simplified version)
    pub async fn search_data(&self, search_term: &str, user_id: i32) -> AppResult<Vec<serde_json::Value>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT
                        p.podcastid,
                        p.podcastname,
                        p.artworkurl,
                        p.author,
                        p.categories,
                        p.description,
                        p.episodecount,
                        p.feedurl,
                        p.websiteurl,
                        p.explicit,
                        p.userid,
                        COALESCE(p.isyoutubechannel, false) as is_youtube,
                        e.episodeid,
                        e.episodetitle,
                        e.episodedescription,
                        e.episodeurl,
                        CASE 
                            WHEN p.usepodcastcoverscustomized = TRUE AND p.usepodcastcovers = TRUE THEN p.artworkurl
                            WHEN u.usepodcastcovers = TRUE THEN p.artworkurl
                            ELSE e.episodeartwork
                        END as episodeartwork,
                        e.episodepubdate,
                        e.episodeduration,
                        COALESCE(h.listenduration, 0) as listenduration,
                        COALESCE(e.completed, false) as completed,
                        CASE WHEN se.episodeid IS NOT NULL THEN true ELSE false END as saved,
                        CASE WHEN eq.episodeid IS NOT NULL THEN true ELSE false END as queued,
                        CASE WHEN de.episodeid IS NOT NULL THEN true ELSE false END as downloaded
                    FROM "Podcasts" p
                    LEFT JOIN "Users" u ON p.userid = u.userid
                    LEFT JOIN "Episodes" e ON p.podcastid = e.podcastid
                    LEFT JOIN "UserEpisodeHistory" h ON e.episodeid = h.episodeid AND h.userid = $2
                    LEFT JOIN "SavedEpisodes" se ON e.episodeid = se.episodeid AND se.userid = $2
                    LEFT JOIN "EpisodeQueue" eq ON e.episodeid = eq.episodeid AND eq.userid = $2 AND eq.is_youtube = false
                    LEFT JOIN "DownloadedEpisodes" de ON e.episodeid = de.episodeid AND de.userid = $2
                    WHERE p.userid = $2 
                      AND (LOWER(p.podcastname) LIKE LOWER($1) 
                           OR LOWER(e.episodetitle) LIKE LOWER($1)
                           OR LOWER(e.episodedescription) LIKE LOWER($1))
                    ORDER BY p.podcastname, e.episodepubdate DESC"#
                )
                .bind(format!("%{}%", search_term))
                .bind(user_id)
                .fetch_all(pool)
                .await?;

                let mut results = Vec::new();
                for row in rows {
                    let pub_date = if let Ok(date) = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate") {
                        date.format("%Y-%m-%dT%H:%M:%S").to_string()
                    } else {
                        "".to_string()
                    };

                    let categories_str = row.try_get::<String, _>("categories").unwrap_or_default();
                    let categories_value = serde_json::to_value(self.parse_categories_json(&categories_str).unwrap_or_default()).unwrap_or(serde_json::Value::Object(serde_json::Map::new()));

                    let result = serde_json::json!({
                        "podcastid": row.try_get::<i32, _>("podcastid").unwrap_or(0),
                        "podcastname": row.try_get::<String, _>("podcastname").unwrap_or_default(),
                        "artworkurl": row.try_get::<Option<String>, _>("artworkurl").unwrap_or_default().unwrap_or_default(),
                        "author": row.try_get::<String, _>("author").unwrap_or_default(),
                        "categories": categories_value,
                        "description": row.try_get::<String, _>("description").unwrap_or_default(),
                        "episodecount": row.try_get::<Option<i32>, _>("episodecount").ok().flatten(),
                        "feedurl": row.try_get::<String, _>("feedurl").unwrap_or_default(),
                        "websiteurl": row.try_get::<String, _>("websiteurl").unwrap_or_default(),
                        "explicit": if row.try_get::<bool, _>("explicit").unwrap_or(false) { 1 } else { 0 },
                        "userid": row.try_get::<i32, _>("userid").unwrap_or(0),
                        "episodeid": row.try_get::<Option<i32>, _>("episodeid").ok().flatten(),
                        "episodetitle": row.try_get::<Option<String>, _>("episodetitle").ok().flatten(),
                        "episodedescription": row.try_get::<Option<String>, _>("episodedescription").ok().flatten(),
                        "episodeurl": row.try_get::<Option<String>, _>("episodeurl").ok().flatten(),
                        "episodeartwork": row.try_get::<Option<String>, _>("episodeartwork").ok().flatten(),
                        "episodepubdate": if pub_date.is_empty() { None } else { Some(pub_date) },
                        "episodeduration": row.try_get::<Option<i32>, _>("episodeduration").ok().flatten(),
                        "listenduration": row.try_get::<Option<i32>, _>("listenduration").ok().flatten(),
                        "completed": row.try_get::<bool, _>("completed").unwrap_or(false),
                        "saved": row.try_get::<bool, _>("saved").unwrap_or(false),
                        "queued": row.try_get::<bool, _>("queued").unwrap_or(false),
                        "downloaded": row.try_get::<bool, _>("downloaded").unwrap_or(false),
                        "is_youtube": row.try_get::<bool, _>("is_youtube").unwrap_or(false)
                    });
                    results.push(result);
                }
                Ok(results)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(
                    "SELECT
                        p.PodcastID as podcastid,
                        p.PodcastName as podcastname,
                        p.ArtworkURL as artworkurl,
                        p.Author as author,
                        p.Categories as categories,
                        p.Description as description,
                        p.EpisodeCount as episodecount,
                        p.FeedURL as feedurl,
                        p.WebsiteURL as websiteurl,
                        p.Explicit as explicit,
                        p.UserID as userid,
                        COALESCE(p.IsYouTubeChannel, false) as is_youtube,
                        e.EpisodeID as episodeid,
                        e.EpisodeTitle as episodetitle,
                        e.EpisodeDescription as episodedescription,
                        e.EpisodeURL as episodeurl,
                        CASE 
                            WHEN p.UsePodcastCoversCustomized = TRUE AND p.UsePodcastCovers = TRUE THEN p.ArtworkURL
                            WHEN u.UsePodcastCovers = TRUE THEN p.ArtworkURL
                            ELSE e.EpisodeArtwork
                        END as episodeartwork,
                        e.EpisodePubDate as episodepubdate,
                        e.EpisodeDuration as episodeduration,
                        COALESCE(h.ListenDuration, 0) as listenduration,
                        COALESCE(e.Completed, false) as completed,
                        CASE WHEN se.EpisodeID IS NOT NULL THEN true ELSE false END as saved,
                        CASE WHEN eq.EpisodeID IS NOT NULL THEN true ELSE false END as queued,
                        CASE WHEN de.EpisodeID IS NOT NULL THEN true ELSE false END as downloaded
                    FROM Podcasts p
                    LEFT JOIN Users u ON p.UserID = u.UserID
                    LEFT JOIN Episodes e ON p.PodcastID = e.PodcastID
                    LEFT JOIN UserEpisodeHistory h ON e.EpisodeID = h.EpisodeID AND h.UserID = ?
                    LEFT JOIN SavedEpisodes se ON e.EpisodeID = se.EpisodeID AND se.UserID = ?
                    LEFT JOIN EpisodeQueue eq ON e.EpisodeID = eq.EpisodeID AND eq.UserID = ? AND eq.is_youtube = false
                    LEFT JOIN DownloadedEpisodes de ON e.EpisodeID = de.EpisodeID AND de.UserID = ?
                    WHERE p.UserID = ? 
                      AND (LOWER(p.PodcastName) LIKE LOWER(?) 
                           OR LOWER(e.EpisodeTitle) LIKE LOWER(?)
                           OR LOWER(e.EpisodeDescription) LIKE LOWER(?))
                    ORDER BY p.PodcastName, e.EpisodePubDate DESC"
                )
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(format!("%{}%", search_term))
                .bind(format!("%{}%", search_term))
                .bind(format!("%{}%", search_term))
                .fetch_all(pool)
                .await?;

                let mut results = Vec::new();
                for row in rows {
                    let pub_date = if let Ok(date) = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate") {
                        date.format("%Y-%m-%dT%H:%M:%S").to_string()
                    } else {
                        "".to_string()
                    };

                    let categories_str = row.try_get::<String, _>("categories").unwrap_or_default();
                    let categories_value = serde_json::to_value(self.parse_categories_json(&categories_str).unwrap_or_default()).unwrap_or(serde_json::Value::Object(serde_json::Map::new()));

                    let result = serde_json::json!({
                        "podcastid": row.try_get::<i32, _>("podcastid").unwrap_or(0),
                        "podcastname": row.try_get::<String, _>("podcastname").unwrap_or_default(),
                        "artworkurl": row.try_get::<Option<String>, _>("artworkurl").unwrap_or_default().unwrap_or_default(),
                        "author": row.try_get::<String, _>("author").unwrap_or_default(),
                        "categories": categories_value,
                        "description": row.try_get::<String, _>("description").unwrap_or_default(),
                        "episodecount": row.try_get::<Option<i32>, _>("episodecount").ok().flatten(),
                        "feedurl": row.try_get::<String, _>("feedurl").unwrap_or_default(),
                        "websiteurl": row.try_get::<String, _>("websiteurl").unwrap_or_default(),
                        "explicit": if row.try_get::<bool, _>("explicit").unwrap_or(false) { 1 } else { 0 },
                        "userid": row.try_get::<i32, _>("userid").unwrap_or(0),
                        "episodeid": row.try_get::<Option<i32>, _>("episodeid").ok().flatten(),
                        "episodetitle": row.try_get::<Option<String>, _>("episodetitle").ok().flatten(),
                        "episodedescription": row.try_get::<Option<String>, _>("episodedescription").ok().flatten(),
                        "episodeurl": row.try_get::<Option<String>, _>("episodeurl").ok().flatten(),
                        "episodeartwork": row.try_get::<Option<String>, _>("episodeartwork").ok().flatten(),
                        "episodepubdate": if pub_date.is_empty() { None } else { Some(pub_date) },
                        "episodeduration": row.try_get::<Option<i32>, _>("episodeduration").ok().flatten(),
                        "listenduration": row.try_get::<Option<i32>, _>("listenduration").ok().flatten(),
                        "completed": row.try_get::<bool, _>("completed").unwrap_or(false),
                        "saved": row.try_get::<bool, _>("saved").unwrap_or(false),
                        "queued": row.try_get::<bool, _>("queued").unwrap_or(false),
                        "downloaded": row.try_get::<bool, _>("downloaded").unwrap_or(false),
                        "is_youtube": row.try_get::<bool, _>("is_youtube").unwrap_or(false)
                    });
                    results.push(result);
                }
                Ok(results)
            }
        }
    }

    // Get home overview data - matches Python get_home_overview function
    pub async fn get_home_overview(&self, user_id: i32) -> AppResult<serde_json::Value> {
        match self {
            DatabasePool::Postgres(pool) => {
                let mut home_data = serde_json::json!({
                    "recent_episodes": [],
                    "in_progress_episodes": [],
                    "top_podcasts": [],
                    "saved_count": 0,
                    "downloaded_count": 0,
                    "queue_count": 0
                });

                // Recent Episodes query
                let recent_query = r#"
                    SELECT
                        "Episodes".episodeid,
                        "Episodes".episodetitle,
                        "Episodes".episodepubdate,
                        "Episodes".episodedescription,
                        CASE 
                            WHEN "Podcasts".usepodcastcoverscustomized = TRUE AND "Podcasts".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                            WHEN "Users".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                            ELSE "Episodes".episodeartwork
                        END as episodeartwork,
                        "Episodes".episodeurl,
                        "Episodes".episodeduration,
                        "Episodes".completed,
                        "Podcasts".podcastname,
                        "Podcasts".podcastid,
                        "Podcasts".isyoutubechannel as is_youtube,
                        "UserEpisodeHistory".listenduration,
                        CASE WHEN "SavedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                        CASE WHEN "EpisodeQueue".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                        CASE WHEN "DownloadedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded
                    FROM "Episodes"
                    INNER JOIN "Podcasts" ON "Episodes".podcastid = "Podcasts".podcastid
                    LEFT JOIN "Users" ON "Podcasts".userid = "Users".userid
                    LEFT JOIN "UserEpisodeHistory" ON
                        "Episodes".episodeid = "UserEpisodeHistory".episodeid
                        AND "UserEpisodeHistory".userid = $1
                    LEFT JOIN "SavedEpisodes" ON
                        "Episodes".episodeid = "SavedEpisodes".episodeid
                        AND "SavedEpisodes".userid = $2
                    LEFT JOIN "EpisodeQueue" ON
                        "Episodes".episodeid = "EpisodeQueue".episodeid
                        AND "EpisodeQueue".userid = $3
                    LEFT JOIN "DownloadedEpisodes" ON
                        "Episodes".episodeid = "DownloadedEpisodes".episodeid
                        AND "DownloadedEpisodes".userid = $4
                    WHERE "Podcasts".userid = $5
                        AND "Episodes".episodepubdate >= NOW() - INTERVAL '7 days'
                    ORDER BY "Episodes".episodepubdate DESC
                    LIMIT 10
                "#;

                let recent_rows = sqlx::query(recent_query)
                    .bind(user_id)
                    .bind(user_id)
                    .bind(user_id)
                    .bind(user_id)
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;

                let mut recent_episodes = Vec::new();
                for row in recent_rows {
                    let episodeid: i32 = row.try_get("episodeid")?;
                    let episodetitle: String = row.try_get("episodetitle")?;
                    let naive = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                    let episodepubdate = naive.format("%Y-%m-%dT%H:%M:%S").to_string();
                    let episodedescription: String = row.try_get("episodedescription")?;
                    let episodeartwork: String = row.try_get("episodeartwork")?;
                    let episodeurl: String = row.try_get("episodeurl")?;
                    let episodeduration: i32 = row.try_get("episodeduration")?;
                    let completed: bool = row.try_get("completed")?;
                    let podcastname: String = row.try_get("podcastname")?;
                    let podcastid: i32 = row.try_get("podcastid")?;
                    let is_youtube: bool = row.try_get("is_youtube")?;
                    let listenduration: Option<i32> = row.try_get("listenduration")?;
                    let saved: bool = row.try_get("saved")?;
                    let queued: bool = row.try_get("queued")?;
                    let downloaded: bool = row.try_get("downloaded")?;

                    recent_episodes.push(serde_json::json!({
                        "episodeid": episodeid,
                        "episodetitle": episodetitle,
                        "episodepubdate": episodepubdate,
                        "episodedescription": episodedescription,
                        "episodeartwork": episodeartwork,
                        "episodeurl": episodeurl,
                        "episodeduration": episodeduration,
                        "completed": completed,
                        "podcastname": podcastname,
                        "podcastid": podcastid,
                        "is_youtube": is_youtube,
                        "listenduration": listenduration,
                        "saved": saved,
                        "queued": queued,
                        "downloaded": downloaded
                    }));
                }
                home_data["recent_episodes"] = serde_json::Value::Array(recent_episodes);

                // In Progress Episodes query
                let in_progress_query = r#"
                    SELECT
                        "Episodes".episodeid,
                        "Episodes".episodetitle,
                        "Episodes".episodepubdate,
                        "Episodes".episodedescription,
                        CASE 
                            WHEN "Podcasts".usepodcastcoverscustomized = TRUE AND "Podcasts".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                            WHEN "Users".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                            ELSE "Episodes".episodeartwork
                        END as episodeartwork,
                        "Episodes".episodeurl,
                        "Episodes".episodeduration,
                        "Episodes".completed,
                        "Podcasts".podcastname,
                        "Podcasts".podcastid,
                        "Podcasts".isyoutubechannel as is_youtube,
                        "UserEpisodeHistory".listenduration,
                        CASE WHEN "SavedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                        CASE WHEN "EpisodeQueue".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                        CASE WHEN "DownloadedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded
                    FROM "UserEpisodeHistory"
                    JOIN "Episodes" ON "UserEpisodeHistory".episodeid = "Episodes".episodeid
                    JOIN "Podcasts" ON "Episodes".podcastid = "Podcasts".podcastid
                    LEFT JOIN "Users" ON "Podcasts".userid = "Users".userid
                    LEFT JOIN "SavedEpisodes" ON
                        "Episodes".episodeid = "SavedEpisodes".episodeid
                        AND "SavedEpisodes".userid = $1
                    LEFT JOIN "EpisodeQueue" ON
                        "Episodes".episodeid = "EpisodeQueue".episodeid
                        AND "EpisodeQueue".userid = $2
                    LEFT JOIN "DownloadedEpisodes" ON
                        "Episodes".episodeid = "DownloadedEpisodes".episodeid
                        AND "DownloadedEpisodes".userid = $3
                    WHERE "UserEpisodeHistory".userid = $4
                    AND "UserEpisodeHistory".listenduration > 0
                    AND "Episodes".completed = FALSE
                    ORDER BY "UserEpisodeHistory".listendate DESC
                    LIMIT 10
                "#;

                let in_progress_rows = sqlx::query(in_progress_query)
                    .bind(user_id)
                    .bind(user_id)
                    .bind(user_id)
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;

                let mut in_progress_episodes = Vec::new();
                for row in in_progress_rows {
                    let episodeid: i32 = row.try_get("episodeid")?;
                    let episodetitle: String = row.try_get("episodetitle")?;
                    let naive = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                    let episodepubdate = naive.format("%Y-%m-%dT%H:%M:%S").to_string();
                    let episodedescription: String = row.try_get("episodedescription")?;
                    let episodeartwork: String = row.try_get("episodeartwork")?;
                    let episodeurl: String = row.try_get("episodeurl")?;
                    let episodeduration: i32 = row.try_get("episodeduration")?;
                    let completed: bool = row.try_get("completed")?;
                    let podcastname: String = row.try_get("podcastname")?;
                    let podcastid: i32 = row.try_get("podcastid")?;
                    let is_youtube: bool = row.try_get("is_youtube")?;
                    let listenduration: Option<i32> = row.try_get("listenduration")?;
                    let saved: bool = row.try_get("saved")?;
                    let queued: bool = row.try_get("queued")?;
                    let downloaded: bool = row.try_get("downloaded")?;

                    in_progress_episodes.push(serde_json::json!({
                        "episodeid": episodeid,
                        "episodetitle": episodetitle,
                        "episodepubdate": episodepubdate,
                        "episodedescription": episodedescription,
                        "episodeartwork": episodeartwork,
                        "episodeurl": episodeurl,
                        "episodeduration": episodeduration,
                        "completed": completed,
                        "podcastname": podcastname,
                        "podcastid": podcastid,
                        "is_youtube": is_youtube,
                        "listenduration": listenduration,
                        "saved": saved,
                        "queued": queued,
                        "downloaded": downloaded
                    }));
                }
                home_data["in_progress_episodes"] = serde_json::Value::Array(in_progress_episodes);

                // Top Podcasts query
                let top_podcasts_query = r#"
                    SELECT
                        "Podcasts".podcastid,
                        "Podcasts".podcastname,
                        "Podcasts".podcastindexid,
                        "Podcasts".artworkurl,
                        "Podcasts".author,
                        "Podcasts".categories,
                        "Podcasts".description,
                        "Podcasts".episodecount,
                        "Podcasts".feedurl,
                        "Podcasts".websiteurl,
                        "Podcasts".explicit,
                        "Podcasts".isyoutubechannel as is_youtube,
                        COUNT(DISTINCT "UserEpisodeHistory".episodeid) as play_count,
                        SUM("UserEpisodeHistory".listenduration) as total_listen_time
                    FROM "Podcasts"
                    LEFT JOIN "Episodes" ON "Podcasts".podcastid = "Episodes".podcastid
                    LEFT JOIN "UserEpisodeHistory" ON "Episodes".episodeid = "UserEpisodeHistory".episodeid
                    WHERE "Podcasts".userid = $1
                    GROUP BY "Podcasts".podcastid
                    ORDER BY total_listen_time DESC NULLS LAST
                    LIMIT 6
                "#;

                let top_podcasts_rows = sqlx::query(top_podcasts_query)
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;

                let mut top_podcasts = Vec::new();
                for row in top_podcasts_rows {
                    let podcastid: i32 = row.try_get("podcastid").unwrap_or(0);
                    let podcastname: String = row.try_get("podcastname").unwrap_or_default();
                    let podcastindexid: Option<i32> = row.try_get("podcastindexid").ok();
                    let artworkurl: String = row.try_get("artworkurl").unwrap_or_default();
                    let author: String = row.try_get("author").unwrap_or_default();
                    let categories_str: String = row.try_get("categories").unwrap_or_default();
                    let categories = self.parse_categories_json(&categories_str).unwrap_or_default();
                    let description: String = row.try_get("description").unwrap_or_default();
                    let episodecount: i32 = row.try_get("episodecount").unwrap_or(0);
                    let feedurl: String = row.try_get("feedurl").unwrap_or_default();
                    let websiteurl: String = row.try_get("websiteurl").unwrap_or_default();
                    let explicit: bool = row.try_get("explicit").unwrap_or(false);
                    let is_youtube: bool = row.try_get("is_youtube").unwrap_or(false);
                    let play_count: i64 = row.try_get("play_count").unwrap_or(0);
                    let total_listen_time: Option<i64> = row.try_get("total_listen_time").ok();

                    top_podcasts.push(serde_json::json!({
                        "podcastid": podcastid,
                        "podcastname": podcastname,
                        "podcastindexid": podcastindexid,
                        "artworkurl": artworkurl,
                        "author": author,
                        "categories": serde_json::to_value(categories).unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
                        "description": description,
                        "episodecount": episodecount,
                        "feedurl": feedurl,
                        "websiteurl": websiteurl,
                        "explicit": explicit,
                        "is_youtube": is_youtube,
                        "play_count": play_count,
                        "total_listen_time": total_listen_time
                    }));
                }
                home_data["top_podcasts"] = serde_json::Value::Array(top_podcasts);

                // Get counts
                let saved_count: i64 = sqlx::query_scalar(r#"SELECT COUNT(*) FROM "SavedEpisodes" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                home_data["saved_count"] = serde_json::Value::Number(serde_json::Number::from(saved_count));

                let downloaded_count: i64 = sqlx::query_scalar(r#"SELECT COUNT(*) FROM "DownloadedEpisodes" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                home_data["downloaded_count"] = serde_json::Value::Number(serde_json::Number::from(downloaded_count));

                let queue_count: i64 = sqlx::query_scalar(r#"SELECT COUNT(*) FROM "EpisodeQueue" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                home_data["queue_count"] = serde_json::Value::Number(serde_json::Number::from(queue_count));

                Ok(home_data)
            }
            DatabasePool::MySQL(pool) => {
                let mut home_data = serde_json::json!({
                    "recent_episodes": [],
                    "in_progress_episodes": [],
                    "top_podcasts": [],
                    "saved_count": 0,
                    "downloaded_count": 0,
                    "queue_count": 0
                });

                // Recent Episodes query for MySQL
                let recent_query = r#"
                    SELECT
                        Episodes.EpisodeID,
                        Episodes.EpisodeTitle,
                        Episodes.EpisodePubDate,
                        Episodes.EpisodeDescription,
                        CASE 
                            WHEN Podcasts.UsePodcastCoversCustomized = 1 AND Podcasts.UsePodcastCovers = 1 THEN Podcasts.ArtworkURL
                            WHEN Users.UsePodcastCovers = 1 THEN Podcasts.ArtworkURL
                            ELSE Episodes.EpisodeArtwork
                        END as EpisodeArtwork,
                        Episodes.EpisodeURL,
                        Episodes.EpisodeDuration,
                        Episodes.Completed,
                        Podcasts.PodcastName,
                        Podcasts.PodcastID,
                        Podcasts.IsYouTubeChannel as is_youtube,
                        UserEpisodeHistory.ListenDuration,
                        CASE WHEN SavedEpisodes.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                        CASE WHEN EpisodeQueue.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                        CASE WHEN DownloadedEpisodes.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded
                    FROM Episodes
                    INNER JOIN Podcasts ON Episodes.PodcastID = Podcasts.PodcastID
                    LEFT JOIN Users ON Podcasts.UserID = Users.UserID
                    LEFT JOIN UserEpisodeHistory ON
                        Episodes.EpisodeID = UserEpisodeHistory.EpisodeID
                        AND UserEpisodeHistory.UserID = ?
                    LEFT JOIN SavedEpisodes ON
                        Episodes.EpisodeID = SavedEpisodes.EpisodeID
                        AND SavedEpisodes.UserID = ?
                    LEFT JOIN EpisodeQueue ON
                        Episodes.EpisodeID = EpisodeQueue.EpisodeID
                        AND EpisodeQueue.UserID = ?
                    LEFT JOIN DownloadedEpisodes ON
                        Episodes.EpisodeID = DownloadedEpisodes.EpisodeID
                        AND DownloadedEpisodes.UserID = ?
                    WHERE Podcasts.UserID = ?
                        AND Episodes.EpisodePubDate >= DATE_SUB(NOW(), INTERVAL 7 DAY)
                    ORDER BY Episodes.EpisodePubDate DESC
                    LIMIT 10
                "#;

                let recent_rows = sqlx::query(recent_query)
                    .bind(user_id)
                    .bind(user_id)
                    .bind(user_id)
                    .bind(user_id)
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;

                let mut recent_episodes = Vec::new();
                for row in recent_rows {
                    let episodeid: i32 = row.try_get("EpisodeID")?;
                    let episodetitle: String = row.try_get("EpisodeTitle")?;
                    let naive = row.try_get::<chrono::NaiveDateTime, _>("EpisodePubDate")?;
                    let episodepubdate = naive.format("%Y-%m-%dT%H:%M:%S").to_string();
                    let episodedescription: String = row.try_get("EpisodeDescription")?;
                    let episodeartwork: String = row.try_get("EpisodeArtwork")?;
                    let episodeurl: String = row.try_get("EpisodeURL")?;
                    let episodeduration: i32 = row.try_get("EpisodeDuration")?;
                    let completed: bool = row.try_get::<i8, _>("Completed")? != 0;
                    let podcastname: String = row.try_get("PodcastName")?;
                    let podcastid: i32 = row.try_get("PodcastID")?;
                    let is_youtube: bool = row.try_get::<i8, _>("is_youtube")? != 0;
                    let listenduration: Option<i32> = row.try_get("ListenDuration")?;
                    let saved: bool = row.try_get::<i8, _>("saved")? != 0;
                    let queued: bool = row.try_get::<i8, _>("queued")? != 0;
                    let downloaded: bool = row.try_get::<i8, _>("downloaded")? != 0;

                    recent_episodes.push(serde_json::json!({
                        "episodeid": episodeid,
                        "episodetitle": episodetitle,
                        "episodepubdate": episodepubdate,
                        "episodedescription": episodedescription,
                        "episodeartwork": episodeartwork,
                        "episodeurl": episodeurl,
                        "episodeduration": episodeduration,
                        "completed": completed,
                        "podcastname": podcastname,
                        "podcastid": podcastid,
                        "is_youtube": is_youtube,
                        "listenduration": listenduration,
                        "saved": saved,
                        "queued": queued,
                        "downloaded": downloaded
                    }));
                }
                home_data["recent_episodes"] = serde_json::Value::Array(recent_episodes);

                // In Progress Episodes query for MySQL
                let in_progress_query = r#"
                    SELECT
                        Episodes.EpisodeID,
                        Episodes.EpisodeTitle,
                        Episodes.EpisodePubDate,
                        Episodes.EpisodeDescription,
                        CASE 
                            WHEN Podcasts.UsePodcastCoversCustomized = 1 AND Podcasts.UsePodcastCovers = 1 THEN Podcasts.ArtworkURL
                            WHEN Users.UsePodcastCovers = 1 THEN Podcasts.ArtworkURL
                            ELSE Episodes.EpisodeArtwork
                        END as EpisodeArtwork,
                        Episodes.EpisodeURL,
                        Episodes.EpisodeDuration,
                        Episodes.Completed,
                        Podcasts.PodcastName,
                        Podcasts.PodcastID,
                        Podcasts.IsYouTubeChannel as is_youtube,
                        UserEpisodeHistory.ListenDuration,
                        CASE WHEN SavedEpisodes.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                        CASE WHEN EpisodeQueue.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                        CASE WHEN DownloadedEpisodes.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded
                    FROM UserEpisodeHistory
                    JOIN Episodes ON UserEpisodeHistory.EpisodeID = Episodes.EpisodeID
                    JOIN Podcasts ON Episodes.PodcastID = Podcasts.PodcastID
                    LEFT JOIN Users ON Podcasts.UserID = Users.UserID
                    LEFT JOIN SavedEpisodes ON
                        Episodes.EpisodeID = SavedEpisodes.EpisodeID
                        AND SavedEpisodes.UserID = ?
                    LEFT JOIN EpisodeQueue ON
                        Episodes.EpisodeID = EpisodeQueue.EpisodeID
                        AND EpisodeQueue.UserID = ?
                    LEFT JOIN DownloadedEpisodes ON
                        Episodes.EpisodeID = DownloadedEpisodes.EpisodeID
                        AND DownloadedEpisodes.UserID = ?
                    WHERE UserEpisodeHistory.UserID = ?
                    AND UserEpisodeHistory.ListenDuration > 0
                    AND Episodes.Completed = 0
                    ORDER BY UserEpisodeHistory.ListenDate DESC
                    LIMIT 10
                "#;

                let in_progress_rows = sqlx::query(in_progress_query)
                    .bind(user_id)
                    .bind(user_id)
                    .bind(user_id)
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;

                let mut in_progress_episodes = Vec::new();
                for row in in_progress_rows {
                    let episodeid: i32 = row.try_get("EpisodeID")?;
                    let episodetitle: String = row.try_get("EpisodeTitle")?;
                    let naive = row.try_get::<chrono::NaiveDateTime, _>("EpisodePubDate")?;
                    let episodepubdate = naive.format("%Y-%m-%dT%H:%M:%S").to_string();
                    let episodedescription: String = row.try_get("EpisodeDescription")?;
                    let episodeartwork: String = row.try_get("EpisodeArtwork")?;
                    let episodeurl: String = row.try_get("EpisodeURL")?;
                    let episodeduration: i32 = row.try_get("EpisodeDuration")?;
                    let completed: bool = row.try_get::<i8, _>("Completed")? != 0;
                    let podcastname: String = row.try_get("PodcastName")?;
                    let podcastid: i32 = row.try_get("PodcastID")?;
                    let is_youtube: bool = row.try_get::<i8, _>("is_youtube")? != 0;
                    let listenduration: Option<i32> = row.try_get("ListenDuration")?;
                    let saved: bool = row.try_get::<i8, _>("saved")? != 0;
                    let queued: bool = row.try_get::<i8, _>("queued")? != 0;
                    let downloaded: bool = row.try_get::<i8, _>("downloaded")? != 0;

                    in_progress_episodes.push(serde_json::json!({
                        "episodeid": episodeid,
                        "episodetitle": episodetitle,
                        "episodepubdate": episodepubdate,
                        "episodedescription": episodedescription,
                        "episodeartwork": episodeartwork,
                        "episodeurl": episodeurl,
                        "episodeduration": episodeduration,
                        "completed": completed,
                        "podcastname": podcastname,
                        "podcastid": podcastid,
                        "is_youtube": is_youtube,
                        "listenduration": listenduration,
                        "saved": saved,
                        "queued": queued,
                        "downloaded": downloaded
                    }));
                }
                home_data["in_progress_episodes"] = serde_json::Value::Array(in_progress_episodes);

                // Top Podcasts query for MySQL
                let top_podcasts_query = r#"
                    SELECT
                        Podcasts.PodcastID,
                        Podcasts.PodcastName,
                        Podcasts.PodcastIndexID,
                        Podcasts.ArtworkURL,
                        Podcasts.Author,
                        Podcasts.Categories,
                        Podcasts.Description,
                        Podcasts.EpisodeCount,
                        Podcasts.FeedURL,
                        Podcasts.WebsiteURL,
                        Podcasts.Explicit,
                        Podcasts.IsYouTubeChannel as is_youtube,
                        COUNT(DISTINCT UserEpisodeHistory.EpisodeID) as play_count,
                        SUM(UserEpisodeHistory.ListenDuration) as total_listen_time
                    FROM Podcasts
                    LEFT JOIN Episodes ON Podcasts.PodcastID = Episodes.PodcastID
                    LEFT JOIN UserEpisodeHistory ON Episodes.EpisodeID = UserEpisodeHistory.EpisodeID
                    WHERE Podcasts.UserID = ?
                    GROUP BY Podcasts.PodcastID
                    ORDER BY total_listen_time DESC
                    LIMIT 5
                "#;

                let top_podcasts_rows = sqlx::query(top_podcasts_query)
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;

                let mut top_podcasts = Vec::new();
                for row in top_podcasts_rows {
                    let podcastid: i32 = row.try_get("PodcastID").unwrap_or(0);
                    let podcastname: String = row.try_get("PodcastName").unwrap_or_default();
                    let podcastindexid: Option<i32> = row.try_get("PodcastIndexID").ok();
                    let artworkurl: String = row.try_get("ArtworkURL").unwrap_or_default();
                    let author: String = row.try_get("Author").unwrap_or_default();
                    let categories_str: String = row.try_get("Categories").unwrap_or_default();
                    let categories = self.parse_categories_json(&categories_str).unwrap_or_default();
                    let description: String = row.try_get("Description").unwrap_or_default();
                    let episodecount: i32 = row.try_get("EpisodeCount").unwrap_or(0);
                    let feedurl: String = row.try_get("FeedURL").unwrap_or_default();
                    let websiteurl: String = row.try_get("WebsiteURL").unwrap_or_default();
                    let explicit: bool = row.try_get::<i8, _>("Explicit").unwrap_or(0) != 0;
                    let is_youtube: bool = row.try_get::<i8, _>("is_youtube").unwrap_or(0) != 0;
                    let play_count: i64 = row.try_get("play_count").unwrap_or(0);
                    let total_listen_time: Option<i64> = row.try_get("total_listen_time").ok();

                    top_podcasts.push(serde_json::json!({
                        "podcastid": podcastid,
                        "podcastname": podcastname,
                        "podcastindexid": podcastindexid,
                        "artworkurl": artworkurl,
                        "author": author,
                        "categories": serde_json::to_value(categories).unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
                        "description": description,
                        "episodecount": episodecount,
                        "feedurl": feedurl,
                        "websiteurl": websiteurl,
                        "explicit": explicit,
                        "is_youtube": is_youtube,
                        "play_count": play_count,
                        "total_listen_time": total_listen_time
                    }));
                }
                home_data["top_podcasts"] = serde_json::Value::Array(top_podcasts);

                // Get counts for MySQL
                let saved_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM SavedEpisodes WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                home_data["saved_count"] = serde_json::Value::Number(serde_json::Number::from(saved_count));

                let downloaded_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM DownloadedEpisodes WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                home_data["downloaded_count"] = serde_json::Value::Number(serde_json::Number::from(downloaded_count));

                let queue_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM EpisodeQueue WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                home_data["queue_count"] = serde_json::Value::Number(serde_json::Number::from(queue_count));

                Ok(home_data)
            }
        }
    }

    // Get playlists - matches Python get_playlists function
    pub async fn get_playlists(&self, user_id: i32) -> AppResult<Vec<serde_json::Value>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let query = r#"
                    SELECT
                        p.playlistid,
                        p.userid,
                        p.name,
                        p.description,
                        p.issystemplaylist,
                        p.podcastids,
                        p.includeunplayed,
                        p.includepartiallyplayed,
                        p.includeplayed,
                        p.minduration,
                        p.maxduration,
                        p.sortorder,
                        p.groupbypodcast,
                        p.maxepisodes,
                        p.lastupdated,
                        p.created,
                        p.iconname,
                        COALESCE(p.episodecount, 0) as episode_count
                    FROM "Playlists" p
                    WHERE p.userid = $1
                    ORDER BY p.issystemplaylist DESC, p.name ASC
                "#;

                let rows = sqlx::query(query)
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;

                let mut playlists = Vec::new();
                for row in rows {
                    let playlist_id: i32 = row.try_get("playlistid")?;
                    
                    // Get preview episodes
                    let preview_query = r#"
                        SELECT e.episodetitle, e.episodeartwork
                        FROM "PlaylistContents" pc
                        JOIN "Episodes" e ON pc.episodeid = e.episodeid
                        JOIN "Podcasts" p ON e.podcastid = p.podcastid
                        WHERE pc.playlistid = $1
                        AND p.userid = $2
                        ORDER BY pc.position
                        LIMIT 3
                    "#;

                    let preview_rows = sqlx::query(preview_query)
                        .bind(playlist_id)
                        .bind(user_id)
                        .fetch_all(pool)
                        .await?;

                    let mut preview_episodes = Vec::new();
                    for preview_row in preview_rows {
                        preview_episodes.push(serde_json::json!({
                            "title": preview_row.try_get::<String, _>("episodetitle")?,
                            "artwork": preview_row.try_get::<String, _>("episodeartwork")?
                        }));
                    }

                    // Process podcast_ids - in PostgreSQL it's stored as an array
                    let podcast_ids: Option<Vec<i32>> = row.try_get("podcastids").ok();

                    let playlist = serde_json::json!({
                        "playlist_id": playlist_id,
                        "user_id": row.try_get::<i32, _>("userid")?,
                        "name": row.try_get::<String, _>("name")?,
                        "description": row.try_get::<String, _>("description")?,
                        "is_system_playlist": row.try_get::<bool, _>("issystemplaylist")?,
                        "podcast_ids": podcast_ids,
                        "include_unplayed": row.try_get::<bool, _>("includeunplayed")?,
                        "include_partially_played": row.try_get::<bool, _>("includepartiallyplayed")?,
                        "include_played": row.try_get::<bool, _>("includeplayed")?,
                        "min_duration": row.try_get::<Option<i32>, _>("minduration")?,
                        "max_duration": row.try_get::<Option<i32>, _>("maxduration")?,
                        "sort_order": row.try_get::<Option<String>, _>("sortorder")?,
                        "group_by_podcast": row.try_get::<bool, _>("groupbypodcast")?,
                        "max_episodes": row.try_get::<Option<i32>, _>("maxepisodes")?,
                        "last_updated": row.try_get::<Option<chrono::NaiveDateTime>, _>("lastupdated")?.map(|dt| dt.format("%Y-%m-%dT%H:%M:%S").to_string()).unwrap_or_default(),
                        "created": row.try_get::<Option<chrono::NaiveDateTime>, _>("created")?.map(|dt| dt.format("%Y-%m-%dT%H:%M:%S").to_string()).unwrap_or_default(),
                        "icon_name": row.try_get::<Option<String>, _>("iconname")?.unwrap_or_default(),
                        "episode_count": row.try_get::<i32, _>("episode_count")?,
                        "preview_episodes": preview_episodes
                    });

                    playlists.push(playlist);
                }

                Ok(playlists)
            }
            DatabasePool::MySQL(pool) => {
                let query = r#"
                    SELECT
                        p.PlaylistID,
                        p.UserID,
                        p.Name,
                        p.Description,
                        p.IsSystemPlaylist,
                        p.PodcastIDs,
                        p.IncludeUnplayed,
                        p.IncludePartiallyPlayed,
                        p.IncludePlayed,
                        p.MinDuration,
                        p.MaxDuration,
                        p.SortOrder,
                        p.GroupByPodcast,
                        p.MaxEpisodes,
                        p.LastUpdated,
                        p.Created,
                        p.IconName,
                        COALESCE(p.EpisodeCount, 0) as episode_count
                    FROM Playlists p
                    WHERE p.UserID = ?
                    ORDER BY p.IsSystemPlaylist DESC, p.Name ASC
                "#;

                let rows = sqlx::query(query)
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;

                let mut playlists = Vec::new();
                for row in rows {
                    let playlist_id: i32 = row.try_get("PlaylistID")?;
                    
                    // Get preview episodes
                    let preview_query = r#"
                        SELECT e.EpisodeTitle, e.EpisodeArtwork
                        FROM PlaylistContents pc
                        JOIN Episodes e ON pc.EpisodeID = e.EpisodeID
                        JOIN Podcasts p ON e.PodcastID = p.PodcastID
                        WHERE pc.PlaylistID = ?
                        AND p.UserID = ?
                        ORDER BY pc.Position
                        LIMIT 3
                    "#;

                    let preview_rows = sqlx::query(preview_query)
                        .bind(playlist_id)
                        .bind(user_id)
                        .fetch_all(pool)
                        .await?;

                    let mut preview_episodes = Vec::new();
                    for preview_row in preview_rows {
                        preview_episodes.push(serde_json::json!({
                            "title": preview_row.try_get::<String, _>("EpisodeTitle")?,
                            "artwork": preview_row.try_get::<String, _>("EpisodeArtwork")?
                        }));
                    }

                    // Process podcast_ids - in MySQL it might be stored as JSON string
                    let raw_podcast_ids: Option<String> = row.try_get("PodcastIDs").ok();
                    let mut podcast_ids: Option<Vec<i32>> = None;
                    
                    if let Some(raw_ids) = raw_podcast_ids {
                        if !raw_ids.is_empty() {
                            // Try to parse as JSON first
                            if let Ok(parsed) = serde_json::from_str::<Vec<i32>>(&raw_ids) {
                                podcast_ids = Some(parsed);
                            } else {
                                // Handle other formats like comma-separated strings
                                let parsed: Result<Vec<i32>, _> = raw_ids
                                    .trim_matches(|c| c == '[' || c == ']' || c == '"' || c == '\'')
                                    .split(',')
                                    .filter(|s| !s.trim().is_empty())
                                    .map(|s| s.trim().parse::<i32>())
                                    .collect();
                                
                                if let Ok(ids) = parsed {
                                    podcast_ids = Some(ids);
                                }
                            }
                        }
                    }

                    let playlist = serde_json::json!({
                        "playlist_id": playlist_id,
                        "user_id": row.try_get::<i32, _>("UserID")?,
                        "name": row.try_get::<String, _>("Name")?,
                        "description": row.try_get::<String, _>("Description")?,
                        "is_system_playlist": row.try_get::<i8, _>("IsSystemPlaylist")? != 0,
                        "podcast_ids": podcast_ids,
                        "include_unplayed": row.try_get::<i8, _>("IncludeUnplayed")? != 0,
                        "include_partially_played": row.try_get::<i8, _>("IncludePartiallyPlayed")? != 0,
                        "include_played": row.try_get::<i8, _>("IncludePlayed")? != 0,
                        "min_duration": row.try_get::<Option<i32>, _>("MinDuration")?,
                        "max_duration": row.try_get::<Option<i32>, _>("MaxDuration")?,
                        "sort_order": row.try_get::<Option<String>, _>("SortOrder")?,
                        "group_by_podcast": row.try_get::<i8, _>("GroupByPodcast")? != 0,
                        "max_episodes": row.try_get::<Option<i32>, _>("MaxEpisodes")?,
                        "last_updated": row.try_get::<Option<sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>>, _>("LastUpdated")?.map(|dt| dt.format("%Y-%m-%dT%H:%M:%S").to_string()).unwrap_or_default(),
                        "created": row.try_get::<Option<sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>>, _>("Created")?.map(|dt| dt.format("%Y-%m-%dT%H:%M:%S").to_string()).unwrap_or_default(),
                        "icon_name": row.try_get::<Option<String>, _>("IconName")?.unwrap_or_default(),
                        "episode_count": row.try_get::<i64, _>("episode_count")?,
                        "preview_episodes": preview_episodes
                    });

                    playlists.push(playlist);
                }

                Ok(playlists)
            }
        }
    }

    // Create playlist - matches Python create_playlist function exactly
    pub async fn create_playlist(&self, _config: &Config, playlist_data: &crate::models::CreatePlaylistRequest) -> AppResult<i32> {
        let min_duration = playlist_data.min_duration.map(|d| d * 60);
        let max_duration = playlist_data.max_duration.map(|d| d * 60);
        
        match self {
            DatabasePool::Postgres(pool) => {
                let podcast_ids_array = if let Some(ref ids) = playlist_data.podcast_ids {
                    ids.clone()
                } else {
                    vec![]
                };

                let result = sqlx::query(r#"
                    INSERT INTO "Playlists" (
                        userid,
                        name,
                        description,
                        issystemplaylist,
                        podcastids,
                        includeunplayed,
                        includepartiallyplayed,
                        includeplayed,
                        minduration,
                        maxduration,
                        sortorder,
                        groupbypodcast,
                        maxepisodes,
                        iconname,
                        playprogressmin,
                        playprogressmax,
                        timefilterhours
                    ) VALUES (
                        $1, $2, $3, FALSE, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16
                    ) RETURNING playlistid
                "#)
                .bind(playlist_data.user_id)
                .bind(&playlist_data.name)
                .bind(&playlist_data.description)
                .bind(&podcast_ids_array)
                .bind(playlist_data.include_unplayed)
                .bind(playlist_data.include_partially_played)
                .bind(playlist_data.include_played)
                .bind(min_duration)
                .bind(max_duration)
                .bind(&playlist_data.sort_order)
                .bind(playlist_data.group_by_podcast)
                .bind(playlist_data.max_episodes)
                .bind(&playlist_data.icon_name)
                .bind(playlist_data.play_progress_min)
                .bind(playlist_data.play_progress_max)
                .bind(playlist_data.time_filter_hours)
                .fetch_one(pool)
                .await?;

                let playlist_id = result.get::<i32, _>("playlistid");
                
                // Update playlist contents immediately like Python does
                self.update_playlist_contents(playlist_id).await?;
                
                Ok(playlist_id)
            }
            DatabasePool::MySQL(pool) => {
                let podcast_ids_json = if let Some(ref ids) = playlist_data.podcast_ids {
                    serde_json::to_string(ids)?
                } else {
                    "[]".to_string()
                };

                let result = sqlx::query(r#"
                    INSERT INTO Playlists (
                        UserID,
                        Name,
                        Description,
                        IsSystemPlaylist,
                        PodcastIDs,
                        IncludeUnplayed,
                        IncludePartiallyPlayed,
                        IncludePlayed,
                        MinDuration,
                        MaxDuration,
                        SortOrder,
                        GroupByPodcast,
                        MaxEpisodes,
                        IconName,
                        PlayProgressMin,
                        PlayProgressMax,
                        TimeFilterHours
                    ) VALUES (
                        ?, ?, ?, FALSE, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
                    )
                "#)
                .bind(playlist_data.user_id)
                .bind(&playlist_data.name)
                .bind(&playlist_data.description)
                .bind(&podcast_ids_json)
                .bind(playlist_data.include_unplayed)
                .bind(playlist_data.include_partially_played)
                .bind(playlist_data.include_played)
                .bind(min_duration)
                .bind(max_duration)
                .bind(&playlist_data.sort_order)
                .bind(playlist_data.group_by_podcast)
                .bind(playlist_data.max_episodes)
                .bind(&playlist_data.icon_name)
                .bind(playlist_data.play_progress_min)
                .bind(playlist_data.play_progress_max)
                .bind(playlist_data.time_filter_hours)
                .execute(pool)
                .await?;

                let playlist_id = result.last_insert_id() as i32;
                
                // Update playlist contents immediately like Python does
                self.update_playlist_contents(playlist_id).await?;
                
                Ok(playlist_id)
            }
        }
    }

    // Mark episode as uncompleted - matches Python mark_episode_uncompleted function
    pub async fn mark_episode_uncompleted(&self, episode_id: i32, user_id: i32, is_youtube: bool) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                let mut transaction = pool.begin().await?;
                
                if is_youtube {
                    // Handle YouTube video
                    sqlx::query(r#"UPDATE "YouTubeVideos" SET completed = FALSE WHERE videoid = $1"#)
                        .bind(episode_id)
                        .execute(&mut *transaction)
                        .await?;
                    
                    sqlx::query(r#"UPDATE "UserVideoHistory" SET listenduration = 0, listendate = NOW() WHERE userid = $1 AND videoid = $2"#)
                        .bind(user_id)
                        .bind(episode_id)
                        .execute(&mut *transaction)
                        .await?;
                } else {
                    // Handle regular episode
                    sqlx::query(r#"UPDATE "Episodes" SET completed = FALSE WHERE episodeid = $1"#)
                        .bind(episode_id)
                        .execute(&mut *transaction)
                        .await?;
                    
                    sqlx::query(r#"UPDATE "UserEpisodeHistory" SET listenduration = 0, listendate = NOW() WHERE userid = $1 AND episodeid = $2"#)
                        .bind(user_id)
                        .bind(episode_id)
                        .execute(&mut *transaction)
                        .await?;
                }
                
                transaction.commit().await?;
                Ok(())
            }
            DatabasePool::MySQL(pool) => {
                let mut transaction = pool.begin().await?;
                
                if is_youtube {
                    // Handle YouTube video
                    sqlx::query("UPDATE YouTubeVideos SET Completed = 0 WHERE VideoID = ?")
                        .bind(episode_id)
                        .execute(&mut *transaction)
                        .await?;
                    
                    sqlx::query("UPDATE UserVideoHistory SET ListenDuration = 0, ListenDate = NOW() WHERE UserID = ? AND VideoID = ?")
                        .bind(user_id)
                        .bind(episode_id)
                        .execute(&mut *transaction)
                        .await?;
                } else {
                    // Handle regular episode
                    sqlx::query("UPDATE Episodes SET Completed = 0 WHERE EpisodeID = ?")
                        .bind(episode_id)
                        .execute(&mut *transaction)
                        .await?;
                    
                    sqlx::query("UPDATE UserEpisodeHistory SET ListenDuration = 0, ListenDate = NOW() WHERE UserID = ? AND EpisodeID = ?")
                        .bind(user_id)
                        .bind(episode_id)
                        .execute(&mut *transaction)
                        .await?;
                }
                
                transaction.commit().await?;
                Ok(())
            }
        }
    }

    // Get saved episodes - matches Python saved_episode_list function
    pub async fn get_saved_episodes(&self, user_id: i32) -> AppResult<Vec<crate::models::SavedEpisode>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT * FROM (
                        SELECT
                            "Podcasts".podcastname as podcastname,
                            "Episodes".episodetitle as episodetitle,
                            "Episodes".episodepubdate as episodepubdate,
                            "Episodes".episodedescription as episodedescription,
                            "Episodes".episodeid as episodeid,
                            CASE 
                                WHEN "Podcasts".usepodcastcoverscustomized = TRUE AND "Podcasts".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                WHEN "Users".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                ELSE "Episodes".episodeartwork
                            END as episodeartwork,
                            "Episodes".episodeurl as episodeurl,
                            "Episodes".episodeduration as episodeduration,
                            "Podcasts".websiteurl as websiteurl,
                            "UserEpisodeHistory".listenduration as listenduration,
                            "Episodes".completed as completed,
                            TRUE as saved,
                            CASE WHEN "EpisodeQueue".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN "DownloadedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            FALSE as is_youtube,
                            "Podcasts".podcastid as podcastid
                        FROM "SavedEpisodes"
                        INNER JOIN "Episodes" ON "SavedEpisodes".episodeid = "Episodes".episodeid
                        INNER JOIN "Podcasts" ON "Episodes".podcastid = "Podcasts".podcastid
                        LEFT JOIN "Users" ON "Podcasts".userid = "Users".userid
                        LEFT JOIN "UserEpisodeHistory" ON
                            "SavedEpisodes".episodeid = "UserEpisodeHistory".episodeid
                            AND "UserEpisodeHistory".userid = $1
                        LEFT JOIN "EpisodeQueue" ON
                            "SavedEpisodes".episodeid = "EpisodeQueue".episodeid
                            AND "EpisodeQueue".userid = $2
                            AND "EpisodeQueue".is_youtube = FALSE
                        LEFT JOIN "DownloadedEpisodes" ON
                            "SavedEpisodes".episodeid = "DownloadedEpisodes".episodeid
                            AND "DownloadedEpisodes".userid = $3
                        WHERE "SavedEpisodes".userid = $4

                        UNION ALL

                        SELECT
                            "Podcasts".podcastname as podcastname,
                            "YouTubeVideos".videotitle as episodetitle,
                            "YouTubeVideos".publishedat as episodepubdate,
                            "YouTubeVideos".videodescription as episodedescription,
                            "YouTubeVideos".videoid as episodeid,
                            CASE 
                                WHEN "Podcasts".usepodcastcoverscustomized = TRUE AND "Podcasts".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                WHEN "Users".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                ELSE "YouTubeVideos".thumbnailurl
                            END as episodeartwork,
                            "YouTubeVideos".videourl as episodeurl,
                            "YouTubeVideos".duration as episodeduration,
                            "Podcasts".websiteurl as websiteurl,
                            "YouTubeVideos".listenposition as listenduration,
                            "YouTubeVideos".completed as completed,
                            TRUE as saved,
                            CASE WHEN "EpisodeQueue".episodeid IS NOT NULL AND "EpisodeQueue".is_youtube = TRUE THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN "DownloadedVideos".videoid IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            TRUE as is_youtube,
                            "Podcasts".podcastid as podcastid
                        FROM "SavedVideos"
                        INNER JOIN "YouTubeVideos" ON "SavedVideos".videoid = "YouTubeVideos".videoid
                        INNER JOIN "Podcasts" ON "YouTubeVideos".podcastid = "Podcasts".podcastid
                        LEFT JOIN "Users" ON "Podcasts".userid = "Users".userid
                        LEFT JOIN "EpisodeQueue" ON
                            "SavedVideos".videoid = "EpisodeQueue".episodeid
                            AND "EpisodeQueue".userid = $5
                            AND "EpisodeQueue".is_youtube = TRUE
                        LEFT JOIN "DownloadedVideos" ON
                            "SavedVideos".videoid = "DownloadedVideos".videoid
                            AND "DownloadedVideos".userid = $6
                        WHERE "SavedVideos".userid = $7
                    ) combined
                    ORDER BY episodepubdate DESC"#
                )
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .fetch_all(pool)
                .await?;

                let mut episodes = Vec::new();
                for row in rows {
                    episodes.push(crate::models::SavedEpisode {
                        episodetitle: row.try_get("episodetitle")?,
                        podcastname: row.try_get("podcastname")?,
                        episodepubdate: {
                            let naive = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                            naive.format("%Y-%m-%dT%H:%M:%S").to_string()
                        },
                        episodedescription: row.try_get("episodedescription")?,
                        episodeartwork: row.try_get("episodeartwork")?,
                        episodeurl: row.try_get("episodeurl")?,
                        episodeduration: row.try_get("episodeduration")?,
                        listenduration: row.try_get("listenduration").ok(),
                        episodeid: row.try_get("episodeid")?,
                        websiteurl: row.try_get("websiteurl")?,
                        completed: row.try_get("completed")?,
                        saved: row.try_get("saved")?,
                        queued: row.try_get("queued")?,
                        downloaded: row.try_get("downloaded")?,
                        is_youtube: row.try_get("is_youtube")?,
                        podcastid: row.try_get("podcastid").ok(),
                    });
                }
                Ok(episodes)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(
                    "SELECT * FROM (
                        SELECT
                            Podcasts.PodcastName as podcastname,
                            Episodes.EpisodeTitle as episodetitle,
                            Episodes.EpisodePubDate as episodepubdate,
                            Episodes.EpisodeDescription as episodedescription,
                            Episodes.EpisodeID as episodeid,
                            CASE 
                                WHEN Podcasts.UsePodcastCoversCustomized = 1 AND Podcasts.UsePodcastCovers = 1 THEN Podcasts.ArtworkURL
                                WHEN Users.UsePodcastCovers = 1 THEN Podcasts.ArtworkURL
                                ELSE Episodes.EpisodeArtwork
                            END as episodeartwork,
                            Episodes.EpisodeURL as episodeurl,
                            Episodes.EpisodeDuration as episodeduration,
                            Podcasts.WebsiteURL as websiteurl,
                            UserEpisodeHistory.ListenDuration as listenduration,
                            Episodes.Completed as completed,
                            TRUE as saved,
                            CASE WHEN EpisodeQueue.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN DownloadedEpisodes.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            FALSE as is_youtube,
                            Podcasts.PodcastID as podcastid
                        FROM SavedEpisodes
                        INNER JOIN Episodes ON SavedEpisodes.EpisodeID = Episodes.EpisodeID
                        INNER JOIN Podcasts ON Episodes.PodcastID = Podcasts.PodcastID
                        LEFT JOIN Users ON Podcasts.UserID = Users.UserID
                        LEFT JOIN UserEpisodeHistory ON
                            SavedEpisodes.EpisodeID = UserEpisodeHistory.EpisodeID
                            AND UserEpisodeHistory.UserID = ?
                        LEFT JOIN EpisodeQueue ON
                            SavedEpisodes.EpisodeID = EpisodeQueue.EpisodeID
                            AND EpisodeQueue.UserID = ?
                            AND EpisodeQueue.is_youtube = FALSE
                        LEFT JOIN DownloadedEpisodes ON
                            SavedEpisodes.EpisodeID = DownloadedEpisodes.EpisodeID
                            AND DownloadedEpisodes.UserID = ?
                        WHERE SavedEpisodes.UserID = ?

                        UNION ALL

                        SELECT
                            Podcasts.PodcastName as podcastname,
                            YouTubeVideos.VideoTitle as episodetitle,
                            YouTubeVideos.PublishedAt as episodepubdate,
                            YouTubeVideos.VideoDescription as episodedescription,
                            YouTubeVideos.VideoID as episodeid,
                            CASE 
                                WHEN Podcasts.UsePodcastCoversCustomized = 1 AND Podcasts.UsePodcastCovers = 1 THEN Podcasts.ArtworkURL
                                WHEN Users.UsePodcastCovers = 1 THEN Podcasts.ArtworkURL
                                ELSE YouTubeVideos.ThumbnailURL
                            END as episodeartwork,
                            YouTubeVideos.VideoURL as episodeurl,
                            YouTubeVideos.Duration as episodeduration,
                            Podcasts.WebsiteURL as websiteurl,
                            YouTubeVideos.ListenPosition as listenduration,
                            YouTubeVideos.Completed as completed,
                            TRUE as saved,
                            CASE WHEN EpisodeQueue.EpisodeID IS NOT NULL AND EpisodeQueue.is_youtube = TRUE THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN DownloadedVideos.VideoID IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            TRUE as is_youtube,
                            Podcasts.PodcastID as podcastid
                        FROM SavedVideos
                        INNER JOIN YouTubeVideos ON SavedVideos.VideoID = YouTubeVideos.VideoID
                        INNER JOIN Podcasts ON YouTubeVideos.PodcastID = Podcasts.PodcastID
                        LEFT JOIN Users ON Podcasts.UserID = Users.UserID
                        LEFT JOIN EpisodeQueue ON
                            SavedVideos.VideoID = EpisodeQueue.EpisodeID
                            AND EpisodeQueue.UserID = ?
                            AND EpisodeQueue.is_youtube = TRUE
                        LEFT JOIN DownloadedVideos ON
                            SavedVideos.VideoID = DownloadedVideos.VideoID
                            AND DownloadedVideos.UserID = ?
                        WHERE SavedVideos.UserID = ?
                    ) combined
                    ORDER BY episodepubdate DESC"
                )
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .fetch_all(pool)
                .await?;

                let mut episodes = Vec::new();
                for row in rows {
                    episodes.push(crate::models::SavedEpisode {
                        episodetitle: row.try_get("episodetitle")?,
                        podcastname: row.try_get("podcastname")?,
                        episodepubdate: {
                            let naive = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                            naive.format("%Y-%m-%dT%H:%M:%S").to_string()
                        },
                        episodedescription: row.try_get("episodedescription")?,
                        episodeartwork: row.try_get("episodeartwork")?,
                        episodeurl: row.try_get("episodeurl")?,
                        episodeduration: row.try_get("episodeduration")?,
                        listenduration: row.try_get("listenduration").ok(),
                        episodeid: row.try_get("episodeid")?,
                        websiteurl: row.try_get("websiteurl")?,
                        completed: row.try_get("completed")?,
                        saved: true,
                        queued: row.try_get("queued")?,
                        downloaded: row.try_get("downloaded")?,
                        is_youtube: row.try_get("is_youtube")?,
                        podcastid: row.try_get("podcastid").ok(),
                    });
                }
                Ok(episodes)
            }
        }
    }

    // Record podcast history - matches Python record_podcast_history function
    pub async fn record_podcast_history(&self, episode_id: i32, user_id: i32, episode_pos: f32, is_youtube: bool) -> AppResult<()> {
        let listen_duration = (episode_pos * 100.0) as i32; // Convert position to duration

        match self {
            DatabasePool::Postgres(pool) => {
                if is_youtube {
                    // Insert or update video history
                    sqlx::query(
                        r#"INSERT INTO "UserVideoHistory" ("videoid", "userid", "listenduration", "listendate")
                           VALUES ($1, $2, $3, NOW())
                           ON CONFLICT ("videoid", "userid") 
                           DO UPDATE SET "listenduration" = $3, "listendate" = NOW()"#
                    )
                    .bind(episode_id)
                    .bind(user_id)
                    .bind(listen_duration)
                    .execute(pool)
                    .await?;
                } else {
                    // Insert or update episode history
                    sqlx::query(
                        r#"INSERT INTO "UserEpisodeHistory" ("episodeid", "userid", "listenduration", "listendate")
                           VALUES ($1, $2, $3, NOW())
                           ON CONFLICT ("episodeid", "userid") 
                           DO UPDATE SET "listenduration" = $3, "listendate" = NOW()"#
                    )
                    .bind(episode_id)
                    .bind(user_id)
                    .bind(listen_duration)
                    .execute(pool)
                    .await?;
                }
                Ok(())
            }
            DatabasePool::MySQL(pool) => {
                if is_youtube {
                    // Insert or update video history
                    sqlx::query(
                        "INSERT INTO UserVideoHistory (VideoID, UserID, ListenDuration, ListenDate)
                         VALUES (?, ?, ?, NOW())
                         ON DUPLICATE KEY UPDATE ListenDuration = ?, ListenDate = NOW()"
                    )
                    .bind(episode_id)
                    .bind(user_id)
                    .bind(listen_duration)
                    .bind(listen_duration)
                    .execute(pool)
                    .await?;
                } else {
                    // Insert or update episode history
                    sqlx::query(
                        "INSERT INTO UserEpisodeHistory (EpisodeID, UserID, ListenDuration, ListenDate)
                         VALUES (?, ?, ?, NOW())
                         ON DUPLICATE KEY UPDATE ListenDuration = ?, ListenDate = NOW()"
                    )
                    .bind(episode_id)
                    .bind(user_id)
                    .bind(listen_duration)
                    .bind(listen_duration)
                    .execute(pool)
                    .await?;
                }
                Ok(())
            }
        }
    }

    // Get user history - matches Python user_history function
    pub async fn get_user_history(&self, user_id: i32) -> AppResult<Vec<crate::models::HistoryEpisode>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT 
                        e."EpisodeTitle" as episodetitle,
                        p."PodcastName" as podcastname,
                        e."EpisodePubDate" as episodepubdate,
                        e."EpisodeDescription" as episodedescription,
                        e."EpisodeArtwork" as episodeartwork,
                        e."EpisodeURL" as episodeurl,
                        e."EpisodeDuration" as episodeduration,
                        ueh."ListenDuration" as listenduration,
                        e."EpisodeID" as episodeid,
                        CASE WHEN ueh."ListenDuration" >= (e."EpisodeDuration" * 0.95) THEN true ELSE false END as completed,
                        ueh."ListenDate" as listendate,
                        false as is_youtube
                    FROM "UserEpisodeHistory" ueh
                    JOIN "Episodes" e ON ueh."EpisodeID" = e."EpisodeID"
                    JOIN "Podcasts" p ON e."PodcastID" = p."PodcastID"
                    WHERE ueh."UserID" = $1 AND p."UserID" = $1
                    
                    UNION ALL
                    
                    SELECT 
                        yv."VideoTitle" as episodetitle,
                        yc."ChannelName" as podcastname,
                        yv."VideoUploadDate" as episodepubdate,
                        yv."VideoDescription" as episodedescription,
                        yv."VideoThumbnail" as episodeartwork,
                        yv."VideoURL" as episodeurl,
                        COALESCE(yv."VideoDuration", 0) as episodeduration,
                        uvh."ListenDuration" as listenduration,
                        yv."VideoID" as episodeid,
                        CASE WHEN uvh."ListenDuration" >= (yv."VideoDuration" * 0.95) THEN true ELSE false END as completed,
                        uvh."ListenDate" as listendate,
                        true as is_youtube
                    FROM "UserVideoHistory" uvh
                    JOIN "YouTubeVideos" yv ON uvh."VideoID" = yv."VideoID"
                    JOIN "YouTubeChannels" yc ON yv."ChannelID" = yc."ChannelID"
                    WHERE uvh."UserID" = $1 AND uvh."ListenDuration" > 0
                    
                    ORDER BY listendate DESC"#
                )
                .bind(user_id)
                .fetch_all(pool)
                .await?;

                let mut episodes = Vec::new();
                for row in rows {
                    episodes.push(crate::models::HistoryEpisode {
                        episodetitle: row.try_get("episodetitle")?,
                        podcastname: row.try_get("podcastname")?,
                        episodepubdate: {
                            let naive = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                            naive.format("%Y-%m-%dT%H:%M:%S").to_string()
                        },
                        episodedescription: row.try_get("episodedescription")?,
                        episodeartwork: row.try_get("episodeartwork")?,
                        episodeurl: row.try_get("episodeurl")?,
                        episodeduration: row.try_get("episodeduration")?,
                        listenduration: row.try_get("listenduration").ok(),
                        episodeid: row.try_get("episodeid")?,
                        completed: row.try_get("completed")?,
                        listendate: row.try_get("listendate").ok(),
                        is_youtube: row.try_get("is_youtube")?,
                    });
                }
                Ok(episodes)
            }
            DatabasePool::MySQL(pool) => {
                // Similar MySQL implementation
                let rows = sqlx::query(
                    "SELECT 
                        e.EpisodeTitle as episodetitle,
                        p.PodcastName as podcastname,
                        e.EpisodePubDate as episodepubdate,
                        e.EpisodeDescription as episodedescription,
                        e.EpisodeArtwork as episodeartwork,
                        e.EpisodeURL as episodeurl,
                        e.EpisodeDuration as episodeduration,
                        ueh.ListenDuration as listenduration,
                        e.EpisodeID as episodeid,
                        CASE WHEN ueh.ListenDuration >= (e.EpisodeDuration * 0.95) THEN true ELSE false END as completed,
                        ueh.ListenDate as listendate,
                        false as is_youtube
                    FROM UserEpisodeHistory ueh
                    JOIN Episodes e ON ueh.EpisodeID = e.EpisodeID
                    JOIN Podcasts p ON e.PodcastID = p.PodcastID
                    WHERE ueh.UserID = ? AND p.UserID = ?
                    ORDER BY listendate DESC"
                )
                .bind(user_id)
                .bind(user_id)
                .fetch_all(pool)
                .await?;

                let mut episodes = Vec::new();
                for row in rows {
                    episodes.push(crate::models::HistoryEpisode {
                        episodetitle: row.try_get("episodetitle")?,
                        podcastname: row.try_get("podcastname")?,
                        episodepubdate: {
                            let naive = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                            naive.format("%Y-%m-%dT%H:%M:%S").to_string()
                        },
                        episodedescription: row.try_get("episodedescription")?,
                        episodeartwork: row.try_get("episodeartwork")?,
                        episodeurl: row.try_get("episodeurl")?,
                        episodeduration: row.try_get("episodeduration")?,
                        listenduration: row.try_get("listenduration").ok(),
                        episodeid: row.try_get("episodeid")?,
                        completed: row.try_get("completed")?,
                        listendate: row.try_get("listendate").ok(),
                        is_youtube: row.try_get("is_youtube")?,
                    });
                }
                Ok(episodes)
            }
        }
    }

    // Get self-service status - matches Python self_service_status function
    pub async fn get_self_service_status(&self) -> AppResult<SelfServiceStatus> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Get self-service status
                let self_service_row = sqlx::query(r#"SELECT selfserviceuser FROM "AppSettings" WHERE selfserviceuser = true"#)
                    .fetch_optional(pool)
                    .await?;
                
                let self_service_enabled = self_service_row.is_some();
                
                // Check if admin exists (excluding background_tasks user)
                let admin_row = sqlx::query(r#"SELECT COUNT(*) as count FROM "Users" WHERE isadmin = true AND username != 'background_tasks'"#)
                    .fetch_one(pool)
                    .await?;
                
                let admin_count: i64 = admin_row.try_get("count")?;
                let admin_exists = admin_count > 0;
                
                Ok(SelfServiceStatus {
                    status: self_service_enabled,
                    admin_exists,
                })
            }
            DatabasePool::MySQL(pool) => {
                // Get self-service status
                let self_service_row = sqlx::query("SELECT SelfServiceUser FROM AppSettings WHERE SelfServiceUser = 1")
                    .fetch_optional(pool)
                    .await?;
                
                let self_service_enabled = self_service_row.is_some();
                
                // Check if admin exists (excluding background_tasks user)
                let admin_row = sqlx::query("SELECT COUNT(*) as count FROM Users WHERE IsAdmin = 1 AND Username != 'background_tasks'")
                    .fetch_one(pool)
                    .await?;
                
                let admin_count: i64 = admin_row.try_get("count")?;
                let admin_exists = admin_count > 0;
                
                Ok(SelfServiceStatus {
                    status: self_service_enabled,
                    admin_exists,
                })
            }
        }
    }

    // Get public OIDC providers - matches Python get_public_oidc_providers function
    pub async fn get_public_oidc_providers(&self) -> AppResult<Vec<PublicOidcProvider>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT
                        providerid,
                        providername,
                        clientid,
                        authorizationurl,
                        scope,
                        buttoncolor,
                        buttontext,
                        buttontextcolor,
                        iconsvg
                    FROM "OIDCProviders"
                    WHERE enabled = true"#
                )
                .fetch_all(pool)
                .await?;
                
                let mut providers = Vec::new();
                for row in rows {
                    providers.push(PublicOidcProvider {
                        provider_id: row.try_get("providerid")?,
                        provider_name: row.try_get("providername")?,
                        client_id: row.try_get("clientid")?,
                        authorization_url: row.try_get("authorizationurl")?,
                        scope: row.try_get("scope")?,
                        button_color: row.try_get("buttoncolor")?,
                        button_text: row.try_get("buttontext")?,
                        button_text_color: row.try_get("buttontextcolor")?,
                        icon_svg: row.try_get("iconsvg").ok(),
                    });
                }
                Ok(providers)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(
                    "SELECT
                        ProviderID as providerid,
                        ProviderName as providername,
                        ClientID as clientid,
                        AuthorizationURL as authorizationurl,
                        Scope as scope,
                        ButtonColor as buttoncolor,
                        ButtonText as buttontext,
                        ButtonTextColor as buttontextcolor,
                        IconSVG as iconsvg
                    FROM OIDCProviders
                    WHERE Enabled = true"
                )
                .fetch_all(pool)
                .await?;
                
                let mut providers = Vec::new();
                for row in rows {
                    providers.push(PublicOidcProvider {
                        provider_id: row.try_get("providerid")?,
                        provider_name: row.try_get("providername")?,
                        client_id: row.try_get("clientid")?,
                        authorization_url: row.try_get("authorizationurl")?,
                        scope: row.try_get("scope")?,
                        button_color: row.try_get("buttoncolor")?,
                        button_text: row.try_get("buttontext")?,
                        button_text_color: row.try_get("buttontextcolor")?,
                        icon_svg: row.try_get("iconsvg").ok(),
                    });
                }
                Ok(providers)
            }
        }
    }

    // Add admin user - matches Python add_admin_user function
    pub async fn add_admin_user(&self, fullname: &str, username: &str, email: &str, hashed_password: &str) -> AppResult<i32> {
        match self {
            DatabasePool::Postgres(pool) => {
                let mut tx = pool.begin().await?;
                
                // Insert the admin user
                let user_row = sqlx::query(
                    r#"WITH inserted_user AS (
                        INSERT INTO "Users"
                        (fullname, username, email, hashed_pw, isadmin)
                        VALUES ($1, $2, $3, $4, true)
                        ON CONFLICT (username) DO NOTHING
                        RETURNING userid
                    )
                    SELECT userid FROM inserted_user
                    UNION ALL
                    SELECT userid FROM "Users" WHERE username = $5
                    LIMIT 1"#
                )
                .bind(fullname)
                .bind(username)
                .bind(email)
                .bind(hashed_password)
                .bind(username)
                .fetch_one(&mut *tx)
                .await?;
                
                let user_id: i32 = user_row.try_get("userid")?;
                
                // Add user settings
                sqlx::query(
                    r#"INSERT INTO "UserSettings" (userid, theme) VALUES ($1, $2)
                       ON CONFLICT (userid) DO NOTHING"#
                )
                .bind(user_id)
                .bind("Nordic")
                .execute(&mut *tx)
                .await?;
                
                // Add user stats
                sqlx::query(
                    r#"INSERT INTO "UserStats" (userid) VALUES ($1)
                       ON CONFLICT (userid) DO NOTHING"#
                )
                .bind(user_id)
                .execute(&mut *tx)
                .await?;
                
                // Create API key for the user
                let api_key = self.generate_api_key();
                sqlx::query(
                    r#"INSERT INTO "APIKeys" (userid, apikey) VALUES ($1, $2)"#
                )
                .bind(user_id)
                .bind(&api_key)
                .execute(&mut *tx)
                .await?;
                
                tx.commit().await?;
                Ok(user_id)
            }
            DatabasePool::MySQL(pool) => {
                let mut tx = pool.begin().await?;
                
                // Insert the admin user
                let result = sqlx::query(
                    "INSERT INTO Users (Fullname, Username, Email, Hashed_PW, IsAdmin) VALUES (?, ?, ?, ?, 1)"
                )
                .bind(fullname)
                .bind(username)
                .bind(email)
                .bind(hashed_password)
                .execute(&mut *tx)
                .await?;
                
                let user_id = result.last_insert_id() as i32;
                
                // Add user settings
                sqlx::query(
                    "INSERT INTO UserSettings (UserID, Theme) VALUES (?, ?)"
                )
                .bind(user_id)
                .bind("Nordic")
                .execute(&mut *tx)
                .await?;
                
                // Add user stats
                sqlx::query(
                    "INSERT INTO UserStats (UserID) VALUES (?)"
                )
                .bind(user_id)
                .execute(&mut *tx)
                .await?;
                
                // Create API key for the user
                let api_key = self.generate_api_key();
                sqlx::query(
                    "INSERT INTO APIKeys (UserID, APIKey) VALUES (?, ?)"
                )
                .bind(user_id)
                .bind(&api_key)
                .execute(&mut *tx)
                .await?;
                
                tx.commit().await?;
                Ok(user_id)
            }
        }
    }

    // Check if admin exists - matches Python check_admin_exists function
    pub async fn check_admin_exists(&self) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT COUNT(*) as count FROM "Users" WHERE isadmin = true AND username != 'background_tasks'"#)
                    .fetch_one(pool)
                    .await?;
                
                let count: i64 = row.try_get("count")?;
                Ok(count > 0)
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT COUNT(*) as count FROM Users WHERE IsAdmin = 1 AND Username != 'background_tasks'")
                    .fetch_one(pool)
                    .await?;
                
                let count: i64 = row.try_get("count")?;
                Ok(count > 0)
            }
        }
    }

    // Generate API key - matches Python create_api_key function
    fn generate_api_key(&self) -> String {
        use rand::Rng;
        const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        let mut rng = rand::rng();
        (0..64)
            .map(|_| {
                let idx = rng.random_range(0..CHARSET.len());
                CHARSET[idx] as char
            })
            .collect()
    }


    // Get theme - matches Python get_theme function
    pub async fn get_theme(&self, user_id: i32) -> AppResult<String> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT theme FROM "UserSettings" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                match row {
                    Some(row) => Ok(row.try_get("theme").unwrap_or_else(|_| "Nordic".to_string())),
                    None => Ok("Nordic".to_string()),
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT Theme FROM UserSettings WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                match row {
                    Some(row) => Ok(row.try_get("Theme").unwrap_or_else(|_| "Nordic".to_string())),
                    None => Ok("Nordic".to_string()),
                }
            }
        }
    }

    // First login done - matches Python first_login_done function
    pub async fn first_login_done(&self, user_id: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT firstlogin FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                match row {
                    Some(row) => Ok(row.try_get("firstlogin").unwrap_or(false)),
                    None => Err(AppError::not_found("User not found")),
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT FirstLogin FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                match row {
                    Some(row) => Ok(row.try_get("FirstLogin").unwrap_or(false)),
                    None => Err(AppError::not_found("User not found")),
                }
            }
        }
    }

    // Add episodes - matches Python add_episodes function exactly
    pub async fn add_episodes(
        &self,
        podcast_id: i32,
        feed_url: &str,
        artwork_url: &str,
        _auto_download: bool,
        username: Option<&str>,
        password: Option<&str>,
    ) -> AppResult<Option<i32>> {
        // Fetch the RSS feed
        let content = self.try_fetch_feed(feed_url, username, password).await?;
        
        // Parse the RSS feed - enable duration estimation for initial podcast adding
        let episodes = self.parse_rss_feed_with_options(&content, podcast_id, artwork_url, true).await?;
        
        let mut first_episode_id = None;
        
        for episode in episodes {
            // Check if episode already exists by EITHER title OR url
            // This handles cases where feed maintainers edit episodes
            let existing_episode_id = match self {
                DatabasePool::Postgres(pool) => {
                    let row = sqlx::query(r#"SELECT episodeid FROM "Episodes" WHERE podcastid = $1 AND (episodetitle = $2 OR episodeurl = $3)"#)
                        .bind(podcast_id)
                        .bind(&episode.title)
                        .bind(&episode.url)
                        .fetch_optional(pool)
                        .await?;
                    row.map(|r| r.try_get::<i32, _>("episodeid").ok()).flatten()
                }
                DatabasePool::MySQL(pool) => {
                    let row = sqlx::query("SELECT EpisodeID FROM Episodes WHERE PodcastID = ? AND (EpisodeTitle = ? OR EpisodeURL = ?)")
                        .bind(podcast_id)
                        .bind(&episode.title)
                        .bind(&episode.url)
                        .fetch_optional(pool)
                        .await?;
                    row.map(|r| r.try_get::<i32, _>("EpisodeID").ok()).flatten()
                }
            };

            if let Some(episode_id) = existing_episode_id {
                // Episode already exists (by title or URL) - UPDATE it with new metadata
                match self {
                    DatabasePool::Postgres(pool) => {
                        sqlx::query(
                            r#"UPDATE "Episodes"
                               SET episodetitle = $1, episodedescription = $2, episodeurl = $3,
                                   episodeartwork = $4, episodepubdate = $5, episodeduration = $6
                               WHERE episodeid = $7"#
                        )
                        .bind(&episode.title)
                        .bind(&episode.description)
                        .bind(&episode.url)
                        .bind(&episode.artwork_url)
                        .bind(&episode.pub_date)
                        .bind(episode.duration)
                        .bind(episode_id)
                        .execute(pool)
                        .await?;
                    }
                    DatabasePool::MySQL(pool) => {
                        sqlx::query(
                            "UPDATE Episodes
                             SET EpisodeTitle = ?, EpisodeDescription = ?, EpisodeURL = ?,
                                 EpisodeArtwork = ?, EpisodePubDate = ?, EpisodeDuration = ?
                             WHERE EpisodeID = ?"
                        )
                        .bind(&episode.title)
                        .bind(&episode.description)
                        .bind(&episode.url)
                        .bind(&episode.artwork_url)
                        .bind(&episode.pub_date)
                        .bind(episode.duration)
                        .bind(episode_id)
                        .execute(pool)
                        .await?;
                    }
                }
                // Skip to next episode - don't insert or send notification for updates
                continue;
            }

            // Insert new episode (neither title nor URL exists)
            let episode_id = match self {
                DatabasePool::Postgres(pool) => {
                    let row = sqlx::query(
                        r#"INSERT INTO "Episodes" 
                           (podcastid, episodetitle, episodedescription, episodeurl, episodeartwork, episodepubdate, episodeduration)
                           VALUES ($1, $2, $3, $4, $5, $6, $7)
                           RETURNING episodeid"#
                    )
                    .bind(podcast_id)
                    .bind(&episode.title)
                    .bind(&episode.description)
                    .bind(&episode.url)
                    .bind(&episode.artwork_url)
                    .bind(&episode.pub_date)
                    .bind(episode.duration)
                    .fetch_one(pool)
                    .await?;
                    
                    row.try_get::<i32, _>("episodeid")?
                }
                DatabasePool::MySQL(pool) => {
                    let result = sqlx::query(
                        "INSERT INTO Episodes 
                         (PodcastID, EpisodeTitle, EpisodeDescription, EpisodeURL, EpisodeArtwork, EpisodePubDate, EpisodeDuration)
                         VALUES (?, ?, ?, ?, ?, ?, ?)"
                    )
                    .bind(podcast_id)
                    .bind(&episode.title)
                    .bind(&episode.description)
                    .bind(&episode.url)
                    .bind(&episode.artwork_url)
                    .bind(&episode.pub_date)
                    .bind(episode.duration)
                    .execute(pool)
                    .await?;
                    
                    result.last_insert_id() as i32
                }
            };
            
            // Send notification for new episode - matches Python implementation exactly
            if let Err(e) = self.check_and_send_notification(podcast_id, &episode.title).await {
                tracing::warn!("Failed to send notification for episode '{}': {}", episode.title, e);
            }
            
            // Set first episode ID if not set
            if first_episode_id.is_none() {
                first_episode_id = Some(episode_id);
            }
        }
        
        // Update episode count
        self.update_episode_count(podcast_id).await?;
        
        // Get the actual first episode ID (earliest by pub date)
        let first_id = self.get_first_episode_id(podcast_id, false).await?;
        
        Ok(first_id)
    }

    // New function to add episodes and return list of newly inserted episodes
    // This matches the Python add_episodes websocket=True functionality
    pub async fn add_episodes_with_new_list(
        &self,
        podcast_id: i32,
        feed_url: &str,
        artwork_url: &str,
        username: Option<&str>,
        password: Option<&str>,
    ) -> AppResult<Vec<crate::handlers::podcasts::Episode>> {
        // Fetch the RSS feed
        let content = self.try_fetch_feed(feed_url, username, password).await?;
        
        // Parse the RSS feed
        let episodes = self.parse_rss_feed(&content, podcast_id, artwork_url).await?;
        
        let mut new_episodes = Vec::new();
        
        for mut episode in episodes {
            // Check if episode already exists by EITHER title OR url
            // This handles cases where feed maintainers edit episodes
            let existing_episode_id = match self {
                DatabasePool::Postgres(pool) => {
                    let row = sqlx::query(r#"SELECT episodeid FROM "Episodes" WHERE podcastid = $1 AND (episodetitle = $2 OR episodeurl = $3)"#)
                        .bind(podcast_id)
                        .bind(&episode.title)
                        .bind(&episode.url)
                        .fetch_optional(pool)
                        .await?;
                    row.map(|r| r.try_get::<i32, _>("episodeid").ok()).flatten()
                }
                DatabasePool::MySQL(pool) => {
                    let row = sqlx::query("SELECT EpisodeID FROM Episodes WHERE PodcastID = ? AND (EpisodeTitle = ? OR EpisodeURL = ?)")
                        .bind(podcast_id)
                        .bind(&episode.title)
                        .bind(&episode.url)
                        .fetch_optional(pool)
                        .await?;
                    row.map(|r| r.try_get::<i32, _>("EpisodeID").ok()).flatten()
                }
            };

            if let Some(episode_id) = existing_episode_id {
                // Episode already exists (by title or URL) - UPDATE it with new metadata
                match self {
                    DatabasePool::Postgres(pool) => {
                        sqlx::query(
                            r#"UPDATE "Episodes"
                               SET episodetitle = $1, episodedescription = $2, episodeurl = $3,
                                   episodeartwork = $4, episodepubdate = $5, episodeduration = $6
                               WHERE episodeid = $7"#
                        )
                        .bind(&episode.title)
                        .bind(&episode.description)
                        .bind(&episode.url)
                        .bind(&episode.artwork_url)
                        .bind(&episode.pub_date)
                        .bind(episode.duration)
                        .bind(episode_id)
                        .execute(pool)
                        .await?;
                    }
                    DatabasePool::MySQL(pool) => {
                        sqlx::query(
                            "UPDATE Episodes
                             SET EpisodeTitle = ?, EpisodeDescription = ?, EpisodeURL = ?,
                                 EpisodeArtwork = ?, EpisodePubDate = ?, EpisodeDuration = ?
                             WHERE EpisodeID = ?"
                        )
                        .bind(&episode.title)
                        .bind(&episode.description)
                        .bind(&episode.url)
                        .bind(&episode.artwork_url)
                        .bind(&episode.pub_date)
                        .bind(episode.duration)
                        .bind(episode_id)
                        .execute(pool)
                        .await?;
                    }
                }
                // Skip to next episode - don't add to new_episodes list for updates
                continue;
            }

            // This is a NEW episode - estimate duration if missing
            if episode.duration == 0 {
                let audio_url = &episode.url;
                if !audio_url.is_empty() {
                    if let Ok(handle) = tokio::runtime::Handle::try_current() {
                        if let Some(estimated_duration) = tokio::task::block_in_place(|| {
                            handle.block_on(self.estimate_duration_from_audio_url_async(audio_url))
                        }) {
                            episode.duration = estimated_duration;
                            println!("Estimated duration {} seconds for new episode: {}", estimated_duration, episode.title);
                        }
                    }
                }
            }
            
            // Insert new episode
            let episode_id = match self {
                DatabasePool::Postgres(pool) => {
                    let row = sqlx::query(
                        r#"INSERT INTO "Episodes" 
                           (podcastid, episodetitle, episodedescription, episodeurl, episodeartwork, episodepubdate, episodeduration)
                           VALUES ($1, $2, $3, $4, $5, $6, $7)
                           RETURNING episodeid"#
                    )
                    .bind(podcast_id)
                    .bind(&episode.title)
                    .bind(&episode.description)
                    .bind(&episode.url)
                    .bind(&episode.artwork_url)
                    .bind(&episode.pub_date)
                    .bind(episode.duration)
                    .fetch_one(pool)
                    .await?;
                    
                    row.try_get::<i32, _>("episodeid")?
                }
                DatabasePool::MySQL(pool) => {
                    let result = sqlx::query(
                        "INSERT INTO Episodes 
                         (PodcastID, EpisodeTitle, EpisodeDescription, EpisodeURL, EpisodeArtwork, EpisodePubDate, EpisodeDuration)
                         VALUES (?, ?, ?, ?, ?, ?, ?)"
                    )
                    .bind(podcast_id)
                    .bind(&episode.title)
                    .bind(&episode.description)
                    .bind(&episode.url)
                    .bind(&episode.artwork_url)
                    .bind(&episode.pub_date)
                    .bind(episode.duration)
                    .execute(pool)
                    .await?;
                    
                    result.last_insert_id() as i32
                }
            };
            
            // Send notification for new episode - matches Python implementation exactly
            if let Err(e) = self.check_and_send_notification(podcast_id, &episode.title).await {
                tracing::warn!("Failed to send notification for episode '{}': {}", episode.title, e);
            }
            
            // Add to new episodes list - this tracks EXACTLY which episodes were just inserted
            new_episodes.push(crate::handlers::podcasts::Episode {
                podcastname: "".to_string(), // Will be filled by the caller if needed
                episodetitle: episode.title,
                episodepubdate: episode.pub_date.format("%Y-%m-%dT%H:%M:%S").to_string(),
                episodedescription: episode.description,
                episodeartwork: episode.artwork_url,
                episodeurl: episode.url,
                episodeduration: episode.duration,
                listenduration: None,
                episodeid: episode_id,
                completed: false,
                saved: false,
                queued: false,
                downloaded: false,
                is_youtube: false,
            });
        }
        
        // Update episode count
        self.update_episode_count(podcast_id).await?;
        
        Ok(new_episodes)
    }

    // Check and send notifications for new episodes - matches Python check_and_send_notification function
    pub async fn check_and_send_notification(&self, podcast_id: i32, episode_title: &str) -> AppResult<bool> {
        use std::time::Duration;
        
        let mut success = false;
        let client = reqwest::Client::builder()
            .timeout(Duration::from_secs(2))
            .build()
            .map_err(|e| AppError::Http(e))?;
        
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT p.notificationsenabled, p.userid, p.podcastname,
                           uns.platform, uns.enabled, uns.ntfytopic, uns.ntfyserverurl,
                           uns.ntfyusername, uns.ntfypassword, uns.ntfyaccesstoken,
                           uns.gotifyurl, uns.gotifytoken
                    FROM "Podcasts" p
                    JOIN "UserNotificationSettings" uns ON p.userid = uns.userid
                    WHERE p.podcastid = $1 AND p.notificationsenabled = true AND uns.enabled = true"#
                )
                .bind(podcast_id)
                .fetch_all(pool)
                .await?;
                
                for result in rows {
                    let podcast_name: String = result.try_get("podcastname")?;
                    let platform: String = result.try_get("platform")?;
                    
                    match platform.as_str() {
                        "ntfy" => {
                            let topic: String = result.try_get("ntfytopic")?;
                            let server_url: String = result.try_get("ntfyserverurl")?;
                            let username: Option<String> = result.try_get("ntfyusername").ok();
                            let password: Option<String> = result.try_get("ntfypassword").ok();
                            let access_token: Option<String> = result.try_get("ntfyaccesstoken").ok();
                            
                            if let Ok(sent) = Self::send_ntfy_notification(&client, &topic, &server_url, username.as_deref(), password.as_deref(), access_token.as_deref(), &podcast_name, episode_title).await {
                                if sent {
                                    success = true;
                                }
                            }
                        }
                        "gotify" => {
                            let url: String = result.try_get("gotifyurl")?;
                            let token: String = result.try_get("gotifytoken")?;
                            
                            if let Ok(sent) = Self::send_gotify_notification(&client, &url, &token, &podcast_name, episode_title).await {
                                if sent {
                                    success = true;
                                }
                            }
                        }
                        _ => {
                            tracing::warn!("Unknown notification platform: {}", platform);
                        }
                    }
                }
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(
                    "SELECT p.NotificationsEnabled, p.UserID, p.PodcastName,
                            uns.Platform, uns.Enabled, uns.NtfyTopic, uns.NtfyServerUrl,
                            uns.NtfyUsername, uns.NtfyPassword, uns.NtfyAccessToken,
                            uns.GotifyUrl, uns.GotifyToken
                     FROM Podcasts p
                     JOIN UserNotificationSettings uns ON p.UserID = uns.UserID
                     WHERE p.PodcastID = ? AND p.NotificationsEnabled = true AND uns.Enabled = true"
                )
                .bind(podcast_id)
                .fetch_all(pool)
                .await?;
                
                for result in rows {
                    let podcast_name: String = result.try_get("PodcastName")?;
                    let platform: String = result.try_get("Platform")?;
                    
                    match platform.as_str() {
                        "ntfy" => {
                            let topic: String = result.try_get("NtfyTopic")?;
                            let server_url: String = result.try_get("NtfyServerUrl")?;
                            let username: Option<String> = result.try_get("NtfyUsername").ok();
                            let password: Option<String> = result.try_get("NtfyPassword").ok();
                            let access_token: Option<String> = result.try_get("NtfyAccessToken").ok();
                            
                            if let Ok(sent) = Self::send_ntfy_notification(&client, &topic, &server_url, username.as_deref(), password.as_deref(), access_token.as_deref(), &podcast_name, episode_title).await {
                                if sent {
                                    success = true;
                                }
                            }
                        }
                        "gotify" => {
                            let url: String = result.try_get("GotifyUrl")?;
                            let token: String = result.try_get("GotifyToken")?;
                            
                            if let Ok(sent) = Self::send_gotify_notification(&client, &url, &token, &podcast_name, episode_title).await {
                                if sent {
                                    success = true;
                                }
                            }
                        }
                        _ => {
                            tracing::warn!("Unknown notification platform: {}", platform);
                        }
                    }
                }
            }
        }
        
        Ok(success)
    }

    // Helper function to send NTFY notification - matches Python send_ntfy_notification function
    async fn send_ntfy_notification(
        client: &reqwest::Client,
        topic: &str,
        server_url: &str,
        username: Option<&str>,
        password: Option<&str>,
        access_token: Option<&str>,
        podcast_name: &str,
        episode_title: &str,
    ) -> AppResult<bool> {
        let url = format!("{}/{}", server_url.trim_end_matches('/'), topic);
        let message = format!("New episode available for {}: {}", podcast_name, episode_title);
        
        let mut request = client
            .post(&url)
            .header("Content-Type", "text/plain")
            .body(message);
        
        // Add authentication if provided
        if let Some(token) = access_token.filter(|t| !t.is_empty()) {
            // Use access token (preferred method)
            request = request.header("Authorization", format!("Bearer {}", token));
        } else if let (Some(user), Some(pass)) = (username.filter(|u| !u.is_empty()), password.filter(|p| !p.is_empty())) {
            // Use username/password basic auth
            request = request.basic_auth(user, Some(pass));
        }
        
        match request.send().await
        {
            Ok(response) => {
                if response.status().is_success() {
                    tracing::info!("Successfully sent NTFY notification to {}", url);
                    Ok(true)
                } else {
                    tracing::warn!("NTFY notification failed with status: {}", response.status());
                    Ok(false)
                }
            }
            Err(e) => {
                tracing::warn!("Failed to send NTFY notification: {}", e);
                Ok(false)
            }
        }
    }

    // Helper function to send Gotify notification - matches Python send_gotify_notification function
    async fn send_gotify_notification(
        client: &reqwest::Client,
        server_url: &str,
        token: &str,
        podcast_name: &str,
        episode_title: &str,
    ) -> AppResult<bool> {
        let url = format!("{}/message?token={}", server_url.trim_end_matches('/'), token);
        let message = format!("New episode available for {}: {}", podcast_name, episode_title);
        
        match client
            .post(&url)
            .header("Content-Type", "application/json")
            .json(&serde_json::json!({
                "message": message,
                "title": "New Podcast Episode"
            }))
            .send()
            .await
        {
            Ok(response) => {
                if response.status().is_success() {
                    tracing::info!("Successfully sent Gotify notification to {}", url);
                    Ok(true)
                } else {
                    tracing::warn!("Gotify notification failed with status: {}", response.status());
                    Ok(false)
                }
            }
            Err(e) => {
                tracing::warn!("Failed to send Gotify notification: {}", e);
                Ok(false)
            }
        }
    }

    // Try to fetch RSS feed - matches Python try_fetch_feed function
    async fn try_fetch_feed(
        &self,
        url: &str,
        username: Option<&str>,
        password: Option<&str>,
    ) -> AppResult<String> {
        println!("try_fetch_feed called with URL: {}", url);
        if let (Some(user), Some(pass)) = (username, password) {
            println!("Using basic authentication for feed: {}", url);
        } else {
            println!("No authentication for feed: {}", url);
        }
        
        // Build HTTP client with proper configuration for container environment
        let client = reqwest::Client::builder()
            .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
            .timeout(std::time::Duration::from_secs(30))
            .build()
            .map_err(|e| {
                println!("Failed to build HTTP client: {}", e);
                AppError::Http(e)
            })?;
            
        let mut request = client.get(url);
        
        if let (Some(user), Some(pass)) = (username, password) {
            println!("Adding basic auth to request for user: {}", user);
            request = request.basic_auth(user, Some(pass));
        }
        
        println!("Sending HTTP request to: {}", url);
        let response = request.send().await.map_err(|e| {
            println!("HTTP request failed for {}: {}", url, e);
            AppError::Http(e)
        })?;
        
        if !response.status().is_success() {
            // If we get a 403, the server might be blocking browser User-Agents
            // Try with a podcast client User-Agent first
            if response.status() == 403 {
                println!("Got 403 Forbidden, trying with podcast client User-Agent");
                
                let podcast_client = reqwest::Client::builder()
                    .user_agent("PinePods/1.0")
                    .timeout(std::time::Duration::from_secs(30))
                    .build()
                    .map_err(|e| {
                        println!("Failed to build podcast client: {}", e);
                        AppError::Http(e)
                    })?;
                
                let mut podcast_request = podcast_client.get(url);
                
                if let (Some(user), Some(pass)) = (username, password) {
                    println!("Adding basic auth to podcast client request for user: {}", user);
                    podcast_request = podcast_request.basic_auth(user, Some(pass));
                }
                
                let podcast_response = podcast_request.send().await.map_err(|e| {
                    println!("Podcast client request failed for {}: {}", url, e);
                    AppError::Http(e)
                })?;
                
                if podcast_response.status().is_success() {
                    println!("Podcast client request succeeded with status: {}", podcast_response.status());
                    return Ok(podcast_response.text().await.map_err(|e| AppError::Http(e))?);
                }
                
                println!("Podcast client request also failed with status: {}", podcast_response.status());
            }
            
            println!("Initial request failed with status: {}, trying alternate URL", response.status());
            // Try alternate URL (www vs non-www)
            let alternate_url = if url.contains("://www.") {
                url.replace("://www.", "://")
            } else {
                url.replace("://", "://www.")
            };
            
            println!("Trying alternate URL: {}", alternate_url);
            let mut alt_request = client.get(&alternate_url);
            
            if let (Some(user), Some(pass)) = (username, password) {
                println!("Adding basic auth to alternate request for user: {}", user);
                alt_request = alt_request.basic_auth(user, Some(pass));
            }
            
            let alt_response = alt_request.send().await.map_err(|e| {
                println!("Alternate HTTP request failed for {}: {}", alternate_url, e);
                AppError::Http(e)
            })?;
            
            if !alt_response.status().is_success() {
                println!("Alternate request also failed with status: {}", alt_response.status());
                return Err(AppError::bad_request(&format!("Feed request failed: HTTP {}", alt_response.status())));
            }
            
            println!("Alternate request succeeded with status: {}", alt_response.status());
            return Ok(alt_response.text().await.map_err(|e| AppError::Http(e))?);
        }
        
        println!("Request succeeded with status: {}", response.status());
        Ok(response.text().await.map_err(|e| AppError::Http(e))?)
    }

    // Custom function to extract raw iTunes durations before feed_rs processes them
    // This is needed because feed_rs incorrectly parses MM:SS durations as seconds only
    fn extract_raw_itunes_durations(content: &str) -> std::collections::HashMap<String, String> {
        use regex::Regex;
        let mut raw_durations = std::collections::HashMap::new();
        
        // Professional-grade regex that handles real-world RSS feeds robustly
        // Uses DOTALL flag (?s) to match across newlines, handles CDATA, whitespace variations
        // Matches both <itunes:duration> and <duration> tags to be comprehensive
        let duration_regex = Regex::new(
            r"(?s)<item[^>]*>.*?<title(?:[^>]*)>(?:\s*<!\[CDATA\[)?\s*([^<\]]+?)(?:\]\]>)?\s*</title>.*?<(?:itunes:)?duration(?:[^>]*)>\s*(?:<!\[CDATA\[)?\s*([^<\]]+?)(?:\]\]>)?\s*</(?:itunes:)?duration>"
        ).unwrap();
        
        for caps in duration_regex.captures_iter(content) {
            if let (Some(title_match), Some(duration_match)) = (caps.get(1), caps.get(2)) {
                let title_str = title_match.as_str().trim();
                let duration_str = duration_match.as_str().trim();
                
                // Skip empty values
                if !title_str.is_empty() && !duration_str.is_empty() {
                    // Decode HTML entities in title to match feed-rs parsed titles
                    let decoded_title = title_str
                        .replace("&apos;", "'")
                        .replace("&quot;", "\"")
                        .replace("&amp;", "&")
                        .replace("&lt;", "<")
                        .replace("&gt;", ">");
                    raw_durations.insert(decoded_title, duration_str.to_string());
                }
            }
        }
        
        // Fallback: try reverse order (duration before title) for edge case XML structures
        let reverse_regex = Regex::new(
            r"(?s)<item[^>]*>.*?<(?:itunes:)?duration(?:[^>]*)>\s*(?:<!\[CDATA\[)?\s*([^<\]]+?)(?:\]\]>)?\s*</(?:itunes:)?duration>.*?<title(?:[^>]*)>(?:\s*<!\[CDATA\[)?\s*([^<\]]+?)(?:\]\]>)?\s*</title>"
        ).unwrap();
        
        for caps in reverse_regex.captures_iter(content) {
            if let (Some(duration_match), Some(title_match)) = (caps.get(1), caps.get(2)) {
                let title_str = title_match.as_str().trim();
                let duration_str = duration_match.as_str().trim();
                
                // Only add if not already found and both values are non-empty
                if !title_str.is_empty() && !duration_str.is_empty() {
                    // Decode HTML entities in title to match feed-rs parsed titles
                    let decoded_title = title_str
                        .replace("&apos;", "'")
                        .replace("&quot;", "\"")
                        .replace("&amp;", "&")
                        .replace("&lt;", "<")
                        .replace("&gt;", ">");
                    if !raw_durations.contains_key(&decoded_title) {
                        raw_durations.insert(decoded_title, duration_str.to_string());
                    }
                }
            }
        }
        
        raw_durations
    }

    // Parse RSS feed - matches Python RSS parsing logic
    async fn parse_rss_feed(
        &self,
        content: &str,
        _podcast_id: i32,
        artwork_url: &str,
    ) -> AppResult<Vec<EpisodeData>> {
        self.parse_rss_feed_with_options(content, _podcast_id, artwork_url, false).await
    }
    
    async fn parse_rss_feed_with_options(
        &self,
        content: &str,
        _podcast_id: i32,
        artwork_url: &str,
        estimate_missing_durations: bool,
    ) -> AppResult<Vec<EpisodeData>> {
        use chrono::Utc;
        use feed_rs::parser;
        use std::collections::HashMap;
        
        // Extract raw iTunes durations before feed_rs processes them
        let raw_durations = Self::extract_raw_itunes_durations(content);
        
        let feed = parser::parse(content.as_bytes())
            .map_err(|e| AppError::Internal(format!("RSS parsing error: {}", e)))?;
        
        let mut episodes = Vec::new();
        
        
        
        for entry in feed.entries {
            // EXACT Python replication: if not all(hasattr(entry, attr) for attr in ["title", "summary", "enclosures"]): continue
            if entry.title.is_none() {
                continue;
            }
            
            let mut episode = EpisodeData {
                title: String::new(),
                description: String::new(),
                url: String::new(),
                artwork_url: artwork_url.to_string(),
                pub_date: Utc::now(),
                duration: 0,
            };
            
            // Create data map to pass to Python-style parsing functions
            let mut episode_data = HashMap::new();
            
            // Extract all data from feed_rs entry
            if let Some(title) = &entry.title {
                episode_data.insert("title".to_string(), title.content.clone());
            }
            
            if let Some(content) = &entry.content {
                if let Some(body) = &content.body {
                    episode_data.insert("content:encoded".to_string(), body.clone());
                }
            }
            
            if let Some(summary) = &entry.summary {
                episode_data.insert("summary".to_string(), summary.content.clone());
            }
            
            // Links for audio/enclosures
            for link in &entry.links {
                if let Some(media_type) = &link.media_type {
                    if media_type.starts_with("audio/") {
                        episode_data.insert("enclosure_url".to_string(), link.href.clone());
                        if let Some(length) = &link.length {
                            episode_data.insert("enclosure_length".to_string(), length.to_string());
                        }
                    }
                }
            }
            
            // Also check for RSS <enclosure> tags that might not be in links
            // feed_rs should put enclosures in the links, but as fallback check media
            
            // Published date - store under both keys for compatibility
            if let Some(published) = &entry.published {
                let date_str = published.to_rfc3339();
                // println!("📅 DEBUG: feed-rs extracted published date: {}", date_str);
                episode_data.insert("published".to_string(), date_str.clone());
                episode_data.insert("pubDate".to_string(), date_str);
            } else {
                // println!("⚠️  DEBUG: No published date found in feed-rs entry for episode: {:?}", entry.title);
            }
            
            // Media extensions
            for media in &entry.media {
                if let Some(duration) = &media.duration {
                    episode_data.insert("duration".to_string(), duration.as_secs().to_string());
                    // Don't use feed_rs processed duration for iTunes - we'll use raw values
                }
                
                // Check if we have a raw iTunes duration for this episode title
                if let Some(title) = &entry.title {
                    if let Some(raw_duration) = raw_durations.get(&title.content) {
                        episode_data.insert("itunes:duration".to_string(), raw_duration.clone());
                    }
                }
                
                for thumbnail in &media.thumbnails {
                    episode_data.insert("media_thumbnail_url".to_string(), thumbnail.image.uri.clone());
                }
                
                for content in &media.content {
                    if let Some(content_type) = &content.content_type {
                        let type_str = content_type.to_string();
                        if type_str.starts_with("audio/") {
                            if let Some(url) = &content.url {
                                // Store as enclosure_url to match Python parsing logic
                                episode_data.insert("enclosure_url".to_string(), url.to_string());
                                if let Some(size) = &content.size {
                                    episode_data.insert("enclosure_length".to_string(), size.to_string());
                                }
                            }
                        } else if type_str.starts_with("image/") {
                            if let Some(url) = &content.url {
                                episode_data.insert("media_image_url".to_string(), url.to_string());
                            }
                        }
                    }
                }
            }
            
            // Debug what we're passing to duration parsing
            
            // Apply all the Python-style parsing logic with ALL fallbacks
            self.apply_python_style_parsing(&mut episode, &episode_data, artwork_url, estimate_missing_durations);
            
            if !episode.title.is_empty() {
                episodes.push(episode);
            }
        }
        
        Ok(episodes)
    }
    
    // Apply Python-style parsing logic with all fallbacks
    fn apply_python_style_parsing(&self, episode: &mut EpisodeData, data: &HashMap<String, String>, default_artwork: &str, estimate_missing_durations: bool) {
        // Title - REQUIRED field with robust cleaning
        if let Some(title) = data.get("title") {
            episode.title = self.clean_and_normalize_title(title);
        }
        // Skip episodes without titles - this is critical like Python version
        if episode.title.is_empty() {
            return;
        }
        
        // EXACT Python replication: parsed_description = entry.get('content', [{}])[0].get('value') or entry.get('summary') or "No description available"
        episode.description = self.parse_description_comprehensive(data);
        
        // EXACT Python replication: parsed_audio_url = entry.enclosures[0].href if entry.enclosures else ""
        episode.url = self.parse_audio_url_comprehensive(data);
        
        // Debug logging for episode URL extraction
        
        // Artwork with comprehensive fallbacks and validation like Python
        episode.artwork_url = self.parse_artwork_comprehensive(data, default_artwork);
        
        // Publication date with extensive format support and timezone handling
        episode.pub_date = self.parse_publication_date_comprehensive(data);
        
        // Duration parsing with extensive fallbacks like Python
        episode.duration = self.parse_duration_comprehensive(data, estimate_missing_durations);
    }
    
    // Clean and normalize titles like Python version
    fn clean_and_normalize_title(&self, title: &str) -> String {
        // HTML entity decoding
        let title = self.decode_html_entities(title);
        
        // HTML tag stripping
        let title = self.strip_html_tags(&title);
        
        // Unicode normalization and whitespace cleaning
        let title = title.trim()
            .split_whitespace()
            .collect::<Vec<_>>()
            .join(" ");
        
        // Truncate if too long (reasonable limit) - use char_indices for Unicode safety
        if title.chars().count() > 200 {
            let mut truncated = String::new();
            for (i, ch) in title.char_indices() {
                if truncated.chars().count() >= 197 {
                    break;
                }
                truncated.push(ch);
            }
            format!("{}...", truncated)
        } else {
            title
        }
    }
    
    // Comprehensive description parsing with HTML cleaning
    fn parse_description_comprehensive(&self, data: &HashMap<String, String>) -> String {
        // EXACT Python replication: entry.get('content', [{}])[0].get('value') or entry.get('summary') or "No description available"
        data.get("content:encoded")
            .or_else(|| data.get("content"))  
            .or_else(|| data.get("summary"))
            .filter(|s| !s.trim().is_empty())
            .cloned()
            .unwrap_or_else(|| "No description available".to_string())
    }
    
    // EXACT Python replication: parsed_audio_url = entry.enclosures[0].href if entry.enclosures else ""
    fn parse_audio_url_comprehensive(&self, data: &HashMap<String, String>) -> String {
        // Python: entry.enclosures[0].href if entry.enclosures else ""
        data.get("enclosure_url")
            .filter(|url| !url.trim().is_empty())
            .cloned()
            .unwrap_or_else(|| String::new())
    }
    
    // Comprehensive artwork URL parsing - prioritizes episode-specific artwork
    fn parse_artwork_comprehensive(&self, data: &HashMap<String, String>, default_artwork: &str) -> String {
        // Priority order: episode-specific artwork first, then fallbacks
        let artwork_candidates = [
            data.get("episode_artwork_href"),    // <itunes:image href=""/> or <image href=""/> in episode
            data.get("media_image_url"),         // <media:content type="image/jpeg" url=""/>
            data.get("media_thumbnail_url"),     // <media:thumbnail url=""/>
            data.get("itunes:image"),           // Text content of <itunes:image>
            data.get("image"),                  // Text content of <image>
            data.get("thumbnail"),              // Generic thumbnail
            data.get("logo"),                   // Logo field
        ];
        
        for candidate in artwork_candidates.iter().flatten() {
            if !candidate.trim().is_empty() && self.is_valid_image_url(candidate) {
                let cleaned_url = self.validate_and_clean_url(candidate);
                return cleaned_url;
            }
        }
        
        // Use default podcast artwork as fallback
        default_artwork.to_string()
    }
    
    // Comprehensive publication date parsing
    fn parse_publication_date_comprehensive(&self, data: &HashMap<String, String>) -> DateTime<Utc> {
        // println!("🔍 DEBUG: Date parsing - episode_data keys: {:?}", data.keys().collect::<Vec<_>>());
        
        // Multiple date field sources
        let date_candidates = [
            data.get("pubDate"),
            data.get("published"),
            data.get("dc:date"),
            data.get("updated"),
            data.get("lastBuildDate"),
            data.get("date"),
        ];
        
        for (i, date_str) in date_candidates.iter().enumerate() {
            if let Some(date_str) = date_str {
                // println!("📅 DEBUG: Trying date candidate {}: '{}'", i, date_str);
                if let Some(parsed_date) = self.try_parse_date(date_str) {
                    // Validate date is reasonable (not too far in future, not before 1990)
                    let now = Utc::now();
                    let year_1990 = DateTime::parse_from_rfc3339("1990-01-01T00:00:00Z").unwrap().with_timezone(&Utc);
                    let one_year_future = now + chrono::Duration::days(365);
                    
                    if parsed_date >= year_1990 && parsed_date <= one_year_future {
                        // println!("✅ DEBUG: Successfully parsed date: {}", parsed_date);
                        return parsed_date;
                    } else {
                        // println!("❌ DEBUG: Date {} outside valid range", parsed_date);
                    }
                } else {
                    // println!("❌ DEBUG: Failed to parse date string: '{}'", date_str);
                }
            }
        }
        
        // Fallback to current time like Python version
        // println!("⚠️  DEBUG: No valid date found, falling back to current time");
        Utc::now()
    }
    
    // Try to parse a date string with multiple formats
    fn try_parse_date(&self, date_str: &str) -> Option<DateTime<Utc>> {
        let date_str = date_str.trim();
        // println!("🔧 DEBUG: Attempting to parse date: '{}'", date_str);
        
        // RFC 2822 format (most common in RSS)
        if let Ok(parsed) = DateTime::parse_from_rfc2822(date_str) {
            // println!("✅ DEBUG: Parsed as RFC 2822: {}", parsed);
            return Some(parsed.with_timezone(&Utc));
        } else {
            // println!("❌ DEBUG: Failed to parse as RFC 2822");
        }
        
        // RFC 3339/ISO 8601 format
        if let Ok(parsed) = DateTime::parse_from_rfc3339(date_str) {
            // println!("✅ DEBUG: Parsed as RFC 3339: {}", parsed);
            return Some(parsed.with_timezone(&Utc));
        } else {
            // println!("❌ DEBUG: Failed to parse as RFC 3339");
        }
        
        // Common custom formats found in real feeds
        let formats = [
            "%Y-%m-%d %H:%M:%S %z",
            "%Y-%m-%dT%H:%M:%S%z",
            "%Y-%m-%d %H:%M:%S",
            "%Y-%m-%dT%H:%M:%S",
            "%a, %d %b %Y %H:%M:%S %z",
            "%a, %d %b %Y %H:%M:%S",
            "%d %b %Y %H:%M:%S %z",
            "%d %b %Y %H:%M:%S",
            "%Y-%m-%d",
            "%d/%m/%Y",
            "%m/%d/%Y",
            "%b %d, %Y",
            "%B %d, %Y",
            "%Y%m%d",
        ];
        
        // Try parsing with timezone
        for format in &formats[..8] {
            if let Ok(parsed) = DateTime::parse_from_str(date_str, format) {
                return Some(parsed.with_timezone(&Utc));
            }
        }
        
        // Try parsing as naive datetime (assume UTC)
        for format in &formats[8..] {
            if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(date_str, format) {
                return Some(DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc));
            }
        }
        
        // Try parsing date only (assume midnight UTC)
        for format in &formats[10..] {
            if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(date_str, format) {
                if let Some(naive_datetime) = naive_date.and_hms_opt(0, 0, 0) {
                    return Some(DateTime::<Utc>::from_naive_utc_and_offset(naive_datetime, Utc));
                }
            }
        }
        
        None
    }
    
    // Validate and clean URLs
    fn validate_and_clean_url(&self, url: &str) -> String {
        let url = url.trim();
        
        // Basic URL validation
        if !url.starts_with("http://") && !url.starts_with("https://") {
            if url.starts_with("//") {
                return format!("https:{}", url);
            } else if url.starts_with("/") {
                // Relative URL - can't fix without base URL
                return url.to_string();
            } else {
                return format!("https://{}", url);
            }
        }
        
        url.to_string()
    }
    
    // Check if URL is likely a valid image
    fn is_valid_image_url(&self, url: &str) -> bool {
        let url_lower = url.to_lowercase();
        url_lower.contains(".jpg") || 
        url_lower.contains(".jpeg") || 
        url_lower.contains(".png") || 
        url_lower.contains(".gif") || 
        url_lower.contains(".webp") || 
        url_lower.contains(".svg") ||
        url_lower.contains("image") ||
        url_lower.contains("artwork") ||
        url_lower.contains("thumbnail") ||
        url_lower.contains("cover")
    }
    
    fn decode_html_entities(&self, text: &str) -> String {
        text.replace("&amp;", "&")
            .replace("&lt;", "<")
            .replace("&gt;", ">")
            .replace("&quot;", "\"")
            .replace("&#x27;", "'")
            .replace("&#39;", "'")
            .replace("&apos;", "'")
            .replace("&nbsp;", " ")
            .replace("&#160;", " ")
            .replace("&mdash;", "—")
            .replace("&ndash;", "–")
            .replace("&hellip;", "…")
            .replace("&rsquo;", "'")
            .replace("&lsquo;", "'")
            .replace("&rdquo;", "\"")
            .replace("&ldquo;", "\"")
    }
    
    // // Check if URL is likely an audio file
    // fn is_audio_url(&self, url: &str) -> bool {
    //     let url_lower = url.to_lowercase();
    //     url_lower.contains(".mp3") || 
    //     url_lower.contains(".m4a") || 
    //     url_lower.contains(".wav") || 
    //     url_lower.contains(".ogg") || 
    //     url_lower.contains(".aac") || 
    //     url_lower.contains(".flac") || 
    //     url_lower.contains(".wma") || 
    //     url_lower.contains(".opus") ||
    //     url_lower.contains("audio") ||
    //     url_lower.contains("/podcast/") ||
    //     url_lower.contains("/episode/") ||
    //     url_lower.contains(".podtrac.com") ||
    //     url_lower.contains("libsyn.com") ||
    //     url_lower.contains("soundcloud.com") ||
    //     url_lower.contains("spotify.com")
    // }
    
    // Extract audio URLs from episode descriptions/content
    fn extract_audio_url_from_description(&self, data: &HashMap<String, String>) -> Option<String> {
        let content_fields = [
            data.get("content:encoded"),
            data.get("content"),
            data.get("description"),
            data.get("summary"),
        ];
        
        for content in content_fields.iter().flatten() {
            // Look for common audio file patterns in HTML content
            let patterns = [
                r#"src="([^"]*\.mp3[^"]*)""#,
                r#"src="([^"]*\.m4a[^"]*)""#,
                r#"href="([^"]*\.mp3[^"]*)""#,
                r#"href="([^"]*\.m4a[^"]*)""#,
                r#"url="([^"]*\.mp3[^"]*)""#,
                r#"url="([^"]*\.m4a[^"]*)""#,
            ];
            
            for pattern in &patterns {
                // Simple regex-like matching (basic implementation)
                if let Some(url) = self.extract_url_from_pattern(content, pattern) {
                    if self.is_audio_url(&url) {
                        return Some(url);
                    }
                }
            }
            
            // Look for direct URLs in plain text
            if let Some(url) = self.extract_direct_audio_url(content) {
                return Some(url);
            }
        }
        
        None
    }
    
    // Extract URL from pattern (simple implementation)
    fn extract_url_from_pattern(&self, content: &str, pattern: &str) -> Option<String> {
        // Very basic pattern matching - look for quoted URLs containing audio extensions
        let content_lower = content.to_lowercase();
        let audio_extensions = [".mp3", ".m4a", ".wav", ".ogg", ".aac"];
        
        for ext in &audio_extensions {
            if let Some(ext_pos) = content_lower.find(ext) {
                // Look backwards for quote or space
                let mut start_pos = 0;
                for (i, ch) in content[..ext_pos].char_indices().rev() {
                    if ch == '"' || ch == '\'' || ch == ' ' || ch == '>' {
                        start_pos = i + 1;
                        break;
                    }
                }
                
                // Look forwards for quote or space after extension
                let mut end_pos = content.len();
                let search_start = ext_pos + ext.len();
                if search_start < content.len() {
                    for (i, ch) in content[search_start..].char_indices() {
                        if ch == '"' || ch == '\'' || ch == ' ' || ch == '<' || ch == '?' {
                            end_pos = search_start + i;
                            break;
                        }
                    }
                }
                
                if start_pos < end_pos {
                    let potential_url = &content[start_pos..end_pos];
                    if potential_url.starts_with("http") && self.is_audio_url(potential_url) {
                        return Some(potential_url.to_string());
                    }
                }
            }
        }
        
        None
    }
    
    // Extract direct audio URLs from plain text
    fn extract_direct_audio_url(&self, content: &str) -> Option<String> {
        // Split by whitespace and look for URLs
        for word in content.split_whitespace() {
            if word.starts_with("http") && self.is_audio_url(word) {
                // Clean up the URL (remove trailing punctuation)
                let cleaned = word.trim_end_matches(&['.', ',', '!', '?', ')', ']', '}']);
                return Some(cleaned.to_string());
            }
        }
        
        None
    }

    
    // Strip HTML tags (basic but effective)
    fn strip_html_tags(&self, text: &str) -> String {
        let mut result = String::new();
        let mut in_tag = false;
        
        for ch in text.chars() {
            match ch {
                '<' => in_tag = true,
                '>' => {
                    in_tag = false;
                    result.push(' '); // Replace tags with space
                }
                _ if !in_tag => result.push(ch),
                _ => {} // Skip characters inside tags
            }
        }
        
        result
    }
    
    // Normalize whitespace
    fn normalize_whitespace(&self, text: &str) -> String {
        text.split_whitespace()
            .collect::<Vec<_>>()
            .join(" ")
            .trim()
            .to_string()
    }
    
    // Comprehensive duration parsing matching Python logic
    fn parse_duration_comprehensive(&self, data: &HashMap<String, String>, estimate_missing_durations: bool) -> i32 {
        // Priority order like Python version
        let duration_candidates = [
            data.get("itunes:duration"),
            data.get("duration"),
            data.get("itunes:duration_seconds"),
            data.get("length"),
            data.get("time"),
        ];
        
        for candidate in duration_candidates.iter().flatten() {
            if let Some(duration) = self.parse_duration_string(candidate) {
                if duration > 0 && duration < 86400 { // Reasonable range: 0-24 hours
                    return duration;
                }
            }
        }
        
        // Try to estimate from file size if available (like Python version)
        if let Some(length_str) = data.get("enclosure_length") {
            if let Ok(file_size) = length_str.parse::<i64>() {
                if file_size > 1_000_000 { // > 1MB
                    return self.estimate_duration_from_file_size(file_size);
                }
            }
        }
        
        // Only estimate duration from HTTP if we're adding a new episode AND the flag is enabled
        if estimate_missing_durations {
            if let Some(audio_url) = data.get("enclosure_url") {
                // Check if we're in an async context and can make the HTTP request
                if let Ok(handle) = tokio::runtime::Handle::try_current() {
                    if let Some(estimated_duration) = tokio::task::block_in_place(|| {
                        handle.block_on(self.estimate_duration_from_audio_url_async(audio_url))
                    }) {
                        return estimated_duration;
                    }
                }
            }
        }
        
        // Default to 0 if no duration can be determined
        0
    }
    
    // Parse duration string - EXACT Python logic replication
    fn parse_duration_string(&self, duration_str: &str) -> Option<i32> {
        let duration_str = duration_str.trim();
        
        if duration_str.contains(':') {
            // Python: time_parts = list(map(int, duration_str.split(':')))
            let parts: Result<Vec<i32>, _> = duration_str.split(':')
                .map(|part| part.parse::<i32>())
                .collect();
                
            if let Ok(mut time_parts) = parts {
                // Python: while len(time_parts) < 3: time_parts.insert(0, 0)
                while time_parts.len() < 3 {
                    time_parts.insert(0, 0);
                }
                
                if time_parts.len() >= 3 {
                    let h = time_parts[0];
                    let m = time_parts[1]; 
                    let s = time_parts[2];
                    
                    // Python: parsed_duration = h * 3600 + m * 60 + s
                    let duration = h * 3600 + m * 60 + s;
                    if duration >= 0 {
                        return Some(duration);
                    }
                }
            }
        } else if duration_str.chars().all(|c| c.is_ascii_digit()) {
            // Python: elif duration_str.isdigit(): parsed_duration = int(duration_str)
            if let Ok(duration) = duration_str.parse::<i32>() {
                return Some(duration);
            }
        }
        
        None
    }
    
    // Parse human-readable duration formats
    fn parse_human_readable_duration(&self, duration_str: &str) -> Option<i32> {
        let duration_str = duration_str.to_lowercase();
        let mut total_seconds = 0;
        
        // Extract hours
        if let Some(hours_match) = self.extract_time_component(&duration_str, &["h", "hr", "hour", "hours"]) {
            total_seconds += hours_match * 3600;
        }
        
        // Extract minutes
        if let Some(minutes_match) = self.extract_time_component(&duration_str, &["m", "min", "mins", "minute", "minutes"]) {
            total_seconds += minutes_match * 60;
        }
        
        // Extract seconds
        if let Some(seconds_match) = self.extract_time_component(&duration_str, &["s", "sec", "secs", "second", "seconds"]) {
            total_seconds += seconds_match;
        }
        
        if total_seconds > 0 {
            Some(total_seconds)
        } else {
            None
        }
    }
    
    // Extract time component (e.g., "30" from "30min")
    fn extract_time_component(&self, text: &str, suffixes: &[&str]) -> Option<i32> {
        for suffix in suffixes {
            if let Some(pos) = text.find(suffix) {
                // Look backwards from position to find the number
                let before = &text[..pos];
                
                // Find the last sequence of digits
                let mut number_start = pos;
                for (i, ch) in before.char_indices().rev() {
                    if ch.is_ascii_digit() {
                        number_start = i;
                    } else if number_start < pos {
                        // Found start of number sequence
                        break;
                    }
                }
                
                if number_start < pos {
                    if let Ok(number) = before[number_start..].trim().parse::<i32>() {
                        return Some(number);
                    }
                }
            }
        }
        None
    }
    
    // Estimate duration from file size like Python version
    fn estimate_duration_from_file_size(&self, file_size_bytes: i64) -> i32 {
        // Assume 128 kbps average bitrate like Python version
        let bitrate_kbps = 128;
        let bytes_per_second = (bitrate_kbps * 1000) / 8;
        (file_size_bytes / bytes_per_second) as i32
    }
    
    // NEW: Estimate duration by fetching HTTP HEAD request to get Content-Length
    // This is a fallback for when RSS feeds don't include duration or file size
    async fn estimate_duration_from_audio_url_async(&self, audio_url: &str) -> Option<i32> {
        println!("Attempting to estimate duration from audio URL: {}", audio_url);
        
        // Build HTTP client with timeout to avoid hanging
        let client = match reqwest::Client::builder()
            .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
            .timeout(std::time::Duration::from_secs(10)) // Short timeout
            .build()
        {
            Ok(client) => client,
            Err(e) => {
                println!("Failed to build HTTP client for duration estimation: {}", e);
                return None;
            }
        };
        
        // Make HEAD request to get Content-Length without downloading the file
        match client.head(audio_url).send().await {
            Ok(response) => {
                if response.status().is_success() {
                    if let Some(content_length) = response.headers().get("content-length") {
                        if let Ok(content_length_str) = content_length.to_str() {
                            if let Ok(file_size) = content_length_str.parse::<i64>() {
                                if file_size > 1_000_000 { // > 1MB, reasonable audio file
                                    let estimated_duration = self.estimate_duration_from_file_size(file_size);
                                    println!("Estimated duration from file size {}: {} seconds", file_size, estimated_duration);
                                    return Some(estimated_duration);
                                }
                            }
                        }
                    }
                    println!("No Content-Length header found in response");
                } else {
                    println!("HEAD request failed with status: {}", response.status());
                }
            }
            Err(e) => {
                println!("HTTP HEAD request failed for {}: {}", audio_url, e);
            }
        }
        
        None
    }


    // Get first episode ID - matches Python get_first_episode_id function
    pub async fn get_first_episode_id(&self, podcast_id: i32, is_youtube: bool) -> AppResult<Option<i32>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let query = if is_youtube {
                    r#"SELECT videoid FROM "YouTubeVideos" WHERE podcastid = $1 ORDER BY publishedat ASC LIMIT 1"#
                } else {
                    r#"SELECT episodeid FROM "Episodes" WHERE podcastid = $1 ORDER BY episodepubdate ASC LIMIT 1"#
                };
                
                let row = sqlx::query(query)
                    .bind(podcast_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    if is_youtube {
                        Ok(Some(row.try_get("videoid")?))
                    } else {
                        Ok(Some(row.try_get("episodeid")?))
                    }
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let query = if is_youtube {
                    "SELECT VideoID FROM YouTubeVideos WHERE PodcastID = ? ORDER BY PublishedAt ASC LIMIT 1"
                } else {
                    "SELECT EpisodeID FROM Episodes WHERE PodcastID = ? ORDER BY EpisodePubDate ASC LIMIT 1"
                };
                
                let row = sqlx::query(query)
                    .bind(podcast_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    if is_youtube {
                        Ok(Some(row.try_get("VideoID")?))
                    } else {
                        Ok(Some(row.try_get("EpisodeID")?))
                    }
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Setup timezone info - matches Python setup_timezone_info function
    pub async fn setup_timezone_info(&self, user_id: i32, timezone: &str, hour_pref: i32, date_format: &str) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"UPDATE "Users" SET timezone = $1, timeformat = $2, dateformat = $3, firstlogin = $4 WHERE userid = $5"#)
                    .bind(timezone)
                    .bind(hour_pref)
                    .bind(date_format)
                    .bind(true)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("UPDATE Users SET Timezone = ?, TimeFormat = ?, DateFormat = ?, FirstLogin = ? WHERE UserID = ?")
                    .bind(timezone)
                    .bind(hour_pref)
                    .bind(date_format)
                    .bind(1)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
        }
    }

    // Update user timezone
    pub async fn update_user_timezone(&self, user_id: i32, timezone: &str) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"UPDATE "Users" SET timezone = $1 WHERE userid = $2"#)
                    .bind(timezone)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("UPDATE Users SET Timezone = ? WHERE UserID = ?")
                    .bind(timezone)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
        }
    }

    // Update user date format
    pub async fn update_user_date_format(&self, user_id: i32, date_format: &str) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"UPDATE "Users" SET dateformat = $1 WHERE userid = $2"#)
                    .bind(date_format)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("UPDATE Users SET DateFormat = ? WHERE UserID = ?")
                    .bind(date_format)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
        }
    }

    // Update user time format (hour preference)
    pub async fn update_user_time_format(&self, user_id: i32, hour_pref: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"UPDATE "Users" SET timeformat = $1 WHERE userid = $2"#)
                    .bind(hour_pref)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("UPDATE Users SET TimeFormat = ? WHERE UserID = ?")
                    .bind(hour_pref)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
        }
    }

    // User admin check - matches Python user_admin_check function
    pub async fn user_admin_check(&self, user_id: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT isadmin FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(row.try_get("isadmin")?)
                } else {
                    Ok(false)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT IsAdmin FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let is_admin: i8 = row.try_get("IsAdmin")?;
                    Ok(is_admin != 0)
                } else {
                    Ok(false)
                }
            }
        }
    }

    // Get podcast ID by user, feed URL, and title
    pub async fn get_podcast_id(&self, user_id: i32, podcast_feed: &str, podcast_title: &str) -> AppResult<Option<i32>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT podcastid FROM "Podcasts" WHERE feedurl = $1 AND podcastname = $2 AND userid = $3"#)
                    .bind(podcast_feed)
                    .bind(podcast_title)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(Some(row.try_get("podcastid")?))
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT PodcastID FROM Podcasts WHERE FeedURL = ? AND PodcastName = ? AND UserID = ?")
                    .bind(podcast_feed)
                    .bind(podcast_title)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(Some(row.try_get("PodcastID")?))
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Get downloaded episodes - matches Python download_episode_list function
    pub async fn download_episode_list(&self, user_id: i32) -> AppResult<Vec<crate::handlers::podcasts::DownloadedEpisode>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT * FROM (
                        SELECT
                            "Podcasts".podcastid as podcastid,
                            "Podcasts".podcastname as podcastname,
                            "Podcasts".artworkurl as artworkurl,
                            "Episodes".episodeid as episodeid,
                            "Episodes".episodetitle as episodetitle,
                            "Episodes".episodepubdate as episodepubdate,
                            "Episodes".episodedescription as episodedescription,
                            CASE 
                                WHEN "Podcasts".usepodcastcoverscustomized = TRUE AND "Podcasts".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                WHEN "Users".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                ELSE "Episodes".episodeartwork
                            END as episodeartwork,
                            "Episodes".episodeurl as episodeurl,
                            "Episodes".episodeduration as episodeduration,
                            "Podcasts".podcastindexid as podcastindexid,
                            "Podcasts".websiteurl as websiteurl,
                            "DownloadedEpisodes".downloadedlocation as downloadedlocation,
                            "UserEpisodeHistory".listenduration as listenduration,
                            "Episodes".completed as completed,
                            CASE WHEN "SavedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN "EpisodeQueue".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                            TRUE as downloaded,
                            FALSE as is_youtube
                        FROM "DownloadedEpisodes"
                        INNER JOIN "Episodes" ON "DownloadedEpisodes".episodeid = "Episodes".episodeid
                        INNER JOIN "Podcasts" ON "Episodes".podcastid = "Podcasts".podcastid
                        LEFT JOIN "Users" ON "Podcasts".userid = "Users".userid
                        LEFT JOIN "UserEpisodeHistory" ON
                            "DownloadedEpisodes".episodeid = "UserEpisodeHistory".episodeid
                            AND "DownloadedEpisodes".userid = "UserEpisodeHistory".userid
                        LEFT JOIN "SavedEpisodes" ON
                            "DownloadedEpisodes".episodeid = "SavedEpisodes".episodeid
                            AND "SavedEpisodes".userid = $1
                        LEFT JOIN "EpisodeQueue" ON
                            "DownloadedEpisodes".episodeid = "EpisodeQueue".episodeid
                            AND "EpisodeQueue".userid = $2
                            AND "EpisodeQueue".is_youtube = FALSE
                        WHERE "DownloadedEpisodes".userid = $3

                        UNION ALL

                        SELECT
                            "Podcasts".podcastid as podcastid,
                            "Podcasts".podcastname as podcastname,
                            "Podcasts".artworkurl as artworkurl,
                            "YouTubeVideos".videoid as episodeid,
                            "YouTubeVideos".videotitle as episodetitle,
                            "YouTubeVideos".publishedat as episodepubdate,
                            "YouTubeVideos".videodescription as episodedescription,
                            CASE 
                                WHEN "Podcasts".usepodcastcoverscustomized = TRUE AND "Podcasts".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                WHEN "Users".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                ELSE "YouTubeVideos".thumbnailurl
                            END as episodeartwork,
                            "YouTubeVideos".videourl as episodeurl,
                            "YouTubeVideos".duration as episodeduration,
                            "Podcasts".podcastindexid as podcastindexid,
                            "Podcasts".websiteurl as websiteurl,
                            "DownloadedVideos".downloadedlocation as downloadedlocation,
                            "YouTubeVideos".listenposition as listenduration,
                            "YouTubeVideos".completed as completed,
                            CASE WHEN "SavedVideos".videoid IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN "EpisodeQueue".episodeid IS NOT NULL AND "EpisodeQueue".is_youtube = TRUE THEN TRUE ELSE FALSE END AS queued,
                            TRUE as downloaded,
                            TRUE as is_youtube
                        FROM "DownloadedVideos"
                        INNER JOIN "YouTubeVideos" ON "DownloadedVideos".videoid = "YouTubeVideos".videoid
                        INNER JOIN "Podcasts" ON "YouTubeVideos".podcastid = "Podcasts".podcastid
                        LEFT JOIN "Users" ON "Podcasts".userid = "Users".userid
                        LEFT JOIN "SavedVideos" ON
                            "DownloadedVideos".videoid = "SavedVideos".videoid
                            AND "SavedVideos".userid = $4
                        LEFT JOIN "EpisodeQueue" ON
                            "DownloadedVideos".videoid = "EpisodeQueue".episodeid
                            AND "EpisodeQueue".userid = $5
                            AND "EpisodeQueue".is_youtube = TRUE
                        WHERE "DownloadedVideos".userid = $6
                    ) combined
                    ORDER BY episodepubdate DESC"#
                )
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .fetch_all(pool)
                .await?;
                
                let mut episodes = Vec::new();
                for row in rows {
                    episodes.push(crate::handlers::podcasts::DownloadedEpisode {
                        podcastid: row.try_get("podcastid")?,
                        podcastname: row.try_get("podcastname")?,
                        artworkurl: row.try_get("artworkurl").ok(),
                        episodeid: row.try_get("episodeid")?,
                        episodetitle: row.try_get("episodetitle")?,
                        episodepubdate: {
                            let naive = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                            naive.format("%Y-%m-%dT%H:%M:%S").to_string()
                        },
                        episodedescription: row.try_get("episodedescription")?,
                        episodeartwork: row.try_get("episodeartwork").ok(),
                        episodeurl: row.try_get("episodeurl")?,
                        episodeduration: row.try_get("episodeduration")?,
                        podcastindexid: row.try_get("podcastindexid").ok(),
                        websiteurl: row.try_get("websiteurl").ok(),
                        downloadedlocation: row.try_get("downloadedlocation")?,
                        listenduration: row.try_get("listenduration").ok(),
                        completed: row.try_get("completed")?,
                        saved: row.try_get("saved")?,
                        queued: row.try_get("queued")?,
                        downloaded: row.try_get("downloaded")?,
                        is_youtube: row.try_get("is_youtube")?,
                    });
                }
                Ok(episodes)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(
                    "SELECT * FROM (
                        SELECT
                            Podcasts.PodcastID as podcastid,
                            Podcasts.PodcastName as podcastname,
                            Podcasts.ArtworkURL as artworkurl,
                            Episodes.EpisodeID as episodeid,
                            Episodes.EpisodeTitle as episodetitle,
                            Episodes.EpisodePubDate as episodepubdate,
                            Episodes.EpisodeDescription as episodedescription,
                            CASE 
                                WHEN Podcasts.UsePodcastCoversCustomized = 1 AND Podcasts.UsePodcastCovers = 1 THEN Podcasts.ArtworkURL
                                WHEN Users.UsePodcastCovers = 1 THEN Podcasts.ArtworkURL
                                ELSE Episodes.EpisodeArtwork
                            END as episodeartwork,
                            Episodes.EpisodeURL as episodeurl,
                            Episodes.EpisodeDuration as episodeduration,
                            Podcasts.PodcastIndexID as podcastindexid,
                            Podcasts.WebsiteURL as websiteurl,
                            DownloadedEpisodes.DownloadedLocation as downloadedlocation,
                            UserEpisodeHistory.ListenDuration as listenduration,
                            Episodes.Completed as completed,
                            CASE WHEN SavedEpisodes.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN EpisodeQueue.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                            TRUE as downloaded,
                            FALSE as is_youtube
                        FROM DownloadedEpisodes
                        INNER JOIN Episodes ON DownloadedEpisodes.EpisodeID = Episodes.EpisodeID
                        INNER JOIN Podcasts ON Episodes.PodcastID = Podcasts.PodcastID
                        LEFT JOIN Users ON Podcasts.UserID = Users.UserID
                        LEFT JOIN UserEpisodeHistory ON
                            DownloadedEpisodes.EpisodeID = UserEpisodeHistory.EpisodeID
                            AND DownloadedEpisodes.UserID = UserEpisodeHistory.UserID
                        LEFT JOIN SavedEpisodes ON
                            DownloadedEpisodes.EpisodeID = SavedEpisodes.EpisodeID
                            AND SavedEpisodes.UserID = ?
                        LEFT JOIN EpisodeQueue ON
                            DownloadedEpisodes.EpisodeID = EpisodeQueue.EpisodeID
                            AND EpisodeQueue.UserID = ?
                            AND EpisodeQueue.is_youtube = FALSE
                        WHERE DownloadedEpisodes.UserID = ?

                        UNION ALL

                        SELECT
                            Podcasts.PodcastID as podcastid,
                            Podcasts.PodcastName as podcastname,
                            Podcasts.ArtworkURL as artworkurl,
                            YouTubeVideos.VideoID as episodeid,
                            YouTubeVideos.VideoTitle as episodetitle,
                            YouTubeVideos.PublishedAt as episodepubdate,
                            YouTubeVideos.VideoDescription as episodedescription,
                            CASE 
                                WHEN Podcasts.UsePodcastCoversCustomized = 1 AND Podcasts.UsePodcastCovers = 1 THEN Podcasts.ArtworkURL
                                WHEN Users.UsePodcastCovers = 1 THEN Podcasts.ArtworkURL
                                ELSE YouTubeVideos.ThumbnailURL
                            END as episodeartwork,
                            YouTubeVideos.VideoURL as episodeurl,
                            YouTubeVideos.Duration as episodeduration,
                            Podcasts.PodcastIndexID as podcastindexid,
                            Podcasts.WebsiteURL as websiteurl,
                            DownloadedVideos.DownloadedLocation as downloadedlocation,
                            YouTubeVideos.ListenPosition as listenduration,
                            YouTubeVideos.Completed as completed,
                            CASE WHEN SavedVideos.VideoID IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN EpisodeQueue.EpisodeID IS NOT NULL AND EpisodeQueue.is_youtube = TRUE THEN TRUE ELSE FALSE END AS queued,
                            TRUE as downloaded,
                            TRUE as is_youtube
                        FROM DownloadedVideos
                        INNER JOIN YouTubeVideos ON DownloadedVideos.VideoID = YouTubeVideos.VideoID
                        INNER JOIN Podcasts ON YouTubeVideos.PodcastID = Podcasts.PodcastID
                        LEFT JOIN Users ON Podcasts.UserID = Users.UserID
                        LEFT JOIN SavedVideos ON
                            DownloadedVideos.VideoID = SavedVideos.VideoID
                            AND SavedVideos.UserID = ?
                        LEFT JOIN EpisodeQueue ON
                            DownloadedVideos.VideoID = EpisodeQueue.EpisodeID
                            AND EpisodeQueue.UserID = ?
                            AND EpisodeQueue.is_youtube = TRUE
                        WHERE DownloadedVideos.UserID = ?
                    ) combined
                    ORDER BY episodepubdate DESC"
                )
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .fetch_all(pool)
                .await?;
                
                let mut episodes = Vec::new();
                for row in rows {
                    episodes.push(crate::handlers::podcasts::DownloadedEpisode {
                        podcastid: row.try_get("podcastid")?,
                        podcastname: row.try_get("podcastname")?,
                        artworkurl: row.try_get("artworkurl").ok(),
                        episodeid: row.try_get("episodeid")?,
                        episodetitle: row.try_get("episodetitle")?,
                        episodepubdate: {
                            let naive = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                            naive.format("%Y-%m-%dT%H:%M:%S").to_string()
                        },
                        episodedescription: row.try_get("episodedescription")?,
                        episodeartwork: row.try_get("episodeartwork").ok(),
                        episodeurl: row.try_get("episodeurl")?,
                        episodeduration: row.try_get("episodeduration")?,
                        podcastindexid: row.try_get("podcastindexid").ok(),
                        websiteurl: row.try_get("websiteurl").ok(),
                        downloadedlocation: row.try_get("downloadedlocation")?,
                        listenduration: row.try_get("listenduration").ok(),
                        completed: row.try_get("completed")?,
                        saved: row.try_get("saved")?,
                        queued: row.try_get("queued")?,
                        downloaded: row.try_get("downloaded")?,
                        is_youtube: row.try_get("is_youtube")?,
                    });
                }
                Ok(episodes)
            }
        }
    }

    // Check if episode is already downloaded
    pub async fn check_downloaded(&self, user_id: i32, episode_id: i32, is_youtube: bool) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let query = if is_youtube {
                    r#"SELECT COUNT(*) as count FROM "DownloadedVideos" WHERE userid = $1 AND videoid = $2"#
                } else {
                    r#"SELECT COUNT(*) as count FROM "DownloadedEpisodes" WHERE userid = $1 AND episodeid = $2"#
                };
                
                let row = sqlx::query(query)
                    .bind(user_id)
                    .bind(episode_id)
                    .fetch_one(pool)
                    .await?;
                    
                let count: i64 = row.try_get("count")?;
                Ok(count > 0)
            }
            DatabasePool::MySQL(pool) => {
                let query = if is_youtube {
                    "SELECT COUNT(*) as count FROM DownloadedVideos WHERE UserID = ? AND VideoID = ?"
                } else {
                    "SELECT COUNT(*) as count FROM DownloadedEpisodes WHERE UserID = ? AND EpisodeID = ?"
                };
                
                let row = sqlx::query(query)
                    .bind(user_id)
                    .bind(episode_id)
                    .fetch_one(pool)
                    .await?;
                    
                let count: i64 = row.try_get("count")?;
                Ok(count > 0)
            }
        }
    }

    // Delete downloaded episode
    pub async fn delete_episode(&self, user_id: i32, episode_id: i32, is_youtube: bool) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                if is_youtube {
                    let result = sqlx::query(r#"DELETE FROM "DownloadedVideos" WHERE userid = $1 AND videoid = $2"#)
                        .bind(user_id)
                        .bind(episode_id)
                        .execute(pool)
                        .await?;

                    // Only update UserStats if a row was actually deleted
                    if result.rows_affected() > 0 {
                        sqlx::query(r#"UPDATE "UserStats" SET episodesdownloaded = episodesdownloaded - 1 WHERE userid = $1"#)
                            .bind(user_id)
                            .execute(pool)
                            .await?;
                    }
                } else {
                    let result = sqlx::query(r#"DELETE FROM "DownloadedEpisodes" WHERE userid = $1 AND episodeid = $2"#)
                        .bind(user_id)
                        .bind(episode_id)
                        .execute(pool)
                        .await?;

                    // Only update UserStats if a row was actually deleted
                    if result.rows_affected() > 0 {
                        sqlx::query(r#"UPDATE "UserStats" SET episodesdownloaded = episodesdownloaded - 1 WHERE userid = $1"#)
                            .bind(user_id)
                            .execute(pool)
                            .await?;
                    }
                }
                Ok(())
            }
            DatabasePool::MySQL(pool) => {
                if is_youtube {
                    let result = sqlx::query("DELETE FROM DownloadedVideos WHERE UserID = ? AND VideoID = ?")
                        .bind(user_id)
                        .bind(episode_id)
                        .execute(pool)
                        .await?;

                    // Only update UserStats if a row was actually deleted
                    if result.rows_affected() > 0 {
                        sqlx::query("UPDATE UserStats SET EpisodesDownloaded = EpisodesDownloaded - 1 WHERE UserID = ?")
                            .bind(user_id)
                            .execute(pool)
                            .await?;
                    }
                } else {
                    let result = sqlx::query("DELETE FROM DownloadedEpisodes WHERE UserID = ? AND EpisodeID = ?")
                        .bind(user_id)
                        .bind(episode_id)
                        .execute(pool)
                        .await?;

                    // Only update UserStats if a row was actually deleted
                    if result.rows_affected() > 0 {
                        sqlx::query("UPDATE UserStats SET EpisodesDownloaded = EpisodesDownloaded - 1 WHERE UserID = ?")
                            .bind(user_id)
                            .execute(pool)
                            .await?;
                    }
                }
                Ok(())
            }
        }
    }

    // Get download status for user
    pub async fn get_download_status(&self, user_id: i32) -> AppResult<serde_json::Value> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Get active download tasks
                let rows = sqlx::query(
                    r#"SELECT taskid, tasktype, progress, status FROM "UserTasks" 
                       WHERE userid = $1 AND tasktype IN ('download_episode', 'download_video', 'download_all_episodes', 'download_all_videos') 
                       AND status IN ('pending', 'running')"#
                )
                .bind(user_id)
                .fetch_all(pool)
                .await?;
                
                let mut tasks = Vec::new();
                for row in rows {
                    tasks.push(serde_json::json!({
                        "task_id": row.try_get::<String, _>("taskid")?,
                        "task_type": row.try_get::<String, _>("tasktype")?,
                        "progress": row.try_get::<Option<i32>, _>("progress")?,
                        "status": row.try_get::<String, _>("status")?
                    }));
                }
                
                Ok(serde_json::json!({ "active_downloads": tasks }))
            }
            DatabasePool::MySQL(pool) => {
                // Get active download tasks
                let rows = sqlx::query(
                    "SELECT TaskID, TaskType, Progress, Status FROM UserTasks 
                     WHERE UserID = ? AND TaskType IN ('download_episode', 'download_video', 'download_all_episodes', 'download_all_videos') 
                     AND Status IN ('pending', 'running')"
                )
                .bind(user_id)
                .fetch_all(pool)
                .await?;
                
                let mut tasks = Vec::new();
                for row in rows {
                    tasks.push(serde_json::json!({
                        "task_id": row.try_get::<String, _>("TaskID")?,
                        "task_type": row.try_get::<String, _>("TaskType")?,
                        "progress": row.try_get::<Option<i32>, _>("Progress")?,
                        "status": row.try_get::<String, _>("Status")?
                    }));
                }
                
                Ok(serde_json::json!({ "active_downloads": tasks }))
            }
        }
    }

    // Get episodes for a specific podcast - matches Python return_podcast_episodes function
    pub async fn return_podcast_episodes(&self, user_id: i32, podcast_id: i32) -> AppResult<Vec<crate::handlers::podcasts::Episode>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT 
                        "Podcasts".podcastid, "Podcasts".podcastname, "Episodes".episodeid,
                        "Episodes".episodetitle, "Episodes".episodepubdate, "Episodes".episodedescription,
                        CASE 
                            WHEN "Podcasts".usepodcastcoverscustomized = TRUE AND "Podcasts".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                            WHEN "Users".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                            ELSE "Episodes".episodeartwork
                        END as episodeartwork,
                        "Episodes".episodeurl, "Episodes".episodeduration,
                        "Episodes".completed,
                        "UserEpisodeHistory".listenduration, CAST("Episodes".episodeid AS VARCHAR) AS guid
                       FROM "Episodes"
                       INNER JOIN "Podcasts" ON "Episodes".podcastid = "Podcasts".podcastid
                       LEFT JOIN "Users" ON "Podcasts".userid = "Users".userid
                       LEFT JOIN "UserEpisodeHistory" ON "Episodes".episodeid = "UserEpisodeHistory".episodeid AND "UserEpisodeHistory".userid = $1
                       WHERE "Podcasts".podcastid = $2 AND "Podcasts".userid = $3
                       ORDER BY "Episodes".episodepubdate DESC"#
                )
                .bind(user_id)
                .bind(podcast_id)
                .bind(user_id)
                .fetch_all(pool)
                .await?;
                
                let mut episodes = Vec::new();
                for row in rows {
                    episodes.push(crate::handlers::podcasts::Episode {
                        podcastname: row.try_get("podcastname")?,
                        episodetitle: row.try_get("episodetitle")?,
                        episodepubdate: {
                            let naive = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                            naive.format("%Y-%m-%dT%H:%M:%S").to_string()
                        },
                        episodedescription: row.try_get("episodedescription")?,
                        episodeartwork: row.try_get("episodeartwork")?,
                        episodeurl: row.try_get("episodeurl")?,
                        episodeduration: row.try_get("episodeduration")?,
                        listenduration: row.try_get("listenduration").ok(),
                        episodeid: row.try_get("episodeid")?,
                        completed: row.try_get("completed")?,
                        saved: false, // Not included in this query
                        queued: false, // Not included in this query
                        downloaded: false, // Not included in this query
                        is_youtube: false, // This is for regular episodes
                    });
                }
                Ok(episodes)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(
                    "SELECT 
                        Podcasts.PodcastID, Podcasts.PodcastName, Episodes.EpisodeID,
                        Episodes.EpisodeTitle, Episodes.EpisodePubDate, Episodes.EpisodeDescription,
                        CASE 
                            WHEN Podcasts.UsePodcastCoversCustomized = TRUE AND Podcasts.UsePodcastCovers = TRUE THEN Podcasts.ArtworkURL
                            WHEN Users.UsePodcastCovers = TRUE THEN Podcasts.ArtworkURL
                            ELSE Episodes.EpisodeArtwork
                        END as EpisodeArtwork,
                        Episodes.EpisodeURL, Episodes.EpisodeDuration,
                        Episodes.Completed,
                        UserEpisodeHistory.ListenDuration, CAST(Episodes.EpisodeID AS CHAR) AS guid
                     FROM Episodes
                     INNER JOIN Podcasts ON Episodes.PodcastID = Podcasts.PodcastID
                     LEFT JOIN Users ON Podcasts.UserID = Users.UserID
                     LEFT JOIN UserEpisodeHistory ON Episodes.EpisodeID = UserEpisodeHistory.EpisodeID AND UserEpisodeHistory.UserID = ?
                     WHERE Podcasts.PodcastID = ? AND Podcasts.UserID = ?
                     ORDER BY Episodes.EpisodePubDate DESC"
                )
                .bind(user_id)
                .bind(podcast_id)
                .bind(user_id)
                .fetch_all(pool)
                .await?;
                
                let mut episodes = Vec::new();
                for row in rows {
                    episodes.push(crate::handlers::podcasts::Episode {
                        podcastname: row.try_get("PodcastName")?,
                        episodetitle: row.try_get("EpisodeTitle")?,
                        episodepubdate: {
                            let datetime = row.try_get::<sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>, _>("EpisodePubDate")?;
                            datetime.format("%Y-%m-%dT%H:%M:%S").to_string()
                        },
                        episodedescription: row.try_get("EpisodeDescription")?,
                        episodeartwork: row.try_get("EpisodeArtwork")?,
                        episodeurl: row.try_get("EpisodeURL")?,
                        episodeduration: row.try_get("EpisodeDuration")?,
                        listenduration: row.try_get("ListenDuration").ok(),
                        episodeid: row.try_get("EpisodeID")?,
                        completed: row.try_get::<i8, _>("Completed")? != 0,
                        saved: false, // Not included in this query
                        queued: false, // Not included in this query
                        downloaded: false, // Not included in this query
                        is_youtube: false, // This is for regular episodes
                    });
                }
                Ok(episodes)
            }
        }
    }

    // Get podcast ID from episode name and URL - matches Python get_podcast_id_from_episode_name function
    pub async fn get_podcast_id_from_episode_name(&self, episode_name: &str, episode_url: &str, user_id: i32) -> AppResult<Option<i32>> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Use UNION query exactly like Python version
                let row = sqlx::query(
                    r#"SELECT podcast_id FROM (
                        SELECT "Episodes".podcastid as podcast_id
                        FROM "Episodes"
                        INNER JOIN "Podcasts" ON "Episodes".podcastid = "Podcasts".podcastid
                        WHERE "Episodes".episodetitle = $1
                        AND "Episodes".episodeurl = $2
                        AND "Podcasts".userid = $3

                        UNION

                        SELECT "YouTubeVideos".podcastid as podcast_id
                        FROM "YouTubeVideos"
                        INNER JOIN "Podcasts" ON "YouTubeVideos".podcastid = "Podcasts".podcastid
                        WHERE "YouTubeVideos".videotitle = $4
                        AND "YouTubeVideos".videourl = $5
                        AND "Podcasts".userid = $6
                    ) combined_results
                    LIMIT 1"#
                )
                .bind(episode_name)
                .bind(episode_url)
                .bind(user_id)
                .bind(episode_name)
                .bind(episode_url)
                .bind(user_id)
                .fetch_optional(pool)
                .await?;
                
                if let Some(row) = row {
                    Ok(Some(row.try_get("podcast_id")?))
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                // Use UNION query exactly like Python version
                let row = sqlx::query(
                    "SELECT podcast_id FROM (
                        SELECT Episodes.PodcastID as podcast_id
                        FROM Episodes
                        INNER JOIN Podcasts ON Episodes.PodcastID = Podcasts.PodcastID
                        WHERE Episodes.EpisodeTitle = ?
                        AND Episodes.EpisodeURL = ?
                        AND Podcasts.UserID = ?

                        UNION

                        SELECT YouTubeVideos.PodcastID as podcast_id
                        FROM YouTubeVideos
                        INNER JOIN Podcasts ON YouTubeVideos.PodcastID = Podcasts.PodcastID
                        WHERE YouTubeVideos.VideoTitle = ?
                        AND YouTubeVideos.VideoURL = ?
                        AND Podcasts.UserID = ?
                    ) combined_results
                    LIMIT 1"
                )
                .bind(episode_name)
                .bind(episode_url)
                .bind(user_id)
                .bind(episode_name)
                .bind(episode_url)
                .bind(user_id)
                .fetch_optional(pool)
                .await?;
                
                if let Some(row) = row {
                    Ok(Some(row.try_get("podcast_id")?))
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Get episode metadata - matches Python get_episode_metadata function exactly
    pub async fn get_episode_metadata(&self, episode_id: i32, user_id: i32, person_episode: bool, is_youtube: bool) -> AppResult<serde_json::Value> {
        match self {
            DatabasePool::Postgres(pool) => {
                if is_youtube {
                    // Query for YouTube videos
                    let row = sqlx::query(
                        r#"SELECT "Podcasts".podcastid, "Podcasts".podcastindexid, "Podcasts".feedurl,
                                "Podcasts".podcastname, "Podcasts".artworkurl,
                                "YouTubeVideos".videotitle as episodetitle,
                                "YouTubeVideos".publishedat as episodepubdate,
                                "YouTubeVideos".videodescription as episodedescription,
                                "YouTubeVideos".thumbnailurl as episodeartwork,
                                "YouTubeVideos".videourl as episodeurl,
                                "YouTubeVideos".duration as episodeduration,
                                "YouTubeVideos".videoid as episodeid,
                                "YouTubeVideos".listenposition as listenduration,
                                "YouTubeVideos".completed,
                                CASE WHEN q.episodeid IS NOT NULL THEN true ELSE false END as is_queued,
                                CASE WHEN s.episodeid IS NOT NULL THEN true ELSE false END as is_saved,
                                CASE WHEN d.episodeid IS NOT NULL THEN true ELSE false END as is_downloaded,
                                TRUE::boolean as is_youtube
                        FROM "YouTubeVideos"
                        INNER JOIN "Podcasts" ON "YouTubeVideos".podcastid = "Podcasts".podcastid
                        LEFT JOIN "EpisodeQueue" q ON "YouTubeVideos".videoid = q.episodeid AND q.userid = $1
                        LEFT JOIN "SavedEpisodes" s ON "YouTubeVideos".videoid = s.episodeid AND s.userid = $1
                        LEFT JOIN "DownloadedEpisodes" d ON "YouTubeVideos".videoid = d.episodeid AND d.userid = $1
                        WHERE "YouTubeVideos".videoid = $2 AND "Podcasts".userid = $1"#
                    )
                    .bind(user_id)
                    .bind(episode_id)
                    .fetch_optional(pool)
                    .await?;
                    
                    if let Some(row) = row {
                        let naive_date = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                        let episode_pubdate = naive_date.format("%Y-%m-%dT%H:%M:%S").to_string();
                        
                        return Ok(serde_json::json!({
                            "podcastid": row.try_get::<i32, _>("podcastid")?,
                            "podcastindexid": row.try_get::<Option<i32>, _>("podcastindexid")?,
                            "feedurl": row.try_get::<String, _>("feedurl").unwrap_or_default(),
                            "podcastname": row.try_get::<String, _>("podcastname")?,
                            "artworkurl": row.try_get::<Option<String>, _>("artworkurl").unwrap_or_default().unwrap_or_default(),
                            "episodetitle": row.try_get::<String, _>("episodetitle")?,
                            "episodepubdate": episode_pubdate,
                            "episodedescription": row.try_get::<String, _>("episodedescription")?,
                            "episodeartwork": row.try_get::<String, _>("episodeartwork")?,
                            "episodeurl": row.try_get::<String, _>("episodeurl")?,
                            "episodeduration": row.try_get::<i32, _>("episodeduration")?,
                            "episodeid": row.try_get::<i32, _>("episodeid")?,
                            "listenduration": row.try_get::<Option<i32>, _>("listenduration")?,
                            "completed": row.try_get::<bool, _>("completed")?,
                            "is_queued": row.try_get::<bool, _>("is_queued")?,
                            "is_saved": row.try_get::<bool, _>("is_saved")?,
                            "is_downloaded": row.try_get::<bool, _>("is_downloaded")?,
                            "is_youtube": row.try_get::<bool, _>("is_youtube")?,
                        }));
                    }
                } else if person_episode {
                    // Query for person episodes - matches Python implementation
                    let row = sqlx::query(
                        r#"SELECT 
                            p.podcastid, 
                            p.podcastindexid, 
                            p.feedurl,
                            p.podcastname, 
                            p.artworkurl,
                            pe.episodetitle,
                            pe.episodepubdate,
                            pe.episodedescription,
                            pe.episodeartwork,
                            pe.episodeurl,
                            pe.episodeduration,
                            pe.episodeid,
                            COALESCE(e.episodeid, pe.episodeid) as real_episode_id,
                            CASE WHEN q.episodeid IS NOT NULL THEN true ELSE false END as is_queued,
                            CASE WHEN s.episodeid IS NOT NULL THEN true ELSE false END as is_saved,
                            CASE WHEN d.episodeid IS NOT NULL THEN true ELSE false END as is_downloaded,
                            FALSE::boolean as is_youtube
                        FROM "PeopleEpisodes" pe
                        INNER JOIN "Podcasts" p ON pe.podcastid = p.podcastid
                        LEFT JOIN "Episodes" e ON (pe.episodetitle = e.episodetitle AND pe.episodeurl = e.episodeurl)
                        LEFT JOIN "EpisodeQueue" q ON COALESCE(e.episodeid, pe.episodeid) = q.episodeid AND q.userid = $1
                        LEFT JOIN "SavedEpisodes" s ON COALESCE(e.episodeid, pe.episodeid) = s.episodeid AND s.userid = $1
                        LEFT JOIN "DownloadedEpisodes" d ON COALESCE(e.episodeid, pe.episodeid) = d.episodeid AND d.userid = $1
                        WHERE pe.episodeid = $2"#
                    )
                    .bind(user_id)
                    .bind(episode_id)
                    .fetch_optional(pool)
                    .await?;
                    
                    if let Some(row) = row {
                        let naive_date = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                        let episode_pubdate = naive_date.format("%Y-%m-%dT%H:%M:%S").to_string();
                        let real_episode_id: i32 = row.try_get("real_episode_id")?;
                        
                        // Get listen history using the real episode ID - matches Python implementation
                        let listen_history = sqlx::query(
                            r#"SELECT "UserEpisodeHistory".listenduration, "Episodes".completed
                               FROM "Episodes"
                               LEFT JOIN "UserEpisodeHistory" ON 
                                   "Episodes".episodeid = "UserEpisodeHistory".episodeid
                                   AND "UserEpisodeHistory".userid = $2
                               WHERE "Episodes".episodeid = $1"#
                        )
                        .bind(real_episode_id)
                        .bind(user_id)
                        .fetch_optional(pool)
                        .await?;
                        
                        let (listenduration, completed) = if let Some(history) = listen_history {
                            (
                                history.try_get::<Option<i32>, _>("listenduration")?,
                                history.try_get::<bool, _>("completed")?
                            )
                        } else {
                            (None, false)
                        };
                        
                        return Ok(serde_json::json!({
                            "podcastid": row.try_get::<i32, _>("podcastid")?,
                            "podcastindexid": row.try_get::<Option<i32>, _>("podcastindexid")?,
                            "feedurl": row.try_get::<String, _>("feedurl").unwrap_or_default(),
                            "podcastname": row.try_get::<String, _>("podcastname")?,
                            "artworkurl": row.try_get::<Option<String>, _>("artworkurl").unwrap_or_default().unwrap_or_default(),
                            "episodetitle": row.try_get::<String, _>("episodetitle")?,
                            "episodepubdate": episode_pubdate,
                            "episodedescription": row.try_get::<String, _>("episodedescription")?,
                            "episodeartwork": row.try_get::<String, _>("episodeartwork")?,
                            "episodeurl": row.try_get::<String, _>("episodeurl")?,
                            "episodeduration": row.try_get::<i32, _>("episodeduration")?,
                            "episodeid": real_episode_id, // Return the real episode ID, not person episode ID
                            "listenduration": listenduration,
                            "completed": completed,
                            "is_queued": row.try_get::<bool, _>("is_queued")?,
                            "is_saved": row.try_get::<bool, _>("is_saved")?,
                            "is_downloaded": row.try_get::<bool, _>("is_downloaded")?,
                            "is_youtube": row.try_get::<bool, _>("is_youtube")?,
                        }));
                    }
                }
                
                // Query for regular episodes
                let row = sqlx::query(
                    r#"SELECT "Podcasts".podcastid, "Podcasts".podcastindexid, "Podcasts".feedurl,
                            "Podcasts".podcastname, "Podcasts".artworkurl,
                            "Episodes".episodetitle,
                            "Episodes".episodepubdate,
                            "Episodes".episodedescription,
                            "Episodes".episodeartwork,
                            "Episodes".episodeurl,
                            "Episodes".episodeduration,
                            "Episodes".episodeid,
                            "UserEpisodeHistory".listenduration,
                            "Episodes".completed,
                            CASE WHEN q.episodeid IS NOT NULL THEN true ELSE false END as is_queued,
                            CASE WHEN s.episodeid IS NOT NULL THEN true ELSE false END as is_saved,
                            CASE WHEN d.episodeid IS NOT NULL THEN true ELSE false END as is_downloaded,
                            FALSE::boolean as is_youtube
                    FROM "Episodes"
                    INNER JOIN "Podcasts" ON "Episodes".podcastid = "Podcasts".podcastid
                    LEFT JOIN "UserEpisodeHistory" ON "Episodes".episodeid = "UserEpisodeHistory".episodeid AND "UserEpisodeHistory".userid = $1
                    LEFT JOIN "EpisodeQueue" q ON "Episodes".episodeid = q.episodeid AND q.userid = $1
                    LEFT JOIN "SavedEpisodes" s ON "Episodes".episodeid = s.episodeid AND s.userid = $1
                    LEFT JOIN "DownloadedEpisodes" d ON "Episodes".episodeid = d.episodeid AND d.userid = $1
                    WHERE "Episodes".episodeid = $2 AND "Podcasts".userid = $1"#
                )
                .bind(user_id)
                .bind(episode_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    let naive_date = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                    let episode_pubdate = naive_date.format("%Y-%m-%dT%H:%M:%S").to_string();
                    
                    Ok(serde_json::json!({
                        "podcastid": row.try_get::<i32, _>("podcastid")?,
                        "podcastindexid": row.try_get::<Option<i32>, _>("podcastindexid")?,
                        "feedurl": row.try_get::<String, _>("feedurl").unwrap_or_default(),
                        "podcastname": row.try_get::<String, _>("podcastname")?,
                        "artworkurl": row.try_get::<Option<String>, _>("artworkurl").unwrap_or_default().unwrap_or_default(),
                        "episodetitle": row.try_get::<String, _>("episodetitle")?,
                        "episodepubdate": episode_pubdate,
                        "episodedescription": row.try_get::<String, _>("episodedescription")?,
                        "episodeartwork": row.try_get::<String, _>("episodeartwork")?,
                        "episodeurl": row.try_get::<String, _>("episodeurl")?,
                        "episodeduration": row.try_get::<i32, _>("episodeduration")?,
                        "episodeid": row.try_get::<i32, _>("episodeid")?,
                        "listenduration": row.try_get::<Option<i32>, _>("listenduration")?,
                        "completed": row.try_get::<bool, _>("completed")?,
                        "is_queued": row.try_get::<bool, _>("is_queued")?,
                        "is_saved": row.try_get::<bool, _>("is_saved")?,
                        "is_downloaded": row.try_get::<bool, _>("is_downloaded")?,
                        "is_youtube": row.try_get::<bool, _>("is_youtube")?,
                    }))
                } else {
                    Err(AppError::not_found("Episode not found"))
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query(
                    r#"SELECT 
                        Podcasts.PodcastID,
                        Podcasts.PodcastIndexID,
                        Podcasts.FeedURL,
                        Podcasts.PodcastName,
                        Podcasts.ArtworkURL,
                        Episodes.EpisodeTitle,
                        Episodes.EpisodePubDate,
                        Episodes.EpisodeDescription,
                        Episodes.EpisodeArtwork,
                        Episodes.EpisodeURL,
                        Episodes.EpisodeDuration,
                        Episodes.EpisodeID,
                        UserEpisodeHistory.ListenDuration,
                        Episodes.Completed,
                        CASE WHEN q.EpisodeID IS NOT NULL THEN 1 ELSE 0 END as is_queued,
                        CASE WHEN s.EpisodeID IS NOT NULL THEN 1 ELSE 0 END as is_saved,
                        CASE WHEN d.EpisodeID IS NOT NULL THEN 1 ELSE 0 END as is_downloaded,
                        0 as is_youtube
                    FROM Episodes
                    INNER JOIN Podcasts ON Episodes.PodcastID = Podcasts.PodcastID
                    LEFT JOIN UserEpisodeHistory ON Episodes.EpisodeID = UserEpisodeHistory.EpisodeID AND UserEpisodeHistory.UserID = ?
                    LEFT JOIN EpisodeQueue q ON Episodes.EpisodeID = q.EpisodeID AND q.UserID = ?
                    LEFT JOIN SavedEpisodes s ON Episodes.EpisodeID = s.EpisodeID AND s.UserID = ?
                    LEFT JOIN DownloadedEpisodes d ON Episodes.EpisodeID = d.EpisodeID AND d.UserID = ?
                    WHERE Episodes.EpisodeID = ? AND Podcasts.UserID = ?"#
                )
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(episode_id)
                .bind(user_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    let datetime = row.try_get::<sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>, _>("EpisodePubDate")?;
                    
                    Ok(serde_json::json!({
                        "podcastid": row.try_get::<i32, _>("PodcastID")?,
                        "podcastindexid": row.try_get::<Option<i32>, _>("PodcastIndexID")?,
                        "feedurl": row.try_get::<String, _>("FeedURL").unwrap_or_default(),
                        "podcastname": row.try_get::<String, _>("PodcastName")?,
                        "artworkurl": row.try_get::<Option<String>, _>("ArtworkURL").unwrap_or_default().unwrap_or_default(),
                        "episodetitle": row.try_get::<String, _>("EpisodeTitle")?,
                        "episodepubdate": datetime.to_rfc3339(),
                        "episodedescription": row.try_get::<String, _>("EpisodeDescription")?,
                        "episodeartwork": row.try_get::<String, _>("EpisodeArtwork")?,
                        "episodeurl": row.try_get::<String, _>("EpisodeURL")?,
                        "episodeduration": row.try_get::<i32, _>("EpisodeDuration")?,
                        "episodeid": row.try_get::<i32, _>("EpisodeID")?,
                        "listenduration": row.try_get::<Option<i32>, _>("ListenDuration")?,
                        "completed": row.try_get::<i8, _>("Completed")? != 0,
                        "is_queued": row.try_get::<i32, _>("is_queued")? != 0,
                        "is_saved": row.try_get::<i32, _>("is_saved")? != 0,
                        "is_downloaded": row.try_get::<i32, _>("is_downloaded")? != 0,
                        "is_youtube": row.try_get::<i32, _>("is_youtube")? != 0,
                    }))
                } else {
                    Err(AppError::not_found("Episode not found"))
                }
            }
        }
    }

    // Fetch podcasting 2.0 data for episode
    pub async fn fetch_podcasting_2_data(&self, episode_id: i32, user_id: i32) -> AppResult<serde_json::Value> {
        // Get episode metadata and podcast details
        let episode_metadata = self.get_episode_metadata(episode_id, user_id, false, false).await?;
        
        let episode_url = episode_metadata["episodeurl"].as_str()
            .ok_or_else(|| AppError::internal("Episode URL not found"))?;
        let podcast_id = episode_metadata["podcastid"].as_i64()
            .ok_or_else(|| AppError::internal("Podcast ID not found"))? as i32;
        
        let podcast_details = self.get_podcast_details(user_id, podcast_id).await?;
        
        let feed_url = podcast_details["feedurl"].as_str()
            .ok_or_else(|| AppError::internal("Feed URL not found"))?;
        
        // Get authentication if available
        let username = podcast_details.get("username").and_then(|v| v.as_str());
        let password = podcast_details.get("password").and_then(|v| v.as_str());
        
        // Fetch the RSS feed with authentication if needed
        let feed_content = self.try_fetch_feed(feed_url, username, password).await?;
        
        // Get podcast index ID for PodPeople API fallback
        let podcast_index_id = self.get_podcast_index_id(podcast_id).await.ok().flatten();
        
        // Parse podcasting 2.0 features
        let chapters_url = self.parse_chapters(&feed_content, episode_url)?;
        let transcripts = self.parse_transcripts(&feed_content, episode_url)?;
        let people = self.parse_people(&feed_content, Some(episode_url), podcast_index_id).await?;
        
        // Fetch chapters data if URL is available
        let mut chapters_data = serde_json::Value::Array(vec![]);
        if let Some(url) = chapters_url {
            if let Ok(chapters) = self.fetch_chapters_data(&url, feed_url, username, password).await {
                chapters_data = chapters;
            }
        }
        
        Ok(serde_json::json!({
            "chapters": chapters_data,
            "transcripts": transcripts,
            "people": people
        }))
    }

    // Parse chapters from RSS feed content - matches Python parse_chapters function
    fn parse_chapters(&self, feed_content: &str, episode_url: &str) -> AppResult<Option<String>> {
        // Simple string-based parsing to match the Python implementation exactly
        let lines: Vec<&str> = feed_content.lines().collect();
        let mut in_item = false;
        let mut found_episode = false;
        
        for line in lines {
            let trimmed = line.trim();
            
            if trimmed.starts_with("<item>") || trimmed.starts_with("<item ") {
                in_item = true;
                found_episode = false;
            } else if trimmed.starts_with("</item>") {
                if found_episode {
                    break; // Exit after processing the matching episode
                }
                in_item = false;
                found_episode = false;
            } else if in_item && trimmed.contains("<enclosure") && trimmed.contains(&format!("url=\"{}\"", episode_url)) {
                found_episode = true;
            } else if in_item && found_episode && (trimmed.contains("podcast:chapters") || trimmed.contains(":chapters")) {
                // Extract URL from the chapters element
                if let Some(start) = trimmed.find("url=\"") {
                    let start_idx = start + 5; // Skip 'url="'
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        let url = &trimmed[start_idx..start_idx + end];
                        return Ok(Some(url.to_string()));
                    }
                }
            }
        }
        
        Ok(None)
    }

    // Parse transcripts from RSS feed content - matches Python parse_transcripts function  
    fn parse_transcripts(&self, feed_content: &str, episode_url: &str) -> AppResult<serde_json::Value> {
        // Simple string-based parsing to match the Python implementation exactly
        let lines: Vec<&str> = feed_content.lines().collect();
        let mut in_item = false;
        let mut found_episode = false;
        let mut transcripts = Vec::new();
        
        for line in lines {
            let trimmed = line.trim();
            
            if trimmed.starts_with("<item>") || trimmed.starts_with("<item ") {
                in_item = true;
                found_episode = false;
            } else if trimmed.starts_with("</item>") {
                if found_episode {
                    break; // Exit after processing the matching episode
                }
                in_item = false;
                found_episode = false;
            } else if in_item && trimmed.contains("<enclosure") && trimmed.contains(&format!("url=\"{}\"", episode_url)) {
                found_episode = true;
            } else if in_item && found_episode && (trimmed.contains("podcast:transcript") || trimmed.contains(":transcript")) {
                // Extract attributes from the transcript element
                let mut transcript_url = None;
                let mut transcript_type = None;
                let mut transcript_language = None;
                let mut transcript_rel = None;
                
                // Extract URL
                if let Some(start) = trimmed.find("url=\"") {
                    let start_idx = start + 5;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        transcript_url = Some(trimmed[start_idx..start_idx + end].to_string());
                    }
                }
                
                // Extract type
                if let Some(start) = trimmed.find("type=\"") {
                    let start_idx = start + 6;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        transcript_type = Some(trimmed[start_idx..start_idx + end].to_string());
                    }
                }
                
                // Extract language
                if let Some(start) = trimmed.find("language=\"") {
                    let start_idx = start + 10;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        transcript_language = Some(trimmed[start_idx..start_idx + end].to_string());
                    }
                }
                
                // Extract rel
                if let Some(start) = trimmed.find("rel=\"") {
                    let start_idx = start + 5;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        transcript_rel = Some(trimmed[start_idx..start_idx + end].to_string());
                    }
                }
                
                transcripts.push(serde_json::json!({
                    "url": transcript_url,
                    "mime_type": transcript_type,
                    "language": transcript_language,
                    "rel": transcript_rel
                }));
            }
        }
        
        Ok(serde_json::Value::Array(transcripts))
    }

    // Parse people from RSS feed content - matches Python parse_people function
    async fn parse_people(&self, feed_content: &str, episode_url: Option<&str>, _podcast_index_id: Option<i32>) -> AppResult<serde_json::Value> {
        use std::collections::HashSet;
        
        // Simple string-based parsing to match the Python implementation exactly
        let lines: Vec<&str> = feed_content.lines().collect();
        let mut in_item = false;
        let mut in_channel = false;
        let mut found_episode = false;
        let mut people = Vec::new();
        let mut seen_people = HashSet::new();
        
        for line in lines {
            let trimmed = line.trim();
            
            if trimmed.starts_with("<channel>") || trimmed.starts_with("<channel ") {
                in_channel = true;
            } else if trimmed.starts_with("</channel>") {
                in_channel = false;
            } else if trimmed.starts_with("<item>") || trimmed.starts_with("<item ") {
                in_item = true;
                found_episode = false;
            } else if trimmed.starts_with("</item>") {
                if found_episode && !people.is_empty() {
                    break; // Exit after processing the matching episode
                }
                in_item = false;
                found_episode = false;
            } else if in_item && trimmed.contains("<enclosure") {
                if let Some(episode_url) = episode_url {
                    if trimmed.contains(&format!("url=\"{}\"", episode_url)) {
                        found_episode = true;
                    }
                }
            } else if (trimmed.contains("podcast:person") || trimmed.contains(":person")) && ((in_item && found_episode) || (in_channel && !in_item)) {
                // Extract person data from the line
                let mut name = String::new();
                let mut role = None;
                let mut group = None;
                let mut img = None;
                let mut href = None;
                
                // Extract name (text between tags)
                if let Some(start) = trimmed.find(">") {
                    if let Some(end) = trimmed.rfind("</") {
                        if start < end {
                            name = trimmed[start + 1..end].trim().to_string();
                        }
                    }
                }
                
                // Extract attributes
                if let Some(start) = trimmed.find("role=\"") {
                    let start_idx = start + 6;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        role = Some(trimmed[start_idx..start_idx + end].to_string());
                    }
                }
                
                if let Some(start) = trimmed.find("group=\"") {
                    let start_idx = start + 7;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        group = Some(trimmed[start_idx..start_idx + end].to_string());
                    }
                }
                
                if let Some(start) = trimmed.find("img=\"") {
                    let start_idx = start + 5;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        img = Some(trimmed[start_idx..start_idx + end].to_string());
                    }
                }
                
                if let Some(start) = trimmed.find("href=\"") {
                    let start_idx = start + 6;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        href = Some(trimmed[start_idx..start_idx + end].to_string());
                    }
                }
                
                if !name.is_empty() {
                    // Create a unique key for deduplication
                    let unique_key = format!("{}|{}|{}|{}", 
                        name, 
                        role.as_deref().unwrap_or(""), 
                        group.as_deref().unwrap_or(""),
                        img.as_deref().unwrap_or("")
                    );
                    
                    if !seen_people.contains(&unique_key) {
                        seen_people.insert(unique_key);
                        people.push(serde_json::json!({
                            "name": name,
                            "role": role,
                            "group": group,
                            "img": img,
                            "href": href
                        }));
                    }
                }
            }
        }
        
        Ok(serde_json::Value::Array(people))
    }

    // Fetch chapters data from external JSON URL - matches Python chapter fetching logic
    async fn fetch_chapters_data(&self, chapters_url: &str, feed_url: &str, username: Option<&str>, password: Option<&str>) -> AppResult<serde_json::Value> {
        let client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(5))
            .build()?;
        
        let mut request = client.get(chapters_url)
            .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
            .header("Accept", "application/json, text/javascript, */*; q=0.01")
            .header("Accept-Language", "en-US,en;q=0.9")
            .header("Referer", feed_url);
        
        // Use same auth for chapters if it's from the same domain
        if chapters_url.starts_with(feed_url) {
            if let (Some(user), Some(pass)) = (username, password) {
                request = request.basic_auth(user, Some(pass));
            }
        }
        
        let response = request.send().await?;
        let json_data: serde_json::Value = response.json().await?;
        
        Ok(json_data.get("chapters").unwrap_or(&serde_json::Value::Array(vec![])).clone())
    }

    // Get auto download status for user
    pub async fn get_auto_download_status(&self, user_id: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT auto_download_episodes FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                    
                if let Some(row) = row {
                    Ok(row.try_get::<bool, _>("auto_download_episodes")?)
                } else {
                    Ok(false)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT AutoDownloadEpisodes FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                    
                if let Some(row) = row {
                    Ok(row.try_get::<bool, _>("AutoDownloadEpisodes")?)
                } else {
                    Ok(false)
                }
            }
        }
    }


    // Fetch podcasting 2.0 podcast data
    pub async fn fetch_podcasting_2_pod_data(&self, podcast_id: i32, user_id: i32) -> AppResult<serde_json::Value> {
        // Get podcast details to fetch the feed URL and authentication
        let podcast_details = self.get_podcast_details(user_id, podcast_id).await?;
        
        let feed_url = podcast_details["feedurl"].as_str()
            .ok_or_else(|| AppError::internal("Feed URL not found"))?;
        
        // Get authentication if available
        let username = podcast_details.get("username").and_then(|v| v.as_str());
        let password = podcast_details.get("password").and_then(|v| v.as_str());
        
        // Fetch the RSS feed with authentication if needed
        let feed_content = self.try_fetch_feed(feed_url, username, password).await?;
        
        // Get podcast index ID for PodPeople API fallback
        let podcast_index_id = self.get_podcast_index_id(podcast_id).await.ok().flatten();
        
        // Parse podcasting 2.0 features at the podcast level
        let people = self.parse_podcast_people(&feed_content, podcast_index_id).await?;
        let podroll = self.parse_podroll(&feed_content)?;
        let funding = self.parse_funding(&feed_content)?;
        let value = self.parse_value(&feed_content)?;
        
        Ok(serde_json::json!({
            "people": people,
            "podroll": podroll,
            "funding": funding,
            "value": value
        }))
    }

    // Parse podcast people/hosts from RSS feed content - matches Python get_podcast_hosts function
    async fn parse_podcast_people(&self, feed_content: &str, podcast_index_id: Option<i32>) -> AppResult<serde_json::Value> {
        use std::collections::HashSet;
        
        // Simple string-based parsing to match the Python implementation exactly
        let lines: Vec<&str> = feed_content.lines().collect();
        let mut in_channel = false;
        let mut people = Vec::new();
        let mut seen_people = HashSet::new();
        
        for line in lines {
            let trimmed = line.trim();
            
            if trimmed.starts_with("<channel>") || trimmed.starts_with("<channel ") {
                in_channel = true;
            } else if trimmed.starts_with("</channel>") {
                in_channel = false;
            } else if in_channel && (trimmed.contains("podcast:person") || trimmed.contains(":person")) {
                // Extract person data from the line
                let mut name = String::new();
                let mut role = None;
                let mut group = None;
                let mut img = None;
                let mut href = None;
                
                // Extract name (text between tags)
                if let Some(start) = trimmed.find(">") {
                    if let Some(end) = trimmed.rfind("</") {
                        if start < end {
                            name = trimmed[start + 1..end].trim().to_string();
                        }
                    }
                }
                
                // Extract attributes
                if let Some(start) = trimmed.find("role=\"") {
                    let start_idx = start + 6;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        role = Some(trimmed[start_idx..start_idx + end].to_string());
                    }
                }
                
                if let Some(start) = trimmed.find("group=\"") {
                    let start_idx = start + 7;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        group = Some(trimmed[start_idx..start_idx + end].to_string());
                    }
                }
                
                if let Some(start) = trimmed.find("img=\"") {
                    let start_idx = start + 5;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        img = Some(trimmed[start_idx..start_idx + end].to_string());
                    }
                }
                
                if let Some(start) = trimmed.find("href=\"") {
                    let start_idx = start + 6;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        href = Some(trimmed[start_idx..start_idx + end].to_string());
                    }
                }
                
                if !name.is_empty() {
                    // Only include hosts, not guests (filter by role)
                    if let Some(ref person_role) = role {
                        if person_role.to_lowercase() != "host" {
                            continue; // Skip non-hosts
                        }
                    } else {
                        // If no role specified, assume it's a host (default behavior)
                    }
                    
                    // Create a unique key for deduplication
                    let unique_key = format!("{}|{}|{}|{}", 
                        name, 
                        role.as_deref().unwrap_or(""), 
                        group.as_deref().unwrap_or(""),
                        img.as_deref().unwrap_or("")
                    );
                    
                    if !seen_people.contains(&unique_key) {
                        seen_people.insert(unique_key);
                        people.push(serde_json::json!({
                            "name": name,
                            "role": role,
                            "group": group,
                            "img": img,
                            "href": href
                        }));
                    }
                }
            }
        }
        
        // If no hosts found, try to get them from PodPeople API (like Python does)
        if people.is_empty() {
            if let Some(index_id) = podcast_index_id {
                if let Ok(podpeople_hosts) = self.get_podpeople_hosts(index_id).await {
                    people = podpeople_hosts;
                }
            }
            
            // Final fallback if PodPeople API also fails
            if people.is_empty() {
                people.push(serde_json::json!({
                    "name": "Unknown Host",
                    "role": "Host",
                    "group": null,
                    "img": null,
                    "href": null,
                    "description": "No host information available."
                }));
            }
        }
        
        Ok(serde_json::Value::Array(people))
    }

    // Get podcast index ID - matches Python get_podcast_index_id function
    pub async fn get_podcast_index_id(&self, podcast_id: i32) -> AppResult<Option<i32>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT podcastindexid FROM "Podcasts" WHERE podcastid = $1"#)
                    .bind(podcast_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(row.try_get("podcastindexid").ok())
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT PodcastIndexID FROM Podcasts WHERE PodcastID = ?")
                    .bind(podcast_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(row.try_get("PodcastIndexID").ok())
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Get hosts from PodPeople API - matches Python get_podpeople_hosts function
    async fn get_podpeople_hosts(&self, podcast_index_id: i32) -> AppResult<Vec<serde_json::Value>> {
        use reqwest;
        
        // Get PodPeople API URL from environment, default to pinepods.online
        let people_url = std::env::var("PEOPLE_API_URL")
            .unwrap_or_else(|_| "https://people.pinepods.online".to_string());
        
        let client = reqwest::Client::new();
        let url = format!("{}/api/hosts/{}", people_url, podcast_index_id);
        
        // Make request with 5 second timeout (matches Python)
        let response = tokio::time::timeout(
            std::time::Duration::from_secs(5),
            client.get(&url).send()
        ).await;
        
        match response {
            Ok(Ok(resp)) => {
                if resp.status().is_success() {
                    if let Ok(hosts_data) = resp.json::<serde_json::Value>().await {
                        if let Some(hosts_array) = hosts_data.as_array() {
                            let mut people = Vec::new();
                            
                            for host in hosts_array {
                                let name = host.get("name")
                                    .and_then(|v| v.as_str())
                                    .unwrap_or("Unknown Host")
                                    .to_string();
                                
                                let role = host.get("role")
                                    .and_then(|v| v.as_str())
                                    .unwrap_or("Host")
                                    .to_string();
                                
                                let img = host.get("img")
                                    .and_then(|v| v.as_str())
                                    .map(|s| s.to_string());
                                
                                let href = host.get("link")
                                    .and_then(|v| v.as_str())
                                    .map(|s| s.to_string());
                                
                                let description = host.get("description")
                                    .and_then(|v| v.as_str())
                                    .map(|s| s.to_string());
                                
                                people.push(serde_json::json!({
                                    "name": name,
                                    "role": role,
                                    "group": null,
                                    "img": img,
                                    "href": href,
                                    "description": description
                                }));
                            }
                            
                            return Ok(people);
                        }
                    }
                }
            }
            Ok(Err(e)) => {
                tracing::error!("HTTP error fetching PodPeople hosts: {}", e);
            }
            Err(_) => {
                tracing::error!("Timeout fetching PodPeople hosts for podcast {}", podcast_index_id);
            }
        }
        
        // Return empty vector on any error (matches Python behavior)
        Ok(vec![])
    }

    // Parse podroll from RSS feed content - matches Python parse_podroll function
    fn parse_podroll(&self, feed_content: &str) -> AppResult<serde_json::Value> {
        let lines: Vec<&str> = feed_content.lines().collect();
        let mut in_channel = false;
        let mut in_podroll = false;
        let mut podroll = Vec::new();
        
        for line in lines {
            let trimmed = line.trim();
            
            if trimmed.starts_with("<channel>") || trimmed.starts_with("<channel ") {
                in_channel = true;
            } else if trimmed.starts_with("</channel>") {
                in_channel = false;
                in_podroll = false;
            } else if in_channel && (trimmed.contains("podcast:podroll") || trimmed.contains(":podroll")) {
                if trimmed.starts_with("<") && !trimmed.contains("</") {
                    in_podroll = true;
                }
            } else if in_podroll && (trimmed.contains("</podcast:podroll") || trimmed.contains("</podroll")) {
                in_podroll = false;
            } else if in_podroll && (trimmed.contains("podcast:remoteItem") || trimmed.contains(":remoteItem")) {
                // Extract feedGuid from remoteItem
                if let Some(start) = trimmed.find("feedGuid=\"") {
                    let start_idx = start + 10;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        let feed_guid = &trimmed[start_idx..start_idx + end];
                        podroll.push(serde_json::json!({
                            "feed_guid": feed_guid
                        }));
                    }
                }
            }
        }
        
        Ok(serde_json::Value::Array(podroll))
    }

    // Parse funding from RSS feed content - matches Python parse_funding function
    fn parse_funding(&self, feed_content: &str) -> AppResult<serde_json::Value> {
        let lines: Vec<&str> = feed_content.lines().collect();
        let mut in_channel = false;
        let mut funding = Vec::new();
        
        for line in lines {
            let trimmed = line.trim();
            
            if trimmed.starts_with("<channel>") || trimmed.starts_with("<channel ") {
                in_channel = true;
            } else if trimmed.starts_with("</channel>") {
                in_channel = false;
            } else if in_channel && (trimmed.contains("podcast:funding") || trimmed.contains(":funding")) {
                let mut url = None;
                let mut description = String::new();
                
                // Extract URL attribute
                if let Some(start) = trimmed.find("url=\"") {
                    let start_idx = start + 5;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        url = Some(trimmed[start_idx..start_idx + end].to_string());
                    }
                }
                
                // Extract description (text between tags)
                if let Some(start) = trimmed.find(">") {
                    if let Some(end) = trimmed.rfind("</") {
                        if start < end {
                            description = trimmed[start + 1..end].trim().to_string();
                        }
                    }
                }
                
                if let Some(url) = url {
                    funding.push(serde_json::json!({
                        "url": url,
                        "description": description
                    }));
                }
            }
        }
        
        Ok(serde_json::Value::Array(funding))
    }

    // Parse value blocks from RSS feed content - matches Python parse_value function
    fn parse_value(&self, feed_content: &str) -> AppResult<serde_json::Value> {
        let lines: Vec<&str> = feed_content.lines().collect();
        let mut in_channel = false;
        let mut in_value = false;
        let mut value_blocks = Vec::new();
        let mut current_value_block = None;
        let mut recipients = Vec::new();
        
        for line in lines {
            let trimmed = line.trim();
            
            if trimmed.starts_with("<channel>") || trimmed.starts_with("<channel ") {
                in_channel = true;
            } else if trimmed.starts_with("</channel>") {
                in_channel = false;
                in_value = false;
            } else if in_channel && (trimmed.contains("podcast:value") || trimmed.contains(":value")) {
                if trimmed.starts_with("<") && !trimmed.contains("</") && !trimmed.contains("Recipient") {
                    // Start of value block
                    in_value = true;
                    let mut value_type = None;
                    let mut method = None;
                    let mut suggested = None;
                    
                    // Extract attributes
                    if let Some(start) = trimmed.find("type=\"") {
                        let start_idx = start + 6;
                        if let Some(end) = trimmed[start_idx..].find("\"") {
                            value_type = Some(trimmed[start_idx..start_idx + end].to_string());
                        }
                    }
                    
                    if let Some(start) = trimmed.find("method=\"") {
                        let start_idx = start + 8;
                        if let Some(end) = trimmed[start_idx..].find("\"") {
                            method = Some(trimmed[start_idx..start_idx + end].to_string());
                        }
                    }
                    
                    if let Some(start) = trimmed.find("suggested=\"") {
                        let start_idx = start + 11;
                        if let Some(end) = trimmed[start_idx..].find("\"") {
                            suggested = Some(trimmed[start_idx..start_idx + end].to_string());
                        }
                    }
                    
                    current_value_block = Some(serde_json::json!({
                        "type": value_type,
                        "method": method,
                        "suggested": suggested
                    }));
                    recipients = Vec::new();
                }
            } else if in_value && (trimmed.contains("</podcast:value") || trimmed.contains("</value")) {
                // End of value block
                in_value = false;
                if let Some(mut block) = current_value_block.take() {
                    block["recipients"] = serde_json::Value::Array(recipients.clone());
                    value_blocks.push(block);
                }
                recipients.clear();
            } else if in_value && (trimmed.contains("podcast:valueRecipient") || trimmed.contains(":valueRecipient")) {
                // Parse value recipient
                let mut name = None;
                let mut recipient_type = None;
                let mut address = None;
                let mut split = None;
                
                if let Some(start) = trimmed.find("name=\"") {
                    let start_idx = start + 6;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        name = Some(trimmed[start_idx..start_idx + end].to_string());
                    }
                }
                
                if let Some(start) = trimmed.find("type=\"") {
                    let start_idx = start + 6;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        recipient_type = Some(trimmed[start_idx..start_idx + end].to_string());
                    }
                }
                
                if let Some(start) = trimmed.find("address=\"") {
                    let start_idx = start + 9;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        address = Some(trimmed[start_idx..start_idx + end].to_string());
                    }
                }
                
                if let Some(start) = trimmed.find("split=\"") {
                    let start_idx = start + 7;
                    if let Some(end) = trimmed[start_idx..].find("\"") {
                        split = Some(trimmed[start_idx..start_idx + end].to_string());
                    }
                }
                
                recipients.push(serde_json::json!({
                    "name": name,
                    "type": recipient_type,
                    "address": address,
                    "split": split
                }));
            }
        }
        
        Ok(serde_json::Value::Array(value_blocks))
    }

    // Legacy web key check - no longer used but kept for compatibility
    // All authentication now uses User ID 1 (background_tasks) for system operations
    pub async fn is_web_key(&self, _api_key: &str) -> AppResult<bool> {
        // Always return false - web keys are deprecated in favor of background_tasks user (ID 1)
        Ok(false)
    }

    // Set podcast playback speed - matches Python set_podcast_playback_speed function
    pub async fn set_podcast_playback_speed(&self, user_id: i32, podcast_id: i32, playback_speed: f64) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Podcasts" SET playbackspeed = $1, playbackspeedcustomized = TRUE WHERE podcastid = $2 AND userid = $3"#)
                    .bind(playback_speed)
                    .bind(podcast_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Podcasts SET PlaybackSpeed = ?, PlaybackSpeedCustomized = TRUE WHERE PodcastID = ? AND UserID = ?")
                    .bind(playback_speed)
                    .bind(podcast_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Enable/disable auto download for podcast - matches Python enable_auto_download function
    pub async fn enable_auto_download(&self, podcast_id: i32, auto_download: bool, user_id: i32) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Podcasts" SET autodownload = $1 WHERE podcastid = $2 AND userid = $3"#)
                    .bind(auto_download)
                    .bind(podcast_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Podcasts SET AutoDownload = ? WHERE PodcastID = ? AND UserID = ?")
                    .bind(auto_download)
                    .bind(podcast_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Toggle podcast notifications - matches Python toggle_podcast_notifications function
    pub async fn toggle_podcast_notifications(&self, user_id: i32, podcast_id: i32, enabled: bool) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                // First verify user owns the podcast
                let row = sqlx::query(r#"SELECT 1 FROM "Podcasts" WHERE podcastid = $1 AND userid = $2"#)
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if row.is_none() {
                    return Ok(false);
                }
                
                // Update notifications setting
                let result = sqlx::query(r#"UPDATE "Podcasts" SET notificationsenabled = $1 WHERE podcastid = $2 AND userid = $3"#)
                    .bind(enabled)
                    .bind(podcast_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
            DatabasePool::MySQL(pool) => {
                // First verify user owns the podcast
                let row = sqlx::query("SELECT 1 FROM Podcasts WHERE PodcastID = ? AND UserID = ?")
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if row.is_none() {
                    return Ok(false);
                }
                
                // Update notifications setting
                let result = sqlx::query("UPDATE Podcasts SET NotificationsEnabled = ? WHERE PodcastID = ? AND UserID = ?")
                    .bind(enabled)
                    .bind(podcast_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
        }
    }

    // Adjust skip times for podcast - matches Python adjust_skip_times function
    pub async fn adjust_skip_times(&self, podcast_id: i32, start_skip: i32, end_skip: i32, user_id: i32) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Podcasts" SET startskip = $1, endskip = $2 WHERE podcastid = $3 AND userid = $4"#)
                    .bind(start_skip)
                    .bind(end_skip)
                    .bind(podcast_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Podcasts SET StartSkip = ?, EndSkip = ? WHERE PodcastID = ? AND UserID = ?")
                    .bind(start_skip)
                    .bind(end_skip)
                    .bind(podcast_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Remove category from podcast - matches Python remove_category function
    pub async fn remove_category(&self, podcast_id: i32, user_id: i32, category: &str) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Fetch current categories
                let row = sqlx::query(r#"SELECT categories FROM "Podcasts" WHERE podcastid = $1 AND userid = $2"#)
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let current_categories_str: String = row.try_get("categories").unwrap_or_default();
                    let mut categories = self.parse_categories_json(&current_categories_str).unwrap_or_default();
                    
                    // Remove the category by value (find and remove the key with this value)
                    categories.retain(|_key, value| value != category);
                    
                    // Convert back to JSON string
                    let new_categories_json = serde_json::to_string(&categories)
                        .unwrap_or_else(|_| "{}".to_string());
                    
                    // Update with new categories
                    sqlx::query(r#"UPDATE "Podcasts" SET categories = $1 WHERE podcastid = $2 AND userid = $3"#)
                        .bind(&new_categories_json)
                        .bind(podcast_id)
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                }
            }
            DatabasePool::MySQL(pool) => {
                // Fetch current categories
                let row = sqlx::query("SELECT Categories FROM Podcasts WHERE PodcastID = ? AND UserID = ?")
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let current_categories_str: String = row.try_get("Categories").unwrap_or_default();
                    let mut categories = self.parse_categories_json(&current_categories_str).unwrap_or_default();
                    
                    // Remove the category by value (find and remove the key with this value)
                    categories.retain(|_key, value| value != category);
                    
                    // Convert back to JSON string
                    let new_categories_json = serde_json::to_string(&categories)
                        .unwrap_or_else(|_| "{}".to_string());
                    
                    // Update with new categories
                    sqlx::query("UPDATE Podcasts SET Categories = ? WHERE PodcastID = ? AND UserID = ?")
                        .bind(&new_categories_json)
                        .bind(podcast_id)
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                }
            }
        }
        Ok(())
    }

    // Add category to podcast - matches Python add_category function  
    pub async fn add_category(&self, podcast_id: i32, user_id: i32, category: &str) -> AppResult<String> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Fetch current categories
                let row = sqlx::query(r#"SELECT categories FROM "Podcasts" WHERE podcastid = $1 AND userid = $2"#)
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let current_categories_str: String = row.try_get("categories").unwrap_or_default();
                    let mut categories = self.parse_categories_json(&current_categories_str).unwrap_or_default();
                    
                    // Check if category already exists
                    if categories.values().any(|v| v == category) {
                        return Ok("Category already exists.".to_string());
                    }
                    
                    // Add new category with a generated key (use the next available number)
                    let next_key = if categories.is_empty() {
                        "1".to_string()
                    } else {
                        let max_key = categories.keys()
                            .filter_map(|k| k.parse::<i32>().ok())
                            .max()
                            .unwrap_or(0);
                        (max_key + 1).to_string()
                    };
                    
                    categories.insert(next_key, category.to_string());
                    
                    // Convert back to JSON string
                    let new_categories_json = serde_json::to_string(&categories)
                        .unwrap_or_else(|_| "{}".to_string());
                    
                    // Update with new categories
                    sqlx::query(r#"UPDATE "Podcasts" SET categories = $1 WHERE podcastid = $2 AND userid = $3"#)
                        .bind(&new_categories_json)
                        .bind(podcast_id)
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                    
                    Ok("Category added!".to_string())
                } else {
                    Err(AppError::not_found("Podcast not found"))
                }
            }
            DatabasePool::MySQL(pool) => {
                // Fetch current categories
                let row = sqlx::query("SELECT Categories FROM Podcasts WHERE PodcastID = ? AND UserID = ?")
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let current_categories_str: String = row.try_get("Categories").unwrap_or_default();
                    let mut categories = self.parse_categories_json(&current_categories_str).unwrap_or_default();
                    
                    // Check if category already exists
                    if categories.values().any(|v| v == category) {
                        return Ok("Category already exists.".to_string());
                    }
                    
                    // Add new category with a generated key (use the next available number)
                    let next_key = if categories.is_empty() {
                        "1".to_string()
                    } else {
                        let max_key = categories.keys()
                            .filter_map(|k| k.parse::<i32>().ok())
                            .max()
                            .unwrap_or(0);
                        (max_key + 1).to_string()
                    };
                    
                    categories.insert(next_key, category.to_string());
                    
                    // Convert back to JSON string
                    let new_categories_json = serde_json::to_string(&categories)
                        .unwrap_or_else(|_| "{}".to_string());
                    
                    // Update with new categories
                    sqlx::query("UPDATE Podcasts SET Categories = ? WHERE PodcastID = ? AND UserID = ?")
                        .bind(&new_categories_json)
                        .bind(podcast_id)
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                    
                    Ok("Category added!".to_string())
                } else {
                    Err(AppError::not_found("Podcast not found"))
                }
            }
        }
    }

    // Call get auto download status - matches Python call_get_auto_download_status function exactly
    pub async fn call_get_auto_download_status(&self, podcast_id: i32, user_id: i32) -> AppResult<Option<bool>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT autodownload FROM "Podcasts" WHERE podcastid = $1 AND userid = $2"#)
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                    
                if let Some(row) = row {
                    let result: Option<bool> = row.try_get("autodownload")?;
                    Ok(result)
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT AutoDownload FROM Podcasts WHERE PodcastID = ? AND UserID = ?")
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                    
                if let Some(row) = row {
                    let result: Option<bool> = row.try_get("AutoDownload")?;
                    Ok(result)
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Get feed cutoff days - matches Python get_feed_cutoff_days function exactly  
    pub async fn get_feed_cutoff_days(&self, podcast_id: i32, user_id: i32) -> AppResult<Option<i32>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT feedcutoffdays FROM "Podcasts" WHERE podcastid = $1 AND userid = $2"#)
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                    
                if let Some(row) = row {
                    let result: Option<i32> = row.try_get("feedcutoffdays")?;
                    Ok(result.or(Some(365))) // Default to 365 if null
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT FeedCutoffDays FROM Podcasts WHERE PodcastID = ? AND UserID = ?")
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                    
                if let Some(row) = row {
                    let result: Option<i32> = row.try_get("FeedCutoffDays")?;
                    Ok(result.or(Some(365))) // Default to 365 if null
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Get play episode details - matches Python get_play_episode_details function exactly
    pub async fn get_play_episode_details(&self, user_id: i32, podcast_id: i32, _is_youtube: bool) -> AppResult<(f64, i32, i32)> {
        match self {
            DatabasePool::Postgres(pool) => {
                // First get user's default playback speed
                let user_row = sqlx::query(r#"SELECT playbackspeed FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                    
                let user_playback_speed = if let Some(row) = user_row {
                    if let Ok(speed) = row.try_get::<Option<bigdecimal::BigDecimal>, _>("playbackspeed") {
                        speed.map(|s| s.to_f64().unwrap_or(1.0)).unwrap_or(1.0)
                    } else {
                        1.0
                    }
                } else {
                    1.0
                };

                // Now get podcast-specific settings
                let podcast_row = sqlx::query(r#"
                    SELECT playbackspeed, playbackspeedcustomized, startskip, endskip
                    FROM "Podcasts"
                    WHERE podcastid = $1 AND userid = $2
                "#)
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                    
                if let Some(row) = podcast_row {
                    let playback_speed_customized: Option<bool> = row.try_get("playbackspeedcustomized")?;
                    let podcast_playback_speed: Option<f64> = if let Ok(speed) = row.try_get::<Option<bigdecimal::BigDecimal>, _>("playbackspeed") {
                        speed.map(|s| s.to_f64().unwrap_or(1.0))
                    } else {
                        None
                    };
                    let start_skip: Option<i32> = row.try_get("startskip")?;
                    let end_skip: Option<i32> = row.try_get("endskip")?;
                    
                    let final_playback_speed = if playback_speed_customized.unwrap_or(false) {
                        podcast_playback_speed.unwrap_or(user_playback_speed)
                    } else {
                        user_playback_speed
                    };
                    
                    Ok((final_playback_speed, start_skip.unwrap_or(0), end_skip.unwrap_or(0)))
                } else {
                    Ok((user_playback_speed, 0, 0))
                }
            }
            DatabasePool::MySQL(pool) => {
                // First get user's default playback speed
                let user_row = sqlx::query("SELECT PlaybackSpeed FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                    
                let user_playback_speed = if let Some(row) = user_row {
                    if let Ok(speed) = row.try_get::<Option<bigdecimal::BigDecimal>, _>("PlaybackSpeed") {
                        speed.map(|s| s.to_f64().unwrap_or(1.0)).unwrap_or(1.0)
                    } else {
                        1.0
                    }
                } else {
                    1.0
                };

                // Now get podcast-specific settings
                let podcast_row = sqlx::query("
                    SELECT PlaybackSpeed, PlaybackSpeedCustomized, StartSkip, EndSkip
                    FROM Podcasts
                    WHERE PodcastID = ? AND UserID = ?
                ")
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                    
                if let Some(row) = podcast_row {
                    let playback_speed_customized: Option<bool> = row.try_get("PlaybackSpeedCustomized")?;
                    let podcast_playback_speed: Option<f64> = if let Ok(speed) = row.try_get::<Option<bigdecimal::BigDecimal>, _>("PlaybackSpeed") {
                        speed.map(|s| s.to_f64().unwrap_or(1.0))
                    } else {
                        None
                    };
                    let start_skip: Option<i32> = row.try_get("StartSkip")?;
                    let end_skip: Option<i32> = row.try_get("EndSkip")?;
                    
                    let final_playback_speed = if playback_speed_customized.unwrap_or(false) {
                        podcast_playback_speed.unwrap_or(user_playback_speed)
                    } else {
                        user_playback_speed
                    };
                    
                    Ok((final_playback_speed, start_skip.unwrap_or(0), end_skip.unwrap_or(0)))
                } else {
                    Ok((user_playback_speed, 0, 0))
                }
            }
        }
    }

    // Get podcast episodes with capitalized field names for frontend compatibility
    pub async fn return_podcast_episodes_capitalized(&self, user_id: i32, podcast_id: i32) -> AppResult<Vec<crate::handlers::podcasts::PodcastEpisode>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT * FROM (
                        SELECT
                            "Podcasts".podcastname as podcastname,
                            "Episodes".episodetitle as "Episodetitle",
                            "Episodes".episodepubdate as "Episodepubdate",
                            "Episodes".episodedescription as "Episodedescription",
                            CASE 
                                WHEN "Podcasts".usepodcastcoverscustomized = TRUE AND "Podcasts".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                WHEN "Users".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                ELSE "Episodes".episodeartwork
                            END as "Episodeartwork",
                            "Episodes".episodeurl as "Episodeurl",
                            "Episodes".episodeduration as "Episodeduration",
                            "UserEpisodeHistory".listenduration as "Listenduration",
                            "Episodes".episodeid as "Episodeid",
                            "Episodes".completed as "Completed",
                            CASE WHEN "SavedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN "EpisodeQueue".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN "DownloadedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            FALSE as is_youtube
                        FROM "Episodes"
                        INNER JOIN "Podcasts" ON "Episodes".podcastid = "Podcasts".podcastid
                        LEFT JOIN "Users" ON "Podcasts".userid = "Users".userid
                        LEFT JOIN "UserEpisodeHistory" ON
                            "Episodes".episodeid = "UserEpisodeHistory".episodeid
                            AND "UserEpisodeHistory".userid = $1
                        LEFT JOIN "SavedEpisodes" ON
                            "Episodes".episodeid = "SavedEpisodes".episodeid
                            AND "SavedEpisodes".userid = $1
                        LEFT JOIN "EpisodeQueue" ON
                            "Episodes".episodeid = "EpisodeQueue".episodeid
                            AND "EpisodeQueue".userid = $1
                            AND "EpisodeQueue".is_youtube = FALSE
                        LEFT JOIN "DownloadedEpisodes" ON
                            "Episodes".episodeid = "DownloadedEpisodes".episodeid
                            AND "DownloadedEpisodes".userid = $1
                        WHERE "Podcasts".userid = $1 AND (
                            "Podcasts".podcastid = $2 OR
                            "Podcasts".podcastid IN (
                                SELECT jsonb_array_elements_text(COALESCE(mergedpodcastids::jsonb, '[]'::jsonb))::int
                                FROM "Podcasts" p2
                                WHERE p2.podcastid = $2 AND p2.userid = $1
                            )
                        )

                        UNION ALL

                        SELECT
                            "Podcasts".podcastname as podcastname,
                            "YouTubeVideos".videotitle as "Episodetitle",
                            "YouTubeVideos".publishedat as "Episodepubdate",
                            "YouTubeVideos".videodescription as "Episodedescription",
                            CASE 
                                WHEN "Podcasts".usepodcastcoverscustomized = TRUE AND "Podcasts".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                WHEN "Users".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                ELSE "YouTubeVideos".thumbnailurl
                            END as "Episodeartwork",
                            "YouTubeVideos".videourl as "Episodeurl",
                            "YouTubeVideos".duration as "Episodeduration",
                            "YouTubeVideos".listenposition as "Listenduration",
                            "YouTubeVideos".videoid as "Episodeid",
                            "YouTubeVideos".completed as "Completed",
                            CASE WHEN "SavedVideos".videoid IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN "EpisodeQueue".episodeid IS NOT NULL AND "EpisodeQueue".is_youtube = TRUE THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN "DownloadedVideos".videoid IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            TRUE as is_youtube
                        FROM "YouTubeVideos"
                        INNER JOIN "Podcasts" ON "YouTubeVideos".podcastid = "Podcasts".podcastid
                        LEFT JOIN "Users" ON "Podcasts".userid = "Users".userid
                        LEFT JOIN "SavedVideos" ON
                            "YouTubeVideos".videoid = "SavedVideos".videoid
                            AND "SavedVideos".userid = $1
                        LEFT JOIN "EpisodeQueue" ON
                            "YouTubeVideos".videoid = "EpisodeQueue".episodeid
                            AND "EpisodeQueue".userid = $1
                            AND "EpisodeQueue".is_youtube = TRUE
                        LEFT JOIN "DownloadedVideos" ON
                            "YouTubeVideos".videoid = "DownloadedVideos".videoid
                            AND "DownloadedVideos".userid = $1
                        WHERE "Podcasts".userid = $1 AND (
                            "Podcasts".podcastid = $2 OR
                            "Podcasts".podcastid IN (
                                SELECT jsonb_array_elements_text(COALESCE(mergedpodcastids::jsonb, '[]'::jsonb))::int
                                FROM "Podcasts" p2
                                WHERE p2.podcastid = $2 AND p2.userid = $1
                            )
                        )
                    ) combined
                    ORDER BY "Episodepubdate" DESC"#
                )
                .bind(user_id)
                .bind(podcast_id)
                .fetch_all(pool)
                .await?;

                let mut episodes = Vec::new();
                for row in rows {
                    let naive_date = row.try_get::<chrono::NaiveDateTime, _>("Episodepubdate")?;
                    let episodepubdate = naive_date.format("%Y-%m-%dT%H:%M:%S").to_string();
                    
                    episodes.push(crate::handlers::podcasts::PodcastEpisode {
                        podcastname: row.try_get("podcastname")?,
                        episodetitle: row.try_get("Episodetitle")?,
                        episodepubdate,
                        episodedescription: row.try_get("Episodedescription")?,
                        episodeartwork: row.try_get("Episodeartwork")?,
                        episodeurl: row.try_get("Episodeurl")?,
                        episodeduration: row.try_get("Episodeduration")?,
                        listenduration: row.try_get("Listenduration")?,
                        episodeid: row.try_get("Episodeid")?,
                        completed: row.try_get("Completed")?,
                        saved: row.try_get("saved")?,
                        queued: row.try_get("queued")?,
                        downloaded: row.try_get("downloaded")?,
                        is_youtube: row.try_get("is_youtube")?,
                    });
                }
                
                Ok(episodes)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(
                    "SELECT * FROM (
                        SELECT
                            Podcasts.PodcastName as podcastname,
                            Episodes.EpisodeTitle as Episodetitle,
                            Episodes.EpisodePubDate as Episodepubdate,
                            Episodes.EpisodeDescription as Episodedescription,
                            CASE 
                                WHEN Podcasts.UsePodcastCoversCustomized = TRUE AND Podcasts.UsePodcastCovers = TRUE THEN Podcasts.ArtworkURL
                                WHEN Users.UsePodcastCovers = TRUE THEN Podcasts.ArtworkURL
                                ELSE Episodes.EpisodeArtwork
                            END as Episodeartwork,
                            Episodes.EpisodeURL as Episodeurl,
                            Episodes.EpisodeDuration as Episodeduration,
                            UserEpisodeHistory.ListenDuration as Listenduration,
                            Episodes.EpisodeID as Episodeid,
                            Episodes.Completed as Completed,
                            CASE WHEN SavedEpisodes.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN EpisodeQueue.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN DownloadedEpisodes.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            FALSE as is_youtube
                        FROM Episodes
                        INNER JOIN Podcasts ON Episodes.PodcastID = Podcasts.PodcastID
                        LEFT JOIN Users ON Podcasts.UserID = Users.UserID
                        LEFT JOIN UserEpisodeHistory ON
                            Episodes.EpisodeID = UserEpisodeHistory.EpisodeID
                            AND UserEpisodeHistory.UserID = ?
                        LEFT JOIN SavedEpisodes ON
                            Episodes.EpisodeID = SavedEpisodes.EpisodeID
                            AND SavedEpisodes.UserID = ?
                        LEFT JOIN EpisodeQueue ON
                            Episodes.EpisodeID = EpisodeQueue.EpisodeID
                            AND EpisodeQueue.UserID = ?
                            AND EpisodeQueue.is_youtube = FALSE
                        LEFT JOIN DownloadedEpisodes ON
                            Episodes.EpisodeID = DownloadedEpisodes.EpisodeID
                            AND DownloadedEpisodes.UserID = ?
                        WHERE Podcasts.UserID = ? AND (
                            Podcasts.PodcastID = ? OR
                            Podcasts.PodcastID IN (
                                SELECT CAST(JSON_UNQUOTE(JSON_EXTRACT(COALESCE(p2.MergedPodcastIDs, '[]'), CONCAT('$[', numbers.n, ']'))) AS UNSIGNED)
                                FROM (SELECT 0 as n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) numbers
                                INNER JOIN Podcasts p2 ON p2.PodcastID = ? AND p2.UserID = ?
                                WHERE JSON_UNQUOTE(JSON_EXTRACT(COALESCE(p2.MergedPodcastIDs, '[]'), CONCAT('$[', numbers.n, ']'))) IS NOT NULL
                            )
                        )

                        UNION ALL

                        SELECT
                            Podcasts.PodcastName as podcastname,
                            YouTubeVideos.VideoTitle as Episodetitle,
                            YouTubeVideos.PublishedAt as Episodepubdate,
                            YouTubeVideos.VideoDescription as Episodedescription,
                            CASE 
                                WHEN Podcasts.UsePodcastCoversCustomized = TRUE AND Podcasts.UsePodcastCovers = TRUE THEN Podcasts.ArtworkURL
                                WHEN Users.UsePodcastCovers = TRUE THEN Podcasts.ArtworkURL
                                ELSE YouTubeVideos.ThumbnailURL
                            END as Episodeartwork,
                            YouTubeVideos.VideoURL as Episodeurl,
                            YouTubeVideos.Duration as Episodeduration,
                            YouTubeVideos.ListenPosition as Listenduration,
                            YouTubeVideos.VideoID as Episodeid,
                            YouTubeVideos.Completed as Completed,
                            CASE WHEN SavedVideos.VideoID IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN EpisodeQueue.EpisodeID IS NOT NULL AND EpisodeQueue.is_youtube = TRUE THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN DownloadedVideos.VideoID IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            TRUE as is_youtube
                        FROM YouTubeVideos
                        INNER JOIN Podcasts ON YouTubeVideos.PodcastID = Podcasts.PodcastID
                        LEFT JOIN Users ON Podcasts.UserID = Users.UserID
                        LEFT JOIN SavedVideos ON
                            YouTubeVideos.VideoID = SavedVideos.VideoID
                            AND SavedVideos.UserID = ?
                        LEFT JOIN EpisodeQueue ON
                            YouTubeVideos.VideoID = EpisodeQueue.EpisodeID
                            AND EpisodeQueue.UserID = ?
                            AND EpisodeQueue.is_youtube = TRUE
                        LEFT JOIN DownloadedVideos ON
                            YouTubeVideos.VideoID = DownloadedVideos.VideoID
                            AND DownloadedVideos.UserID = ?
                        WHERE Podcasts.UserID = ? AND (
                            Podcasts.PodcastID = ? OR
                            Podcasts.PodcastID IN (
                                SELECT CAST(JSON_UNQUOTE(JSON_EXTRACT(COALESCE(p2.MergedPodcastIDs, '[]'), CONCAT('$[', numbers.n, ']'))) AS UNSIGNED)
                                FROM (SELECT 0 as n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) numbers
                                INNER JOIN Podcasts p2 ON p2.PodcastID = ? AND p2.UserID = ?
                                WHERE JSON_UNQUOTE(JSON_EXTRACT(COALESCE(p2.MergedPodcastIDs, '[]'), CONCAT('$[', numbers.n, ']'))) IS NOT NULL
                            )
                        )
                    ) combined
                    ORDER BY Episodepubdate DESC"
                )
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(podcast_id)
                .bind(podcast_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(podcast_id)
                .bind(podcast_id)
                .bind(user_id)
                .fetch_all(pool)
                .await?;

                let mut episodes = Vec::new();
                for row in rows {
                    let datetime = row.try_get::<sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>, _>("Episodepubdate")?;
                    let episodepubdate = datetime.format("%Y-%m-%dT%H:%M:%S").to_string();
                    
                    episodes.push(crate::handlers::podcasts::PodcastEpisode {
                        podcastname: row.try_get("podcastname")?,
                        episodetitle: row.try_get("Episodetitle")?,
                        episodepubdate,
                        episodedescription: row.try_get("Episodedescription")?,
                        episodeartwork: row.try_get("Episodeartwork")?,
                        episodeurl: row.try_get("Episodeurl")?,
                        episodeduration: row.try_get("Episodeduration")?,
                        listenduration: row.try_get("Listenduration").ok(),
                        episodeid: row.try_get("Episodeid")?,
                        completed: row.try_get::<i8, _>("Completed")? != 0,
                        saved: row.try_get::<i8, _>("saved")? != 0,
                        queued: row.try_get::<i8, _>("queued")? != 0,
                        downloaded: row.try_get::<i8, _>("downloaded")? != 0,
                        is_youtube: row.try_get::<i8, _>("is_youtube")? != 0,
                    });
                }
                
                Ok(episodes)
            }
        }
    }

    // Record listen duration - matches Python record_listen_duration function exactly
    pub async fn record_listen_duration(&self, episode_id: i32, user_id: i32, listen_duration: f64) -> AppResult<()> {
        println!("Recording listen duration: episode_id={}, user_id={}, duration={}", episode_id, user_id, listen_duration);
        
        if listen_duration < 0.0 {
            println!("Skipped updating listen duration for user {} and episode {} due to invalid duration: {}", user_id, episode_id, listen_duration);
            return Ok(());
        }
        
        let listen_duration_int = listen_duration as i32;
        
        match self {
            DatabasePool::Postgres(pool) => {
                // Check if record exists and get existing duration
                let existing_row = sqlx::query(r#"SELECT listenduration FROM "UserEpisodeHistory" WHERE userid = $1 AND episodeid = $2"#)
                    .bind(user_id)
                    .bind(episode_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = existing_row {
                    let existing_duration: Option<i32> = row.try_get("listenduration")?;
                    let existing_duration = existing_duration.unwrap_or(0);
                    
                    // Update only if new duration is greater than existing
                    if listen_duration_int > existing_duration {
                        sqlx::query(r#"UPDATE "UserEpisodeHistory" SET listenduration = $1, listendate = NOW() WHERE userid = $2 AND episodeid = $3"#)
                            .bind(listen_duration_int)
                            .bind(user_id)
                            .bind(episode_id)
                            .execute(pool)
                            .await?;
                        println!("Updated listen duration for user {} episode {} from {} to {}", user_id, episode_id, existing_duration, listen_duration_int);
                    } else {
                        println!("No update required for user {} and episode {} as existing duration {} is greater than or equal to new duration {}", user_id, episode_id, existing_duration, listen_duration_int);
                    }
                } else {
                    // Insert new record
                    sqlx::query(r#"INSERT INTO "UserEpisodeHistory" (userid, episodeid, listendate, listenduration) VALUES ($1, $2, NOW(), $3)"#)
                        .bind(user_id)
                        .bind(episode_id)
                        .bind(listen_duration_int)
                        .execute(pool)
                        .await?;
                    println!("Inserted new listen duration record for user {} episode {} with duration {}", user_id, episode_id, listen_duration_int);
                }
            }
            DatabasePool::MySQL(pool) => {
                // Check if record exists and get existing duration
                let existing_row = sqlx::query("SELECT ListenDuration FROM UserEpisodeHistory WHERE UserID = ? AND EpisodeID = ?")
                    .bind(user_id)
                    .bind(episode_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = existing_row {
                    let existing_duration: Option<i32> = row.try_get("ListenDuration")?;
                    let existing_duration = existing_duration.unwrap_or(0);
                    
                    // Update only if new duration is greater than existing
                    if listen_duration_int > existing_duration {
                        sqlx::query("UPDATE UserEpisodeHistory SET ListenDuration = ?, ListenDate = NOW() WHERE UserID = ? AND EpisodeID = ?")
                            .bind(listen_duration_int)
                            .bind(user_id)
                            .bind(episode_id)
                            .execute(pool)
                            .await?;
                        println!("Updated listen duration for user {} episode {} from {} to {}", user_id, episode_id, existing_duration, listen_duration_int);
                    } else {
                        println!("No update required for user {} and episode {} as existing duration {} is greater than or equal to new duration {}", user_id, episode_id, existing_duration, listen_duration_int);
                    }
                } else {
                    // Insert new record
                    sqlx::query("INSERT INTO UserEpisodeHistory (UserID, EpisodeID, ListenDate, ListenDuration) VALUES (?, ?, NOW(), ?)")
                        .bind(user_id)
                        .bind(episode_id)
                        .bind(listen_duration_int)
                        .execute(pool)
                        .await?;
                    println!("Inserted new listen duration record for user {} episode {} with duration {}", user_id, episode_id, listen_duration_int);
                }
            }
        }
        Ok(())
    }

    // Record YouTube listen duration - matches Python record_youtube_listen_duration function exactly  
    pub async fn record_youtube_listen_duration(&self, video_id: i32, user_id: i32, listen_duration: f64) -> AppResult<()> {
        println!("Recording YouTube listen duration: video_id={}, user_id={}, duration={}", video_id, user_id, listen_duration);
        
        if listen_duration < 0.0 {
            println!("Skipped updating listen duration for user {} and video {} due to invalid duration: {}", user_id, video_id, listen_duration);
            return Ok(());
        }
        
        let listen_duration_int = listen_duration as i32;
        
        match self {
            DatabasePool::Postgres(pool) => {
                // Check if record exists and get existing duration
                let existing_row = sqlx::query(r#"SELECT listenduration FROM "UserVideoHistory" WHERE userid = $1 AND videoid = $2"#)
                    .bind(user_id)
                    .bind(video_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = existing_row {
                    let existing_duration: Option<i32> = row.try_get("listenduration")?;
                    let existing_duration = existing_duration.unwrap_or(0);
                    
                    // Update only if new duration is greater than existing
                    if listen_duration_int > existing_duration {
                        sqlx::query(r#"UPDATE "UserVideoHistory" SET listenduration = $1, listendate = NOW() WHERE userid = $2 AND videoid = $3"#)
                            .bind(listen_duration_int)
                            .bind(user_id)
                            .bind(video_id)
                            .execute(pool)
                            .await?;
                        println!("Updated YouTube listen duration for user {} video {} from {} to {}", user_id, video_id, existing_duration, listen_duration_int);
                    } else {
                        println!("No update required for user {} and video {} as existing duration {} is greater than or equal to new duration {}", user_id, video_id, existing_duration, listen_duration_int);
                    }
                } else {
                    // Insert new record
                    sqlx::query(r#"INSERT INTO "UserVideoHistory" (userid, videoid, listendate, listenduration) VALUES ($1, $2, NOW(), $3)"#)
                        .bind(user_id)
                        .bind(video_id)
                        .bind(listen_duration_int)
                        .execute(pool)
                        .await?;
                    println!("Inserted new YouTube listen duration record for user {} video {} with duration {}", user_id, video_id, listen_duration_int);
                }
            }
            DatabasePool::MySQL(pool) => {
                // Check if record exists and get existing duration
                let existing_row = sqlx::query("SELECT ListenDuration FROM UserVideoHistory WHERE UserID = ? AND VideoID = ?")
                    .bind(user_id)
                    .bind(video_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = existing_row {
                    let existing_duration: Option<i32> = row.try_get("ListenDuration")?;
                    let existing_duration = existing_duration.unwrap_or(0);
                    
                    // Update only if new duration is greater than existing
                    if listen_duration_int > existing_duration {
                        sqlx::query("UPDATE UserVideoHistory SET ListenDuration = ?, ListenDate = NOW() WHERE UserID = ? AND VideoID = ?")
                            .bind(listen_duration_int)
                            .bind(user_id)
                            .bind(video_id)
                            .execute(pool)
                            .await?;
                        println!("Updated YouTube listen duration for user {} video {} from {} to {}", user_id, video_id, existing_duration, listen_duration_int);
                    } else {
                        println!("No update required for user {} and video {} as existing duration {} is greater than or equal to new duration {}", user_id, video_id, existing_duration, listen_duration_int);
                    }
                } else {
                    // Insert new record
                    sqlx::query("INSERT INTO UserVideoHistory (UserID, VideoID, ListenDate, ListenDuration) VALUES (?, ?, NOW(), ?)")
                        .bind(user_id)
                        .bind(video_id)
                        .bind(listen_duration_int)
                        .execute(pool)
                        .await?;
                    println!("Inserted new YouTube listen duration record for user {} video {} with duration {}", user_id, video_id, listen_duration_int);
                }
            }
        }
        Ok(())
    }


    // Helper function to check if a URL is likely an audio file
    fn is_audio_url(&self, url: &str) -> bool {
        let url_lower = url.to_lowercase();
        url_lower.contains(".mp3") || 
        url_lower.contains(".m4a") || 
        url_lower.contains(".wav") || 
        url_lower.contains(".ogg") || 
        url_lower.contains(".aac") || 
        url_lower.contains(".flac") || 
        url_lower.contains(".opus") ||
        url_lower.contains("audio") ||
        url_lower.contains("podcast") ||
        url_lower.contains("media") ||
        // Common podcast hosting patterns
        url_lower.contains("feeds.feedburner.com") ||
        url_lower.contains("anchor.fm") ||
        url_lower.contains("buzzsprout.com") ||
        url_lower.contains("libsyn.com") ||
        url_lower.contains("soundcloud.com") ||
        url_lower.contains("podomatic.com") ||
        url_lower.contains("blubrry.com") ||
        url_lower.contains("simplecast.com") ||
        url_lower.contains("podbean.com")
    }

    // Extract audio URL from description/content HTML - matches Python logic
    // fn extract_audio_url_from_description(&self, data: &std::collections::HashMap<String, String>) -> Option<String> {
    //     // Check various description fields for audio URLs
    //     for field in ["content:encoded", "description", "summary", "itunes:summary"] {
    //         if let Some(content) = data.get(field) {
    //             if let Some(url) = self.find_audio_url_in_text(content) {
    //                 return Some(url.to_string());
    //             }
    //         }
    //     }
    //     None
    // }

    fn find_audio_url_in_text<'a>(&self, text: &'a str) -> Option<&'a str> {
        // Look for href= or src= attributes
        for pattern in ["href=\"", "src=\"", "url=\""] {
            // Use lowercase only for finding pattern positions
            if let Some(start) = text.to_lowercase().find(pattern) {
                // Match same index in original text
                let url_start = start + pattern.len();
                if let Some(end) = text[url_start..].find('\"') {
                    let url = &text[url_start..url_start + end];
                    if self.is_audio_url(url) {
                        return Some(url);
                    }
                }
            }
        }

        // Look for standalone URLs
        for word in text.split_whitespace() {
            if word.starts_with("http") && self.is_audio_url(word) {
                return Some(word);
            }
        }

        None
    }


    // Set user theme - matches Python set_theme function exactly
    pub async fn set_theme(&self, user_id: i32, theme: &str) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "UserSettings" SET theme = $1 WHERE userid = $2"#)
                    .bind(theme)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE UserSettings SET Theme = ? WHERE UserID = ?")
                    .bind(theme)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Get all users info - matches Python get_user_info function exactly
    pub async fn get_user_info(&self) -> AppResult<Vec<crate::handlers::settings::UserInfo>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT userid, fullname, username, email, CASE WHEN isadmin THEN true ELSE false END AS isadmin FROM "Users""#
                )
                .fetch_all(pool)
                .await?;

                let mut users = Vec::new();
                for row in rows {
                    users.push(crate::handlers::settings::UserInfo {
                        userid: row.try_get("userid")?,
                        fullname: row.try_get("fullname")?,
                        username: row.try_get("username")?,
                        email: row.try_get("email")?,
                        isadmin: row.try_get("isadmin")?,
                    });
                }
                Ok(users)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(
                    "SELECT UserID as userid, Fullname as fullname, Username as username, Email as email, IsAdmin as isadmin FROM Users"
                )
                .fetch_all(pool)
                .await?;

                let mut users = Vec::new();
                for row in rows {
                    users.push(crate::handlers::settings::UserInfo {
                        userid: row.try_get("userid")?,
                        fullname: row.try_get("fullname")?,
                        username: row.try_get("username")?,
                        email: row.try_get("email")?,
                        isadmin: row.try_get("isadmin")?,
                    });
                }
                Ok(users)
            }
        }
    }

    // Get specific user info - matches Python get_my_user_info function exactly
    pub async fn get_my_user_info(&self, user_id: i32) -> AppResult<Option<serde_json::Value>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(
                    r#"SELECT userid, fullname, username, email, CASE WHEN isadmin THEN 1 ELSE 0 END AS isadmin, timezone, timeformat, dateformat, language FROM "Users" WHERE userid = $1"#
                )
                .bind(user_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    Ok(Some(serde_json::json!({
                        "userid": row.try_get::<i32, _>("userid")?,
                        "fullname": row.try_get::<String, _>("fullname")?,
                        "username": row.try_get::<String, _>("username")?,
                        "email": row.try_get::<String, _>("email")?,
                        "isadmin": row.try_get::<i32, _>("isadmin")?,
                        "timezone": row.try_get::<String, _>("timezone")?,
                        "timeformat": row.try_get::<i32, _>("timeformat")?,
                        "dateformat": row.try_get::<String, _>("dateformat")?,
                        "language": row.try_get::<String, _>("language")?,
                    })))
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query(
                    "SELECT UserID as userid, Fullname as fullname, Username as username, Email as email, IsAdmin as isadmin, TimeZone as timezone, TimeFormat as timeformat, DateFormat as dateformat, Language as language FROM Users WHERE UserID = ?"
                )
                .bind(user_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    Ok(Some(serde_json::json!({
                        "userid": row.try_get::<i32, _>("userid")?,
                        "fullname": row.try_get::<String, _>("fullname")?,
                        "username": row.try_get::<String, _>("username")?,
                        "email": row.try_get::<String, _>("email")?,
                        "isadmin": row.try_get::<i32, _>("isadmin")?,
                        "timezone": row.try_get::<String, _>("timezone")?,
                        "timeformat": row.try_get::<i32, _>("timeformat")?,
                        "dateformat": row.try_get::<String, _>("dateformat")?,
                        "language": row.try_get::<String, _>("language")?,
                    })))
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Add user - matches Python add_user function exactly
    pub async fn add_user(&self, fullname: &str, username: &str, email: &str, hashed_pw: &str) -> AppResult<i32> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(
                    r#"INSERT INTO "Users" (fullname, username, email, hashed_pw, isadmin) VALUES ($1, $2, $3, $4, false) RETURNING userid"#
                )
                .bind(fullname)
                .bind(username)
                .bind(email)
                .bind(hashed_pw)
                .fetch_one(pool)
                .await?;

                let user_id: i32 = row.try_get("userid")?;

                // Add user settings like Python version
                sqlx::query(r#"INSERT INTO "UserSettings" (userid, theme) VALUES ($1, $2)"#)
                    .bind(user_id)
                    .bind("light")
                    .execute(pool)
                    .await?;

                // Create default playlists for the new user
                if let Err(e) = self.create_default_playlists_for_user(user_id).await {
                    tracing::warn!("⚠️ Failed to create default playlists for new user {}: {}", user_id, e);
                    // Don't fail user creation if playlist creation fails
                }

                Ok(user_id)
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query(
                    "INSERT INTO Users (Fullname, Username, Email, Hashed_PW, IsAdmin) VALUES (?, ?, ?, ?, 0)"
                )
                .bind(fullname)
                .bind(username)
                .bind(email)
                .bind(hashed_pw)
                .execute(pool)
                .await?;

                let user_id = result.last_insert_id() as i32;

                // Add user settings like Python version
                sqlx::query("INSERT INTO UserSettings (UserID, Theme) VALUES (?, ?)")
                    .bind(user_id)
                    .bind("light")
                    .execute(pool)
                    .await?;

                // Create default playlists for the new user
                if let Err(e) = self.create_default_playlists_for_user(user_id).await {
                    tracing::warn!("⚠️ Failed to create default playlists for new user {}: {}", user_id, e);
                    // Don't fail user creation if playlist creation fails
                }

                Ok(user_id)
            }
        }
    }

    // Set fullname - matches Python set_fullname function exactly
    pub async fn set_fullname(&self, user_id: i32, new_name: &str) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Users" SET fullname = $1 WHERE userid = $2"#)
                    .bind(new_name)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Users SET Fullname = ? WHERE UserID = ?")
                    .bind(new_name)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Set password - matches Python set_password function exactly
    pub async fn set_password(&self, user_id: i32, hash_pw: &str) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Users" SET hashed_pw = $1 WHERE userid = $2"#)
                    .bind(hash_pw)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Users SET Hashed_PW = ? WHERE UserID = ?")
                    .bind(hash_pw)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Delete user - matches Python delete_user function exactly
    pub async fn delete_user(&self, user_id: i32) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Delete from UserEpisodeHistory
                sqlx::query(r#"DELETE FROM "UserEpisodeHistory" WHERE userid = $1"#)
                    .bind(user_id)
                    .execute(pool)
                    .await.ok();

                // Delete from DownloadedEpisodes  
                sqlx::query(r#"DELETE FROM "DownloadedEpisodes" WHERE userid = $1"#)
                    .bind(user_id)
                    .execute(pool)
                    .await.ok();

                // Delete from EpisodeQueue
                sqlx::query(r#"DELETE FROM "EpisodeQueue" WHERE userid = $1"#)
                    .bind(user_id)
                    .execute(pool)
                    .await.ok();

                // Delete from Podcasts
                sqlx::query(r#"DELETE FROM "Podcasts" WHERE userid = $1"#)
                    .bind(user_id)
                    .execute(pool)
                    .await.ok();

                // Delete from UserSettings
                sqlx::query(r#"DELETE FROM "UserSettings" WHERE userid = $1"#)
                    .bind(user_id)
                    .execute(pool)
                    .await.ok();

                // Delete from UserStats
                sqlx::query(r#"DELETE FROM "UserStats" WHERE userid = $1"#)
                    .bind(user_id)
                    .execute(pool)
                    .await.ok();

                // CRITICAL: Delete user's playlists to avoid foreign key constraint violations
                sqlx::query(r#"DELETE FROM "Playlists" WHERE userid = $1"#)
                    .bind(user_id)
                    .execute(pool)
                    .await.ok();

                // Delete from Users (main table)
                sqlx::query(r#"DELETE FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                // Delete from UserEpisodeHistory
                sqlx::query("DELETE FROM UserEpisodeHistory WHERE UserID = ?")
                    .bind(user_id)
                    .execute(pool)
                    .await.ok();

                // Delete from DownloadedEpisodes
                sqlx::query("DELETE FROM DownloadedEpisodes WHERE UserID = ?")
                    .bind(user_id)
                    .execute(pool)
                    .await.ok();

                // Delete from EpisodeQueue
                sqlx::query("DELETE FROM EpisodeQueue WHERE UserID = ?")
                    .bind(user_id)
                    .execute(pool)
                    .await.ok();

                // Delete from Podcasts
                sqlx::query("DELETE FROM Podcasts WHERE UserID = ?")
                    .bind(user_id)
                    .execute(pool)
                    .await.ok();

                // Delete from UserSettings
                sqlx::query("DELETE FROM UserSettings WHERE UserID = ?")
                    .bind(user_id)
                    .execute(pool)
                    .await.ok();

                // Delete from UserStats
                sqlx::query("DELETE FROM UserStats WHERE UserID = ?")
                    .bind(user_id)
                    .execute(pool)
                    .await.ok();

                // CRITICAL: Delete user's playlists to avoid foreign key constraint violations
                sqlx::query("DELETE FROM Playlists WHERE UserID = ?")
                    .bind(user_id)
                    .execute(pool)
                    .await.ok();

                // Delete from Users (main table)
                sqlx::query("DELETE FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Set email - matches Python set_email function exactly
    pub async fn set_email(&self, user_id: i32, new_email: &str) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Users" SET email = $1 WHERE userid = $2"#)
                    .bind(new_email)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Users SET Email = ? WHERE UserID = ?")
                    .bind(new_email)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Set username - matches Python set_username function exactly
    pub async fn set_username(&self, user_id: i32, new_username: &str) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Users" SET username = $1 WHERE userid = $2"#)
                    .bind(new_username)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Users SET Username = ? WHERE UserID = ?")
                    .bind(new_username)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Set isadmin - matches Python set_isadmin function exactly
    pub async fn set_isadmin(&self, user_id: i32, isadmin: bool) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Users" SET isadmin = $1 WHERE userid = $2"#)
                    .bind(isadmin)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Users SET IsAdmin = ? WHERE UserID = ?")
                    .bind(isadmin as i32)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Final admin check - matches Python final_admin function exactly
    pub async fn final_admin(&self, user_id: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Count total admins
                let admin_count: i64 = sqlx::query_scalar(r#"SELECT COUNT(*) FROM "Users" WHERE isadmin = TRUE"#)
                    .fetch_one(pool)
                    .await?;

                if admin_count == 1 {
                    // Check if this user is admin
                    let is_admin: Option<bool> = sqlx::query_scalar(r#"SELECT isadmin FROM "Users" WHERE userid = $1"#)
                        .bind(user_id)
                        .fetch_optional(pool)
                        .await?;

                    Ok(is_admin.unwrap_or(false))
                } else {
                    Ok(false)
                }
            }
            DatabasePool::MySQL(pool) => {
                // Count total admins
                let admin_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM Users WHERE IsAdmin = 1")
                    .fetch_one(pool)
                    .await?;

                if admin_count == 1 {
                    // Check if this user is admin
                    let is_admin: Option<i32> = sqlx::query_scalar("SELECT IsAdmin FROM Users WHERE UserID = ?")
                        .bind(user_id)
                        .fetch_optional(pool)
                        .await?;

                    Ok(is_admin.unwrap_or(0) == 1)
                } else {
                    Ok(false)
                }
            }
        }
    }

    // Enable/disable guest - matches Python enable_disable_guest function exactly
    pub async fn enable_disable_guest(&self) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Users" SET email = CASE WHEN email = 'inactive' THEN 'active' ELSE 'inactive' END WHERE username = 'guest'"#)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Users SET Email = CASE WHEN Email = 'inactive' THEN 'active' ELSE 'inactive' END WHERE Username = 'guest'")
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Enable/disable downloads - matches Python enable_disable_downloads function exactly
    pub async fn enable_disable_downloads(&self) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "AppSettings" SET downloadenabled = CASE WHEN downloadenabled = true THEN false ELSE true END"#)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE AppSettings SET DownloadEnabled = CASE WHEN DownloadEnabled = 1 THEN 0 ELSE 1 END")
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Enable/disable self service - matches Python enable_disable_self_service function exactly
    pub async fn enable_disable_self_service(&self) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "AppSettings" SET selfserviceuser = CASE WHEN selfserviceuser = true THEN false ELSE true END"#)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE AppSettings SET SelfServiceUser = CASE WHEN SelfServiceUser = 1 THEN 0 ELSE 1 END")
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Get guest status - matches Python guest_status function exactly
    pub async fn guest_status(&self) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result: Option<String> = sqlx::query_scalar(r#"SELECT email FROM "Users" WHERE email = 'active'"#)
                    .fetch_optional(pool)
                    .await?;
                Ok(result.is_some())
            }
            DatabasePool::MySQL(pool) => {
                let result: Option<String> = sqlx::query_scalar("SELECT Email FROM Users WHERE Email = 'active'")
                    .fetch_optional(pool)
                    .await?;
                Ok(result.is_some())
            }
        }
    }

    // Toggle RSS feeds - matches Python toggle_rss_feeds function exactly
    pub async fn toggle_rss_feeds(&self, user_id: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Get current status
                let current_status: Option<bool> = sqlx::query_scalar(r#"SELECT enablerssfeeds FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                let new_status = !current_status.unwrap_or(false);

                // Update status
                sqlx::query(r#"UPDATE "Users" SET enablerssfeeds = $1 WHERE userid = $2"#)
                    .bind(new_status)
                    .bind(user_id)
                    .execute(pool)
                    .await?;

                Ok(new_status)
            }
            DatabasePool::MySQL(pool) => {
                // Get current status
                let current_status: Option<i32> = sqlx::query_scalar("SELECT EnableRSSFeeds FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                let new_status = current_status.unwrap_or(0) == 0;

                // Update status
                sqlx::query("UPDATE Users SET EnableRSSFeeds = ? WHERE UserID = ?")
                    .bind(new_status as i32)
                    .bind(user_id)
                    .execute(pool)
                    .await?;

                Ok(new_status)
            }
        }
    }

    // Get download status - matches Python download_status function exactly
    pub async fn download_status(&self) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result: Option<bool> = sqlx::query_scalar(r#"SELECT downloadenabled FROM "AppSettings""#)
                    .fetch_optional(pool)
                    .await?;
                Ok(result.unwrap_or(false))
            }
            DatabasePool::MySQL(pool) => {
                let result: Option<i32> = sqlx::query_scalar("SELECT DownloadEnabled FROM AppSettings")
                    .fetch_optional(pool)
                    .await?;
                Ok(result.unwrap_or(0) == 1)
            }
        }
    }

    // Get self service status - matches Python self_service_status function exactly
    pub async fn self_service_status(&self) -> AppResult<SelfServiceStatus> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Get self-service status
                let self_service_result: Option<bool> = sqlx::query_scalar(r#"SELECT selfserviceuser FROM "AppSettings" WHERE selfserviceuser = TRUE"#)
                    .fetch_optional(pool)
                    .await?;

                // Get admin count
                let admin_count: i64 = sqlx::query_scalar(r#"SELECT COUNT(*) FROM "Users" WHERE isadmin = TRUE"#)
                    .fetch_one(pool)
                    .await?;

                Ok(SelfServiceStatus {
                    status: self_service_result.unwrap_or(false),
                    admin_exists: admin_count > 0,
                })
            }
            DatabasePool::MySQL(pool) => {
                // Get self-service status
                let self_service_result: Option<i32> = sqlx::query_scalar("SELECT SelfServiceUser FROM AppSettings WHERE SelfServiceUser = 1")
                    .fetch_optional(pool)
                    .await?;

                // Get admin count
                let admin_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM Users WHERE IsAdmin = 1")
                    .fetch_one(pool)
                    .await?;

                Ok(SelfServiceStatus {
                    status: self_service_result.unwrap_or(0) == 1,
                    admin_exists: admin_count > 0,
                })
            }
        }
    }

    // Save email settings - matches Python save_email_settings function exactly
    pub async fn save_email_settings(&self, email_settings: &crate::handlers::settings::EmailSettings) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                let auth_required = email_settings.auth_required != 0;
                sqlx::query(
                    r#"UPDATE "EmailSettings" SET 
                       server_name = $1, server_port = $2, from_email = $3, 
                       send_mode = $4, encryption = $5, auth_required = $6, 
                       username = $7, password = $8 
                       WHERE emailsettingsid = 1"#
                )
                .bind(&email_settings.server_name)
                .bind(email_settings.server_port)
                .bind(&email_settings.from_email)
                .bind(&email_settings.send_mode)
                .bind(&email_settings.encryption)
                .bind(auth_required)
                .bind(&email_settings.email_username)
                .bind(&email_settings.email_password)
                .execute(pool)
                .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query(
                    "UPDATE EmailSettings SET 
                     Server_Name = ?, Server_Port = ?, From_Email = ?, 
                     Send_Mode = ?, Encryption = ?, Auth_Required = ?, 
                     Username = ?, Password = ? 
                     WHERE EmailSettingsID = 1"
                )
                .bind(&email_settings.server_name)
                .bind(email_settings.server_port)
                .bind(&email_settings.from_email)
                .bind(&email_settings.send_mode)
                .bind(&email_settings.encryption)
                .bind(email_settings.auth_required)
                .bind(&email_settings.email_username)
                .bind(&email_settings.email_password)
                .execute(pool)
                .await?;
            }
        }
        Ok(())
    }

    // Get email settings - matches Python get_email_settings function exactly
    pub async fn get_email_settings(&self) -> AppResult<Option<crate::handlers::settings::EmailSettingsResponse>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(
                    r#"SELECT emailsettingsid, server_name, server_port, from_email, 
                       send_mode, encryption, auth_required, username, password 
                       FROM "EmailSettings""#
                )
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    let auth_required: bool = row.try_get("auth_required")?;
                    Ok(Some(crate::handlers::settings::EmailSettingsResponse {
                        emailsettingsid: row.try_get("emailsettingsid")?,
                        server_name: row.try_get("server_name")?,
                        server_port: row.try_get("server_port")?,
                        from_email: row.try_get("from_email")?,
                        send_mode: row.try_get("send_mode")?,
                        encryption: row.try_get("encryption")?,
                        auth_required: if auth_required { 1 } else { 0 },
                        username: row.try_get("username")?,
                        password: row.try_get("password")?,
                    }))
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query(
                    "SELECT EmailSettingsID, Server_Name, Server_Port, From_Email, 
                     Send_Mode, Encryption, Auth_Required, Username, Password 
                     FROM EmailSettings"
                )
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    Ok(Some(crate::handlers::settings::EmailSettingsResponse {
                        emailsettingsid: row.try_get("EmailSettingsID")?,
                        server_name: row.try_get("Server_Name")?,
                        server_port: row.try_get("Server_Port")?,
                        from_email: row.try_get("From_Email")?,
                        send_mode: row.try_get("Send_Mode")?,
                        encryption: row.try_get("Encryption")?,
                        auth_required: row.try_get("Auth_Required")?,
                        username: row.try_get("Username")?,
                        password: row.try_get("Password")?,
                    }))
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Get API info - matches Python get_api_info function exactly
    pub async fn get_api_info(&self, user_id: i32) -> AppResult<Option<Vec<crate::handlers::settings::ApiInfo>>> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Check if user is admin
                let is_admin: Option<bool> = sqlx::query_scalar(r#"SELECT isadmin FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                let is_admin = is_admin.unwrap_or(false);

                let query = if is_admin {
                    // Admin sees all API keys
                    r#"SELECT a.apikeyid, a.userid, u.username, RIGHT(a.apikey, 4) as lastfourdigits, 
                       a.created::text as created
                       FROM "APIKeys" a
                       JOIN "Users" u ON a.userid = u.userid"#
                } else {
                    // Non-admin sees only their own API keys
                    r#"SELECT a.apikeyid, a.userid, u.username, RIGHT(a.apikey, 4) as lastfourdigits,
                       a.created::text as created
                       FROM "APIKeys" a
                       JOIN "Users" u ON a.userid = u.userid
                       WHERE a.userid = $1"#
                };

                let rows = if is_admin {
                    sqlx::query(query).fetch_all(pool).await?
                } else {
                    sqlx::query(query).bind(user_id).fetch_all(pool).await?
                };

                let mut api_infos = Vec::new();
                for row in rows {
                    api_infos.push(crate::handlers::settings::ApiInfo {
                        apikeyid: row.try_get("apikeyid")?,
                        userid: row.try_get("userid")?,
                        username: row.try_get("username")?,
                        lastfourdigits: row.try_get("lastfourdigits")?,
                        created: row.try_get("created")?,
                        podcastids: vec![], // Empty array as in Python
                    });
                }

                if api_infos.is_empty() {
                    Ok(None)
                } else {
                    Ok(Some(api_infos))
                }
            }
            DatabasePool::MySQL(pool) => {
                // Check if user is admin
                let is_admin: Option<i32> = sqlx::query_scalar("SELECT IsAdmin FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                let is_admin = is_admin.unwrap_or(0) == 1;

                let query = if is_admin {
                    // Admin sees all API keys
                    "SELECT a.APIKeyID as apikeyid, a.UserID as userid, u.Username as username, RIGHT(a.APIKey, 4) as lastfourdigits,
                     a.Created as created
                     FROM APIKeys a
                     JOIN Users u ON a.UserID = u.UserID"
                } else {
                    // Non-admin sees only their own API keys  
                    "SELECT a.APIKeyID as apikeyid, a.UserID as userid, u.Username as username, RIGHT(a.APIKey, 4) as lastfourdigits,
                     a.Created as created
                     FROM APIKeys a
                     JOIN Users u ON a.UserID = u.UserID
                     WHERE a.UserID = ?"
                };

                let rows = if is_admin {
                    sqlx::query(query).fetch_all(pool).await?
                } else {
                    sqlx::query(query).bind(user_id).fetch_all(pool).await?
                };

                let mut api_infos = Vec::new();
                for row in rows {
                    api_infos.push(crate::handlers::settings::ApiInfo {
                        apikeyid: row.try_get("apikeyid")?,
                        userid: row.try_get("userid")?,
                        username: row.try_get("username")?,
                        lastfourdigits: row.try_get("lastfourdigits")?,
                        created: row.try_get::<chrono::DateTime<chrono::Utc>, _>("created")?.to_string(),
                        podcastids: vec![], // Empty array as in Python
                    });
                }

                if api_infos.is_empty() {
                    Ok(None)
                } else {
                    Ok(Some(api_infos))
                }
            }
        }
    }

    // Create API key - matches Python create_api_key function exactly
    pub async fn create_api_key(&self, user_id: i32) -> AppResult<String> {
        use rand::Rng;
        
        // Generate 64-character API key
        let charset: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
                                abcdefghijklmnopqrstuvwxyz\
                                0123456789";
        let api_key: String = {
            let mut rng = rand::rng();
            (0..64)
                .map(|_| {
                    let idx = rng.random_range(0..charset.len());
                    charset[idx] as char
                })
                .collect()
        };

        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"INSERT INTO "APIKeys" (userid, apikey) VALUES ($1, $2)"#)
                    .bind(user_id)
                    .bind(&api_key)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("INSERT INTO APIKeys (UserID, APIKey) VALUES (?, ?)")
                    .bind(user_id)
                    .bind(&api_key)
                    .execute(pool)
                    .await?;
            }
        }

        Ok(api_key)
    }

    // Create RSS key - matches Python create_rss_key function exactly
    pub async fn create_rss_key(&self, user_id: i32, podcast_ids: Option<Vec<i32>>) -> AppResult<String> {
        use rand::Rng;
        use rand::distr::Alphanumeric;
        
        // Generate 64-character RSS key
        let rss_key: String = rand::rng()
            .sample_iter(&Alphanumeric)
            .take(64)
            .map(char::from)
            .collect();

        match self {
            DatabasePool::Postgres(pool) => {
                let key_id: i32 = sqlx::query_scalar(r#"INSERT INTO "RssKeys" (userid, rsskey) VALUES ($1, $2) RETURNING rsskeyid"#)
                    .bind(user_id)
                    .bind(&rss_key)
                    .fetch_one(pool)
                    .await?;

                // Handle podcast IDs if provided
                if let Some(podcast_ids) = podcast_ids {
                    for podcast_id in podcast_ids {
                        sqlx::query(r#"INSERT INTO "RssKeyPodcasts" (rsskeyid, podcastid) VALUES ($1, $2)"#)
                            .bind(key_id)
                            .bind(podcast_id)
                            .execute(pool)
                            .await?;
                    }
                }
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("INSERT INTO RssKeys (UserID, RssKey) VALUES (?, ?)")
                    .bind(user_id)
                    .bind(&rss_key)
                    .execute(pool)
                    .await?;

                let key_id = result.last_insert_id() as i32;

                // Handle podcast IDs if provided
                if let Some(podcast_ids) = podcast_ids {
                    for podcast_id in podcast_ids {
                        sqlx::query("INSERT INTO RssKeyPodcasts (RssKeyID, PodcastID) VALUES (?, ?)")
                            .bind(key_id)
                            .bind(podcast_id)
                            .execute(pool)
                            .await?;
                    }
                }
            }
        }

        Ok(rss_key)
    }

    // Count user API keys excluding a specific one - safety check for final API key
    pub async fn count_user_api_keys_excluding(&self, user_id: i32, exclude_api_id: i32) -> AppResult<i32> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT COUNT(*) as count FROM "APIKeys" WHERE userid = $1 AND apikeyid != $2"#)
                    .bind(user_id)
                    .bind(exclude_api_id)
                    .fetch_one(pool)
                    .await?;
                Ok(row.try_get::<i64, _>("count")? as i32)
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT COUNT(*) as count FROM APIKeys WHERE UserID = ? AND APIKeyID != ?")
                    .bind(user_id)
                    .bind(exclude_api_id)
                    .fetch_one(pool)
                    .await?;
                Ok(row.try_get::<i64, _>("count")? as i32)
            }
        }
    }

    // Delete API key - matches Python delete_api function exactly
    pub async fn delete_api_key(&self, api_id: i32) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"DELETE FROM "APIKeys" WHERE apikeyid = $1"#)
                    .bind(api_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("DELETE FROM APIKeys WHERE APIKeyID = ?")
                    .bind(api_id)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Check if API key is the same as the one being deleted - matches Python is_same_api_key function exactly
    pub async fn is_same_api_key(&self, api_id: i32, api_key: &str) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result: Option<String> = sqlx::query_scalar(r#"SELECT apikey FROM "APIKeys" WHERE apikeyid = $1"#)
                    .bind(api_id)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(result.map(|key| key == api_key).unwrap_or(false))
            }
            DatabasePool::MySQL(pool) => {
                let result: Option<String> = sqlx::query_scalar("SELECT APIKey FROM APIKeys WHERE APIKeyID = ?")
                    .bind(api_id)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(result.map(|key| key == api_key).unwrap_or(false))
            }
        }
    }

    // Check if API key belongs to guest user - matches Python belongs_to_guest_user function exactly
    pub async fn belongs_to_guest_user(&self, api_id: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result: Option<i32> = sqlx::query_scalar(r#"SELECT userid FROM "APIKeys" WHERE apikeyid = $1"#)
                    .bind(api_id)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(result.map(|user_id| user_id == 1).unwrap_or(false))
            }
            DatabasePool::MySQL(pool) => {
                let result: Option<i32> = sqlx::query_scalar("SELECT UserID FROM APIKeys WHERE APIKeyID = ?")
                    .bind(api_id)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(result.map(|user_id| user_id == 1).unwrap_or(false))
            }
        }
    }

    // Get the owner user ID of an API key by API key ID - for authorization checks
    pub async fn get_api_key_owner(&self, api_id: i32) -> AppResult<Option<i32>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result: Option<i32> = sqlx::query_scalar(r#"SELECT userid FROM "APIKeys" WHERE apikeyid = $1"#)
                    .bind(api_id)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(result)
            }
            DatabasePool::MySQL(pool) => {
                let result: Option<i32> = sqlx::query_scalar("SELECT UserID FROM APIKeys WHERE APIKeyID = ?")
                    .bind(api_id)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(result)
            }
        }
    }

    // Backup user data - matches Python backup_user function exactly
    pub async fn backup_user(&self, user_id: i32) -> AppResult<String> {
        let podcasts = match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT podcastname, feedurl FROM "Podcasts" WHERE userid = $1 AND (username IS NULL OR username = '') AND (password IS NULL OR password = '')"#
                )
                .bind(user_id)
                .fetch_all(pool)
                .await?;

                let mut podcasts = Vec::new();
                for row in rows {
                    podcasts.push((
                        row.try_get::<String, _>("podcastname")?,
                        row.try_get::<String, _>("feedurl")?,
                    ));
                }
                podcasts
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(
                    "SELECT PodcastName, FeedURL FROM Podcasts WHERE UserID = ? AND (Username IS NULL OR Username = '') AND (Password IS NULL OR Password = '')"
                )
                .bind(user_id)
                .fetch_all(pool)
                .await?;

                let mut podcasts = Vec::new();
                for row in rows {
                    podcasts.push((
                        row.try_get::<String, _>("PodcastName")?,
                        row.try_get::<String, _>("FeedURL")?,
                    ));
                }
                podcasts
            }
        };

        // Generate OPML content
        let mut opml_content = String::from(
            r#"<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
  <head>
    <title>Podcast Subscriptions</title>
  </head>
  <body>
"#
        );

        for (podcast_name, feed_url) in podcasts {
            // Escape XML characters in podcast name and feed URL
            let escaped_name = podcast_name
                .replace('&', "&amp;")
                .replace('<', "&lt;")
                .replace('>', "&gt;")
                .replace('"', "&quot;")
                .replace('\'', "&#39;");
            
            let escaped_url = feed_url
                .replace('&', "&amp;")
                .replace('<', "&lt;")
                .replace('>', "&gt;")
                .replace('"', "&quot;");

            opml_content.push_str(&format!(
                r#"    <outline text="{}" title="{}" type="rss" xmlUrl="{}" />
"#,
                escaped_name, escaped_name, escaped_url
            ));
        }

        opml_content.push_str("  </body>\n</opml>");
        Ok(opml_content)
    }

    // Add more database operations as needed...
}

#[derive(Debug, Clone)]
pub struct EpisodeData {
    pub title: String,
    pub description: String,
    pub url: String,
    pub artwork_url: String,
    pub pub_date: DateTime<Utc>,
    pub duration: i32,
}

#[derive(Debug, Clone)]
pub struct UserCredentials {
    pub user_id: i32,
    pub username: String,
    pub hashed_password: String,
    pub email: Option<String>,
}

#[derive(Debug, Clone)]
pub struct UserSettings {
    pub user_id: i32,
    pub api_key: String,
    pub theme: String,
    pub auto_download_episodes: bool,
    pub auto_delete_episodes: bool,
}

#[derive(Debug, Clone)]
pub struct SelfServiceStatus {
    pub status: bool,
    pub admin_exists: bool,
}

#[derive(Debug, Clone)]
pub struct PublicOidcProvider {
    pub provider_id: i32,
    pub provider_name: String,
    pub client_id: String,
    pub authorization_url: String,
    pub scope: String,
    pub button_color: String,
    pub button_text: String,
    pub button_text_color: String,
    pub icon_svg: Option<String>,
}

// Nextcloud login data structure
#[derive(Debug, Clone)]
pub struct NextcloudLoginData {
    pub raw_response: serde_json::Value,
}

// Sync result structure
#[derive(Debug, Clone)]
pub struct SyncResult {
    pub synced_podcasts: i32,
    pub synced_episodes: i32,
}

// gPodder status structure - matches Python get_user_gpodder_status response
#[derive(Debug, Clone)]
pub struct GpodderStatus {
    pub sync_type: String,
    pub gpodder_url: Option<String>,
    pub gpodder_login: Option<String>,
}

// User sync settings structure
#[derive(Debug, Clone)]
pub struct UserSyncSettings {
    pub url: String,
    pub username: String,
    pub token: String,
    pub sync_type: String,
}

#[derive(Debug, Clone)]
pub struct UserAutoComplete {
    pub user_id: i32,
    pub auto_complete_seconds: i32,
}

// gPodder device structure
#[derive(Debug, Clone)]
pub struct GpodderDevice {
    pub device_id: i32,
    pub device_name: String,
    pub device_type: String,
    pub device_caption: Option<String>,
    pub is_default: bool,
    pub is_remote: bool,
    pub user_id: i32,
}

// gPodder session structure for authentication
#[derive(Debug, Clone)]
pub struct GpodderSession {
    pub client: reqwest::Client,
    pub session_id: Option<String>,
    pub authenticated: bool,
}

impl DatabasePool {
    // Use actual psql/mysql for reliable restore
    pub async fn restore_server_data(&self, sql_content: &str) -> AppResult<()> {
        use tokio::process::Command;
        use tokio::io::AsyncWriteExt;
        
        // First, clear all existing data from tables
        self.clear_all_data().await?;
        
        match self {
            DatabasePool::Postgres(_) => {
                // Extract connection details from environment
                let host = std::env::var("DB_HOST").unwrap_or_else(|_| "localhost".to_string());
                let port = std::env::var("DB_PORT").unwrap_or_else(|_| "5432".to_string());
                let database = std::env::var("DB_NAME").unwrap_or_else(|_| "pinepods".to_string());
                let username = std::env::var("DB_USER").unwrap_or_else(|_| "postgres".to_string());
                let password = std::env::var("DB_PASSWORD").unwrap_or_else(|_| "password".to_string());
                
                // Use psql to restore data - connect to target database
                let mut cmd = Command::new("psql");
                cmd.arg("--host").arg(&host)
                   .arg("--port").arg(&port)
                   .arg("--username").arg(&username)
                   .arg("--no-password")
                   .arg("--dbname").arg(&database)
                   .arg("--quiet")
                   .stdin(std::process::Stdio::piped())
                   .stdout(std::process::Stdio::piped())
                   .stderr(std::process::Stdio::piped());
                
                // Set password via environment variable
                cmd.env("PGPASSWORD", &password);
                
                let mut child = cmd.spawn()
                    .map_err(|e| AppError::internal(&format!("Failed to start psql: {}", e)))?;
                
                // Write SQL content to stdin
                if let Some(stdin) = child.stdin.as_mut() {
                    stdin.write_all(sql_content.as_bytes()).await
                        .map_err(|e| AppError::internal(&format!("Failed to write SQL to psql: {}", e)))?;
                    stdin.shutdown().await
                        .map_err(|e| AppError::internal(&format!("Failed to close psql stdin: {}", e)))?;
                }
                
                // Wait for completion
                let output = child.wait_with_output().await
                    .map_err(|e| AppError::internal(&format!("Failed to wait for psql: {}", e)))?;
                
                if !output.status.success() {
                    let stderr = String::from_utf8_lossy(&output.stderr);
                    return Err(AppError::internal(&format!("psql failed: {}", stderr)));
                }
            }
            DatabasePool::MySQL(_) => {
                // Extract connection details from environment
                let host = std::env::var("DB_HOST").unwrap_or_else(|_| "localhost".to_string());
                let port = std::env::var("DB_PORT").unwrap_or_else(|_| "3306".to_string());
                let database = std::env::var("DB_NAME").unwrap_or_else(|_| "pinepods".to_string());
                let username = std::env::var("DB_USER").unwrap_or_else(|_| "root".to_string());
                let password = std::env::var("DB_PASSWORD").unwrap_or_else(|_| "password".to_string());
                
                // Use mysql to restore
                let mut cmd = Command::new("mysql");
                cmd.arg("--host").arg(&host)
                   .arg("--port").arg(&port)
                   .arg("--user").arg(&username)
                   .arg(format!("--password={}", password))
                   .arg("--skip-ssl")
                   .arg("--default-auth=mysql_native_password")
                   .arg(&database)
                   .stdin(std::process::Stdio::piped())
                   .stdout(std::process::Stdio::piped())
                   .stderr(std::process::Stdio::piped());
                
                let mut child = cmd.spawn()
                    .map_err(|e| AppError::internal(&format!("Failed to start mysql: {}", e)))?;
                
                // Write SQL content to stdin
                if let Some(stdin) = child.stdin.as_mut() {
                    stdin.write_all(sql_content.as_bytes()).await
                        .map_err(|e| AppError::internal(&format!("Failed to write SQL to mysql: {}", e)))?;
                    stdin.shutdown().await
                        .map_err(|e| AppError::internal(&format!("Failed to close mysql stdin: {}", e)))?;
                }
                
                // Wait for completion
                let output = child.wait_with_output().await
                    .map_err(|e| AppError::internal(&format!("Failed to wait for mysql: {}", e)))?;
                
                if !output.status.success() {
                    let stderr = String::from_utf8_lossy(&output.stderr);
                    return Err(AppError::internal(&format!("mysql failed: {}", stderr)));
                }
            }
        }
        
        Ok(())
    }
    
    // Clear all data from tables while preserving schema
    async fn clear_all_data(&self) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Disable foreign key constraints temporarily
                sqlx::query("SET session_replication_role = replica;").execute(pool).await?;
                
                // Get all table names
                let tables = sqlx::query(r#"
                    SELECT tablename FROM pg_tables 
                    WHERE schemaname = 'public' 
                    AND tablename NOT LIKE '%_seq'
                "#)
                .fetch_all(pool)
                .await?;
                
                // Clear each table
                for table in tables {
                    let table_name: String = table.try_get("tablename")?;
                    let query = format!(r#"TRUNCATE TABLE "{}" RESTART IDENTITY CASCADE"#, table_name);
                    sqlx::query(&query).execute(pool).await?;
                }
                
                // Re-enable foreign key constraints
                sqlx::query("SET session_replication_role = DEFAULT;").execute(pool).await?;
            }
            DatabasePool::MySQL(pool) => {
                // Disable foreign key checks
                sqlx::query("SET FOREIGN_KEY_CHECKS = 0;").execute(pool).await?;
                
                // Get all table names
                let database = std::env::var("DB_NAME").unwrap_or_else(|_| "pinepods".to_string());
                let tables = sqlx::query(&format!(r#"
                    SELECT TABLE_NAME FROM information_schema.TABLES 
                    WHERE TABLE_SCHEMA = '{}' AND TABLE_TYPE = 'BASE TABLE'
                "#, database))
                .fetch_all(pool)
                .await?;
                
                // Clear each table
                for table in tables {
                    // Handle both VARCHAR and VARBINARY cases for TABLE_NAME
                    let table_name: String = if let Ok(name) = table.try_get::<String, _>("TABLE_NAME") {
                        name
                    } else if let Ok(name_bytes) = table.try_get::<Vec<u8>, _>("TABLE_NAME") {
                        String::from_utf8(name_bytes).map_err(|_| AppError::internal("Invalid table name encoding"))?
                    } else {
                        return Err(AppError::internal("Could not retrieve table name"));
                    };
                    // Use DELETE instead of TRUNCATE to avoid foreign key constraint issues
                    let query = format!("DELETE FROM `{}`", table_name);
                    sqlx::query(&query).execute(pool).await?;
                }
                
                // Re-enable foreign key checks
                sqlx::query("SET FOREIGN_KEY_CHECKS = 1;").execute(pool).await?;
            }
        }
        Ok(())
    }

    // Helper function to parse SQL statements more carefully
    fn parse_sql_statements(&self, sql_content: &str) -> AppResult<Vec<String>> {
        let mut statements = Vec::new();
        let mut current_statement = String::new();
        let mut in_string = false;
        let mut string_delimiter = None;
        let mut escape_next = false;
        
        let chars: Vec<char> = sql_content.chars().collect();
        let mut i = 0;
        
        while i < chars.len() {
            let ch = chars[i];
            
            if escape_next {
                current_statement.push(ch);
                escape_next = false;
                i += 1;
                continue;
            }
            
            if ch == '\\' && in_string {
                escape_next = true;
                current_statement.push(ch);
                i += 1;
                continue;
            }
            
            if !in_string {
                // Check for start of string literals
                if ch == '\'' || ch == '"' {
                    in_string = true;
                    string_delimiter = Some(ch);
                    current_statement.push(ch);
                } else if ch == ';' {
                    // End of statement
                    let trimmed = current_statement.trim();
                    if !trimmed.is_empty() && 
                       !trimmed.starts_with("--") && 
                       !trimmed.to_lowercase().starts_with("/*!") {
                        // Allow SET statements for PostgreSQL configuration
                        statements.push(current_statement.clone());
                    }
                    current_statement.clear();
                } else if ch == '-' && i + 1 < chars.len() && chars[i + 1] == '-' {
                    // Skip single-line comments
                    while i < chars.len() && chars[i] != '\n' {
                        i += 1;
                    }
                    continue;
                } else {
                    current_statement.push(ch);
                }
            } else {
                // Inside string literal
                if ch == string_delimiter.unwrap() {
                    in_string = false;
                    string_delimiter = None;
                }
                current_statement.push(ch);
            }
            
            i += 1;
        }
        
        // Add final statement if any
        let trimmed = current_statement.trim();
        if !trimmed.is_empty() && 
           !trimmed.starts_with("--") && 
           !trimmed.to_lowercase().starts_with("/*!") {
            // Allow SET statements for PostgreSQL configuration
            statements.push(current_statement);
        }
        
        Ok(statements)
    }

    // Generate MFA secret with QR code - matches Python generate_mfa_secret function exactly
    pub async fn generate_mfa_secret(&self, user_id: i32) -> AppResult<(String, String)> {
        use totp_rs::{Algorithm, Secret, TOTP};
        use qrcode::QrCode;
        use rand::Rng;
        
        // Generate random base32 secret (matches Python random_base32())
        let secret = {
            let mut rng = rand::rng();
            let secret_bytes: [u8; 20] = rng.random(); // 160 bits = 32 base32 chars
            Secret::Raw(secret_bytes.to_vec()).to_encoded().to_string()
        };
        
        // Get user email for provisioning URI
        let email = match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT email FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                row.try_get::<String, _>("email")?
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT Email FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                row.try_get::<String, _>("Email")?
            }
        };
        
        // Create TOTP instance and provisioning URI
        let totp = TOTP::new(
            Algorithm::SHA1,
            6,
            1,
            30,
            Secret::Encoded(secret.clone()).to_bytes().unwrap(),
            Some("Pinepods".to_string()),
            email.clone(),
        ).map_err(|e| AppError::internal(&format!("TOTP creation failed: {}", e)))?;
        
        let provisioning_uri = totp.get_url();
        
        // Generate QR code as SVG (matches Python qrcode generation)
        let qr_code = QrCode::new(&provisioning_uri)
            .map_err(|e| AppError::internal(&format!("QR code generation failed: {}", e)))?;
        
        let qr_code_svg = qr_code
            .render::<qrcode::render::svg::Color>()
            .min_dimensions(200, 200)
            .dark_color(qrcode::render::svg::Color("#000000"))
            .light_color(qrcode::render::svg::Color("#ffffff"))
            .build();
        
        // Store temporarily in Redis (matches Python temp_mfa_secrets)
        let temp_key = format!("temp_mfa_{}", user_id);
        self.store_temp_mfa_secret(&temp_key, &secret).await?;
        
        Ok((secret, qr_code_svg))
    }
    
    // Store temporary MFA secret in memory (matches Python temp_mfa_secrets)
    async fn store_temp_mfa_secret(&self, key: &str, secret: &str) -> AppResult<()> {
        let mut secrets = TEMP_MFA_SECRETS.lock().map_err(|e| AppError::internal(&format!("Failed to lock temp MFA secrets: {}", e)))?;
        secrets.insert(key.to_string(), secret.to_string());
        Ok(())
    }
    
    // Verify temporary MFA code - matches Python verify_temp_mfa function exactly  
    pub async fn verify_temp_mfa(&self, user_id: i32, mfa_code: &str) -> AppResult<bool> {
        use totp_rs::{Algorithm, Secret, TOTP};
        
        // Get temporary secret (in production this would be from Redis)
        let temp_key = format!("temp_mfa_{}", user_id);
        let secret = match self.get_temp_mfa_secret(&temp_key).await? {
            Some(secret) => secret,
            None => return Ok(false),
        };
        
        // Create TOTP instance and verify code
        let totp = TOTP::new(
            Algorithm::SHA1,
            6,
            1,
            30,
            Secret::Encoded(secret.clone()).to_bytes().unwrap(),
            Some("Pinepods".to_string()),
            "verification".to_string(),
        ).map_err(|e| AppError::internal(&format!("TOTP creation failed: {}", e)))?;
        
        let verified = totp.check_current(mfa_code)
            .map_err(|e| AppError::internal(&format!("TOTP verification failed: {}", e)))?;
        
        if verified {
            // Save to permanent storage and remove from temp
            self.save_mfa_secret(user_id, &secret).await?;
            self.remove_temp_mfa_secret(&temp_key).await?;
        }
        
        Ok(verified)
    }
    
    // Get temporary MFA secret from memory (matches Python temp_mfa_secrets lookup)
    async fn get_temp_mfa_secret(&self, key: &str) -> AppResult<Option<String>> {
        let secrets = TEMP_MFA_SECRETS.lock().map_err(|e| AppError::internal(&format!("Failed to lock temp MFA secrets: {}", e)))?;
        Ok(secrets.get(key).cloned())
    }
    
    // Remove temporary MFA secret from memory (matches Python temp_mfa_secrets cleanup)
    async fn remove_temp_mfa_secret(&self, key: &str) -> AppResult<()> {
        let mut secrets = TEMP_MFA_SECRETS.lock().map_err(|e| AppError::internal(&format!("Failed to lock temp MFA secrets: {}", e)))?;
        secrets.remove(key);
        Ok(())
    }
    
    // Check if MFA is enabled - matches Python check_mfa_enabled function exactly
    pub async fn check_mfa_enabled(&self, user_id: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT mfa_secret FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let secret: Option<String> = row.try_get("mfa_secret")?;
                    Ok(secret.is_some())
                } else {
                    Ok(false)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT MFA_Secret FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let secret: Option<String> = row.try_get("MFA_Secret")?;
                    Ok(secret.is_some())
                } else {
                    Ok(false)
                }
            }
        }
    }
    
    // Save MFA secret - matches Python save_mfa_secret function exactly
    pub async fn save_mfa_secret(&self, user_id: i32, mfa_secret: &str) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"UPDATE "Users" SET mfa_secret = $1 WHERE userid = $2"#)
                    .bind(mfa_secret)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("UPDATE Users SET MFA_Secret = ? WHERE UserID = ?")
                    .bind(mfa_secret)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
        }
    }
    
    // Delete MFA secret - matches Python delete_mfa_secret function exactly
    pub async fn delete_mfa_secret(&self, user_id: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"UPDATE "Users" SET mfa_secret = NULL WHERE userid = $1"#)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("UPDATE Users SET MFA_Secret = NULL WHERE UserID = ?")
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
        }
    }

    // Initiate Nextcloud login - matches Python initiate_nextcloud_login function exactly
    pub async fn initiate_nextcloud_login(&self, _user_id: i32, nextcloud_url: &str) -> AppResult<NextcloudLoginData> {
        let client = reqwest::Client::new();
        
        // Call Nextcloud login flow v2 API
        let login_url = format!("{}/index.php/login/v2", nextcloud_url.trim_end_matches('/'));
        
        let response = client
            .post(&login_url)
            .send()
            .await
            .map_err(|e| AppError::internal(&format!("Failed to initiate Nextcloud login: {}", e)))?;
        
        if !response.status().is_success() {
            return Err(AppError::internal("Nextcloud login initiation failed"));
        }
        
        let json: serde_json::Value = response.json().await
            .map_err(|e| AppError::internal(&format!("Failed to parse Nextcloud response: {}", e)))?;
        
        // Return the raw JSON response from Nextcloud to match Python behavior
        Ok(NextcloudLoginData {
            raw_response: json,
        })
    }
    
    // Add Nextcloud server - matches Python add_nextcloud_server function exactly
    pub async fn add_nextcloud_server(&self, user_id: i32, nextcloud_url: &str, token: &str) -> AppResult<bool> {
        let client = reqwest::Client::new();
        
        // Poll for completion
        let poll_url = format!("{}/index.php/login/v2/poll", nextcloud_url.trim_end_matches('/'));
        let poll_data = serde_json::json!({ "token": token });
        
        let response = client
            .post(&poll_url)
            .json(&poll_data)
            .send()
            .await
            .map_err(|e| AppError::internal(&format!("Failed to poll Nextcloud: {}", e)))?;
        
        if !response.status().is_success() {
            return Ok(false);
        }
        
        let json: serde_json::Value = response.json().await
            .map_err(|e| AppError::internal(&format!("Failed to parse Nextcloud poll response: {}", e)))?;
        
        let app_password = json["appPassword"].as_str()
            .ok_or_else(|| AppError::internal("Missing app password in Nextcloud response"))?;
        let login_name = json["loginName"].as_str()
            .ok_or_else(|| AppError::internal("Missing login name in Nextcloud response"))?;
        
        // Encrypt the app password
        let encrypted_password = self.encrypt_password(app_password).await?;
        
        // Store Nextcloud credentials
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Users" SET gpodderurl = $1, gpodderloginname = $2, gpoddertoken = $3, pod_sync_type = 'nextcloud' WHERE userid = $4"#)
                    .bind(nextcloud_url)
                    .bind(login_name)
                    .bind(&encrypted_password)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Users SET GpodderUrl = ?, GpodderLoginName = ?, GpodderToken = ?, Pod_Sync_Type = 'nextcloud' WHERE UserID = ?")
                    .bind(nextcloud_url)
                    .bind(login_name)
                    .bind(&encrypted_password)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        
        // Perform initial full sync for Nextcloud to get ALL user subscriptions
        if let Err(e) = self.call_nextcloud_initial_full_sync(user_id, nextcloud_url, login_name, app_password).await {
            tracing::warn!("Initial Nextcloud full sync failed during setup: {}", e);
            // Don't fail setup if initial sync fails
        }
        
        Ok(true)
    }

    // Save Nextcloud credentials - helper method for background polling
    pub async fn save_nextcloud_credentials(&self, user_id: i32, nextcloud_url: &str, app_password: &str, login_name: &str) -> AppResult<()> {
        // Encrypt the app password
        let encrypted_password = self.encrypt_password(app_password).await?;
        
        // Store Nextcloud credentials
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Users" SET gpodderurl = $1, gpodderloginname = $2, gpoddertoken = $3, pod_sync_type = 'nextcloud' WHERE userid = $4"#)
                    .bind(nextcloud_url)
                    .bind(login_name)
                    .bind(&encrypted_password)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Users SET GpodderUrl = ?, GpodderLoginName = ?, GpodderToken = ?, Pod_Sync_Type = 'nextcloud' WHERE UserID = ?")
                    .bind(nextcloud_url)
                    .bind(login_name)
                    .bind(&encrypted_password)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        
        Ok(())
    }
    
    
    // Add gPodder server - matches Python add_gpodder_server function exactly
    pub async fn add_gpodder_server(&self, user_id: i32, gpodder_url: &str, username: &str, password: &str) -> AppResult<bool> {
        // Encrypt the password
        let encrypted_password = self.encrypt_password(password).await?;
        
        // Try to get devices from the external server to set a default device
        let default_device_name = match self.get_first_device_from_server(gpodder_url, username, password).await {
            Ok(device_name) => Some(device_name),
            Err(e) => {
                tracing::warn!("Could not get default device from external GPodder server: {}", e);
                None
            }
        };
        
        // Store gPodder credentials
        match self {
            DatabasePool::Postgres(pool) => {
                if let Some(device_name) = &default_device_name {
                    sqlx::query(r#"UPDATE "Users" SET gpodderurl = $1, gpodderloginname = $2, gpoddertoken = $3, pod_sync_type = 'external', defaultgpodderdevice = $5 WHERE userid = $4"#)
                        .bind(gpodder_url)
                        .bind(username)
                        .bind(&encrypted_password)
                        .bind(user_id)
                        .bind(device_name)
                        .execute(pool)
                        .await?;
                } else {
                    sqlx::query(r#"UPDATE "Users" SET gpodderurl = $1, gpodderloginname = $2, gpoddertoken = $3, pod_sync_type = 'external' WHERE userid = $4"#)
                        .bind(gpodder_url)
                        .bind(username)
                        .bind(&encrypted_password)
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                }
            }
            DatabasePool::MySQL(pool) => {
                if let Some(device_name) = &default_device_name {
                    sqlx::query("UPDATE Users SET GpodderUrl = ?, GpodderLoginName = ?, GpodderToken = ?, Pod_Sync_Type = 'external', DefaultGpodderDevice = ? WHERE UserID = ?")
                        .bind(gpodder_url)
                        .bind(username)
                        .bind(&encrypted_password)
                        .bind(device_name)
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                } else {
                    sqlx::query("UPDATE Users SET GpodderUrl = ?, GpodderLoginName = ?, GpodderToken = ?, Pod_Sync_Type = 'external' WHERE UserID = ?")
                        .bind(gpodder_url)
                        .bind(username)
                        .bind(&encrypted_password)
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                }
            }
        }
        
        // Spawn initial full sync as background task to avoid blocking the API response
        if let Some(device_name) = default_device_name.clone() {
            let pool_clone = self.clone();
            let gpodder_url_owned = gpodder_url.to_string();
            let username_owned = username.to_string();
            let password_owned = password.to_string();
            
            tokio::spawn(async move {
                if let Err(e) = pool_clone.call_gpodder_initial_full_sync(user_id, &gpodder_url_owned, &username_owned, &password_owned, &device_name).await {
                    tracing::warn!("Initial GPodder full sync failed during external server setup: {}", e);
                }
            });
        }
        
        Ok(true)
    }
    
    // Helper function to get first device from external GPodder server
    async fn get_first_device_from_server(&self, gpodder_url: &str, username: &str, password: &str) -> AppResult<String> {
        let session = self.create_gpodder_session_with_password(gpodder_url, username, password).await?;
        
        let devices_url = format!("{}/api/2/devices/{}.json", gpodder_url.trim_end_matches('/'), username);
        
        let response = if session.authenticated {
            session.client.get(&devices_url).send().await
        } else {
            session.client.get(&devices_url).basic_auth(username, Some(password)).send().await
        };
        
        match response {
            Ok(resp) if resp.status().is_success() => {
                let devices_data: serde_json::Value = resp.json().await
                    .map_err(|e| AppError::internal(&format!("Failed to parse devices: {}", e)))?;
                    
                if let Some(devices_array) = devices_data.as_array() {
                    if let Some(first_device) = devices_array.first() {
                        if let Some(device_id) = first_device.get("id").and_then(|v| v.as_str()) {
                            return Ok(device_id.to_string());
                        }
                    }
                }
                
                Err(AppError::internal("No devices found on external GPodder server"))
            }
            Ok(resp) => {
                Err(AppError::internal(&format!("Failed to get devices from external server: {}", resp.status())))
            }
            Err(e) => {
                Err(AppError::internal(&format!("Error connecting to external server: {}", e)))
            }
        }
    }
    
    // Encrypt password using Fernet - matches Python encryption
    pub async fn encrypt_password(&self, password: &str) -> AppResult<String> {
        use fernet::Fernet;
        
        // Get encryption key from app settings (base64 string)
        let encryption_key = self.get_encryption_key().await?;
        let fernet = Fernet::new(&encryption_key)
            .ok_or_else(|| AppError::internal("Failed to create Fernet cipher"))?;
        
        let encrypted = fernet.encrypt(password.as_bytes());
        Ok(encrypted)
    }
    
    // Get encryption key from app settings
    async fn get_encryption_key(&self) -> AppResult<String> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Try as string first (new format), fallback to bytes (old format)
                let row = sqlx::query(r#"SELECT encryptionkey FROM "AppSettings" LIMIT 1"#)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    // Try string first
                    if let Ok(key_string) = row.try_get::<String, _>("encryptionkey") {
                        return Ok(key_string);
                    }
                    // Fallback to bytes and convert to string
                    let key_bytes: Option<Vec<u8>> = row.try_get("encryptionkey")?;
                    if let Some(bytes) = key_bytes {
                        String::from_utf8(bytes)
                            .map_err(|e| AppError::internal(&format!("Invalid UTF-8 in encryption key: {}", e)))
                    } else {
                        Err(AppError::internal("Encryption key not found"))
                    }
                } else {
                    Err(AppError::internal("App settings not found"))
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT EncryptionKey FROM AppSettings LIMIT 1")
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let key: Option<String> = row.try_get("EncryptionKey")?;
                    key.ok_or_else(|| AppError::internal("Encryption key not found"))
                } else {
                    Err(AppError::internal("App settings not found"))
                }
            }
        }
    }

    // Get gPodder settings - matches Python get_gpodder_settings function exactly
    pub async fn get_gpodder_settings(&self, user_id: i32) -> AppResult<Option<serde_json::Value>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT gpodderurl, gpodderloginname, pod_sync_type FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let url: Option<String> = row.try_get("gpodderurl")?;
                    let username: Option<String> = row.try_get("gpodderloginname")?;
                    let sync_type: Option<String> = row.try_get("pod_sync_type")?;
                    
                    if url.is_some() && username.is_some() {
                        Ok(Some(serde_json::json!({
                            "gpodderurl": url.unwrap_or_default(),
                            "gpoddertoken": "", // Frontend expects this field but it's not used for display
                            "sync_type": sync_type.unwrap_or_default()
                        })))
                    } else {
                        Ok(None)
                    }
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT GpodderUrl, GpodderLoginName, Pod_Sync_Type FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let url: Option<String> = row.try_get("GpodderUrl")?;
                    let username: Option<String> = row.try_get("GpodderLoginName")?;
                    let sync_type: Option<String> = row.try_get("Pod_Sync_Type")?;
                    
                    if url.is_some() && username.is_some() {
                        Ok(Some(serde_json::json!({
                            "gpodderurl": url.unwrap_or_default(),
                            "gpoddertoken": "", // Frontend expects this field but it's not used for display
                            "sync_type": sync_type.unwrap_or_default()
                        })))
                    } else {
                        Ok(None)
                    }
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Check gPodder settings - matches Python check_gpodder_settings function exactly
    pub async fn check_gpodder_settings(&self, user_id: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT gpodderurl, gpodderloginname FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let url: Option<String> = row.try_get("gpodderurl")?;
                    let username: Option<String> = row.try_get("gpodderloginname")?;
                    Ok(url.is_some() && username.is_some())
                } else {
                    Ok(false)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT GpodderUrl, GpodderLoginName FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let url: Option<String> = row.try_get("GpodderUrl")?;
                    let username: Option<String> = row.try_get("GpodderLoginName")?;
                    Ok(url.is_some() && username.is_some())
                } else {
                    Ok(false)
                }
            }
        }
    }

    // Remove podcast sync - matches Python remove_podcast_sync function exactly
    pub async fn remove_podcast_sync(&self, user_id: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"UPDATE "Users" SET gpodderurl = NULL, gpodderloginname = NULL, gpoddertoken = NULL, pod_sync_type = 'None' WHERE userid = $1"#)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("UPDATE Users SET GpodderUrl = NULL, GpodderLoginName = NULL, GpodderToken = NULL, Pod_Sync_Type = 'None' WHERE UserID = ?")
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
        }
    }

    // Set default gPodder device - matches Python set_default_device function exactly
    pub async fn gpodder_set_default_device(&self, user_id: i32, device_id: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Clear all existing defaults for user
                sqlx::query(r#"UPDATE "GpodderDevices" SET isdefault = false WHERE userid = $1"#)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                // Set new default
                let result = sqlx::query(r#"UPDATE "GpodderDevices" SET isdefault = true WHERE deviceid = $1 AND userid = $2"#)
                    .bind(device_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
            DatabasePool::MySQL(pool) => {
                // Clear all existing defaults for user
                sqlx::query("UPDATE GpodderDevices SET IsDefault = false WHERE UserID = ?")
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                // Set new default
                let result = sqlx::query("UPDATE GpodderDevices SET IsDefault = true WHERE DeviceID = ? AND UserID = ?")
                    .bind(device_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
        }
    }

    // Set default gPodder device by name - for frontend compatibility
    pub async fn gpodder_set_default_device_by_name(&self, user_id: i32, device_name: &str) -> AppResult<bool> {
        println!("Setting default device: user_id={}, device_name='{}'", user_id, device_name);
        
        // Check sync type to determine which tables to update
        let sync_settings = self.get_user_sync_settings(user_id).await?;
        let is_external = sync_settings
            .as_ref()
            .map(|s| s.sync_type == "external")
            .unwrap_or(false);
        
        match self {
            DatabasePool::Postgres(pool) => {
                if !is_external {
                    // For internal/both: update GpodderDevices table
                    let clear_result = sqlx::query(r#"UPDATE "GpodderDevices" SET isdefault = false WHERE userid = $1"#)
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                    println!("Cleared {} devices for user {}", clear_result.rows_affected(), user_id);
                    
                    let result = sqlx::query(r#"UPDATE "GpodderDevices" SET isdefault = true WHERE devicename = $1 AND userid = $2"#)
                        .bind(device_name)
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                    println!("Set default result: {} rows affected", result.rows_affected());
                }
                
                // Always update Users table (for both internal and external)
                sqlx::query(r#"UPDATE "Users" SET defaultgpodderdevice = $1 WHERE userid = $2"#)
                    .bind(device_name)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                println!("Updated Users table with default device");
                
                Ok(true)
            }
            DatabasePool::MySQL(pool) => {
                if !is_external {
                    // For internal/both: update GpodderDevices table
                    let clear_result = sqlx::query("UPDATE GpodderDevices SET IsDefault = false WHERE UserID = ?")
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                    println!("Cleared {} devices for user {}", clear_result.rows_affected(), user_id);
                    
                    let result = sqlx::query("UPDATE GpodderDevices SET IsDefault = true WHERE DeviceName = ? AND UserID = ?")
                        .bind(device_name)
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                    println!("Set default result: {} rows affected", result.rows_affected());
                }
                
                // Always update Users table (for both internal and external)
                sqlx::query("UPDATE Users SET DefaultGpodderDevice = ? WHERE UserID = ?")
                    .bind(device_name)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                println!("Updated Users table with default device");
                
                Ok(true)
            }
        }
    }

    // Get gPodder devices for user - matches Python get_devices function exactly with remote device support
    pub async fn gpodder_get_user_devices(&self, user_id: i32) -> AppResult<Vec<serde_json::Value>> {
        // Check what type of sync is configured
        if let Some(sync_settings) = self.get_user_sync_settings(user_id).await? {
            match sync_settings.sync_type.as_str() {
                "gpodder" | "external" => {
                    // Both internal and external use HTTP API calls to GPodder server
                    // Internal: http://localhost:8042, External: user's configured URL
                    let mut devices = self.fetch_devices_from_gpodder_api(&sync_settings).await?;
                    
                    // Get the default device name from Users table to mark the correct device
                    let default_device_name = match self {
                        DatabasePool::Postgres(pool) => {
                            let row = sqlx::query(r#"SELECT defaultgpodderdevice FROM "Users" WHERE userid = $1"#)
                                .bind(user_id)
                                .fetch_optional(pool)
                                .await?;
                            
                            row.and_then(|r| r.try_get::<Option<String>, _>("defaultgpodderdevice").ok().flatten())
                        }
                        DatabasePool::MySQL(pool) => {
                            let row = sqlx::query("SELECT DefaultGpodderDevice FROM Users WHERE UserID = ?")
                                .bind(user_id)
                                .fetch_optional(pool)
                                .await?;
                            
                            row.and_then(|r| r.try_get::<Option<String>, _>("DefaultGpodderDevice").ok().flatten())
                        }
                    };
                    
                    // Mark the default device
                    if let Some(default_name) = default_device_name {
                        for device in &mut devices {
                            if device.get("name").and_then(|v| v.as_str()) == Some(&default_name) {
                                device["is_default"] = serde_json::Value::Bool(true);
                                device["is_remote"] = serde_json::Value::Bool(false); // Default device is treated as local
                                break;
                            }
                        }
                    }
                    
                    Ok(devices)
                }
                "nextcloud" => {
                    // Nextcloud doesn't have device concept like GPodder
                    Ok(vec![])
                }
                _ => {
                    // No sync configured - return empty list
                    Ok(vec![])
                }
            }
        } else {
            // No sync settings found - return empty list
            Ok(vec![])
        }
    }

    // Get local devices only - internal helper
    async fn get_local_devices(&self, user_id: i32) -> AppResult<Vec<serde_json::Value>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(r#"SELECT deviceid, devicename, devicetype, isdefault FROM "GpodderDevices" WHERE userid = $1 ORDER BY devicename"#)
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;
                
                let mut devices = Vec::new();
                for row in rows {
                    devices.push(serde_json::json!({
                        "id": row.try_get::<i32, _>("deviceid")?,
                        "name": row.try_get::<String, _>("devicename")?,
                        "type": row.try_get::<String, _>("devicetype")?,
                        "caption": Option::<String>::None, // Local devices don't have captions
                        "last_sync": Option::<String>::None,
                        "is_active": true,
                        "is_default": row.try_get::<bool, _>("isdefault")?,
                        "is_remote": false
                    }));
                }
                Ok(devices)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query("SELECT DeviceID, DeviceName, DeviceType, IsDefault FROM GpodderDevices WHERE UserID = ? ORDER BY DeviceName")
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;
                
                let mut devices = Vec::new();
                for row in rows {
                    devices.push(serde_json::json!({
                        "id": row.try_get::<i32, _>("DeviceID")?,
                        "name": row.try_get::<String, _>("DeviceName")?,
                        "type": row.try_get::<String, _>("DeviceType")?,
                        "caption": Option::<String>::None,
                        "last_sync": Option::<String>::None,
                        "is_active": true,
                        "is_default": row.try_get::<bool, _>("IsDefault")?,
                        "is_remote": false
                    }));
                }
                Ok(devices)
            }
        }
    }

    // Fetch devices from GPodder API server - works for both internal and external
    async fn fetch_devices_from_gpodder_api(&self, settings: &UserSyncSettings) -> AppResult<Vec<serde_json::Value>> {
        // For internal GPodder API, use X-GPodder-Token header
        // For external GPodder API, use session auth with basic auth fallback
        let (client, auth_headers) = if settings.url == "http://localhost:8042" {
            // Internal GPodder API - use X-GPodder-Token
            let client = reqwest::Client::new();
            let mut headers = reqwest::header::HeaderMap::new();
            headers.insert("X-GPodder-Token", settings.token.parse().unwrap());
            (client, Some(headers))
        } else {
            // External GPodder API - decrypt password and use session auth  
            let decrypted_password = self.decrypt_password(&settings.token).await?;
            let session = self.create_gpodder_session_with_password(&settings.url, &settings.username, &decrypted_password).await?;
            (session.client, None)
        };
        
        let devices_url = format!("{}/api/2/devices/{}.json", 
            settings.url.trim_end_matches('/'), settings.username);
        
        let mut request = client.get(&devices_url);
        
        // Add authentication headers if needed
        if let Some(headers) = auth_headers {
            request = request.headers(headers);
        } else {
            // External server with session auth - if session failed, fall back to basic auth
            let decrypted_password = self.decrypt_password(&settings.token).await?;
            request = request.basic_auth(&settings.username, Some(&decrypted_password));
        }
        
        let response = request
            .send()
            .await
            .map_err(|e| AppError::internal(&format!("Failed to fetch devices from GPodder API: {}", e)))?;
        
        if response.status().is_success() {
            let devices_data: serde_json::Value = response.json().await
                .map_err(|e| AppError::internal(&format!("Failed to parse devices from GPodder API: {}", e)))?;
            
            let mut devices = Vec::new();
            
            if let Some(devices_array) = devices_data.as_array() {
                for device in devices_array.iter() {
                    if let Some(device_id) = device["id"].as_str() {
                        // Convert GPodder API response to Pinepods device format
                        devices.push(serde_json::json!({
                            "id": device_id,  // Use actual GPodder device ID (string)
                            "name": device_id,
                            "type": device["type"].as_str().unwrap_or("other"),
                            "caption": device["caption"].as_str().unwrap_or(device_id),
                            "last_sync": Option::<String>::None,
                            "is_active": true,
                            "is_default": false, // GPodder devices are not local defaults
                            "is_remote": true,
                            "subscriptions": device["subscriptions"].as_i64().unwrap_or(0)
                        }));
                    }
                }
            }
            
            Ok(devices)
        } else {
            tracing::warn!("Failed to fetch devices from GPodder API: {}", response.status());
            Ok(Vec::new())
        }
    }

    // Create gPodder session with authentication - matches Python session handling
    async fn create_gpodder_session(&self, settings: &UserSyncSettings) -> AppResult<GpodderSession> {
        let client = reqwest::Client::builder()
            .build()
            .map_err(|e| AppError::internal(&format!("Failed to create HTTP client: {}", e)))?;
        
        let password = if settings.url == "http://localhost:8042" {
            // Internal API uses encrypted token directly
            settings.token.clone()
        } else {
            // External API needs decrypted password
            self.decrypt_password(&settings.token).await?
        };
        
        // Try session-based authentication first - matches Python login flow
        let login_url = format!("{}/api/2/auth/{}/login.json", 
            settings.url.trim_end_matches('/'), settings.username);
        
        let login_response = client
            .post(&login_url)
            .basic_auth(&settings.username, Some(&password))
            .send()
            .await;
        
        let session_authenticated = match login_response {
            Ok(response) if response.status().is_success() => {
                tracing::info!("gPodder session authentication successful");
                true
            }
            _ => {
                tracing::warn!("gPodder session authentication failed, will use basic auth");
                false
            }
        };
        
        Ok(GpodderSession {
            client,
            session_id: None,
            authenticated: session_authenticated,
        })
    }

    // Create gPodder session with already-decrypted password (avoids double decryption)
    async fn create_gpodder_session_with_password(&self, gpodder_url: &str, username: &str, password: &str) -> AppResult<GpodderSession> {
        let jar = std::sync::Arc::new(reqwest::cookie::Jar::default());
        let client = reqwest::Client::builder()
            .cookie_provider(jar)
            .build()
            .map_err(|e| AppError::internal(&format!("Failed to create HTTP client: {}", e)))?;
        
        // Try session-based authentication first - matches Python login flow
        let login_url = format!("{}/api/2/auth/{}/login.json", 
            gpodder_url.trim_end_matches('/'), username);
        
        let login_response = client
            .post(&login_url)
            .basic_auth(username, Some(password))
            .send()
            .await;
        
        let session_authenticated = match login_response {
            Ok(response) if response.status().is_success() => {
                tracing::info!("gPodder session authentication successful");
                true
            }
            _ => {
                tracing::warn!("gPodder session authentication failed, will use basic auth");
                false
            }
        };
        
        Ok(GpodderSession {
            client,
            session_id: None,
            authenticated: session_authenticated,
        })
    }

    // gPodder force sync - calls Go gPodder service exactly like Python force_full_sync_to_gpodder
    pub async fn gpodder_force_sync(&self, user_id: i32) -> AppResult<bool> {
        let sync_settings = self.get_user_sync_settings(user_id).await?;
        if sync_settings.is_none() {
            return Ok(false);
        }
        
        let settings = sync_settings.unwrap();
        let device_name = self.get_or_create_default_device(user_id).await?;
        
        match settings.sync_type.as_str() {
            "gpodder" => {
                // Internal gPodder API on localhost:8042 - use unencrypted token directly
                self.call_gpodder_service_sync(user_id, "http://localhost:8042", &settings.username, &settings.token, &device_name, true).await
            }
            "nextcloud" => {
                self.sync_with_nextcloud(user_id, &settings, true).await
            }
            "external" => {
                // External gPodder server - decrypt token first
                let decrypted_token = self.decrypt_password(&settings.token).await?;
                self.call_gpodder_service_sync(user_id, &settings.url, &settings.username, &decrypted_token, &device_name, true).await
            }
            "both" => {
                let internal_result = self.call_gpodder_service_sync(user_id, "http://localhost:8042", &settings.username, &settings.token, &device_name, true).await?;
                let decrypted_token = self.decrypt_password(&settings.token).await?;
                let external_result = self.call_gpodder_service_sync(user_id, &settings.url, &settings.username, &decrypted_token, &device_name, true).await?;
                Ok(internal_result || external_result)
            }
            _ => Ok(false)
        }
    }

    // gPodder regular sync - calls Go gPodder service exactly like Python refresh_gpodder_subscription
    pub async fn gpodder_sync(&self, user_id: i32) -> AppResult<SyncResult> {
        let sync_settings = self.get_user_sync_settings(user_id).await?;
        if sync_settings.is_none() {
            return Ok(SyncResult { synced_podcasts: 0, synced_episodes: 0 });
        }
        
        let settings = sync_settings.unwrap();
        let device_name = self.get_or_create_default_device(user_id).await?;
        
        let success = match settings.sync_type.as_str() {
            "gpodder" => {
                self.call_gpodder_service_sync(user_id, "http://localhost:8042", &settings.username, &settings.token, &device_name, false).await?
            }
            "nextcloud" => {
                self.sync_with_nextcloud(user_id, &settings, false).await?
            }
            "external" => {
                let decrypted_token = self.decrypt_password(&settings.token).await?;
                self.call_gpodder_service_sync(user_id, &settings.url, &settings.username, &decrypted_token, &device_name, false).await?
            }
            "both" => {
                let internal_success = self.call_gpodder_service_sync(user_id, "http://localhost:8042", &settings.username, &settings.token, &device_name, false).await?;
                let decrypted_token = self.decrypt_password(&settings.token).await?;
                let external_success = self.call_gpodder_service_sync(user_id, &settings.url, &settings.username, &decrypted_token, &device_name, false).await?;
                internal_success || external_success
            }
            _ => false
        };
        
        Ok(SyncResult { 
            synced_podcasts: if success { 1 } else { 0 }, 
            synced_episodes: 0 
        })
    }

    // Call gPodder service for sync - matches Python API calls exactly with enhanced error handling
    async fn call_gpodder_service_sync(&self, user_id: i32, gpodder_url: &str, username: &str, password: &str, device_name: &str, force: bool) -> AppResult<bool> {
        // Step 1: Get ALL devices first (critical for detecting changes from external devices like AntennaPod)
        let devices_url = format!("{}/api/2/devices/{}.json", 
            gpodder_url.trim_end_matches('/'), username);
        
        // Use correct authentication based on internal vs external
        let devices_response = if gpodder_url == "http://localhost:8042" {
            // Internal GPodder API - use X-GPodder-Token header
            let client = reqwest::Client::new();
            client.get(&devices_url)
                .header("X-GPodder-Token", password)
                .send()
                .await
        } else {
            // External GPodder API - use session auth with basic fallback
            let session = self.create_gpodder_session_with_password(gpodder_url, username, password).await?;
            if session.authenticated {
                session.client
                    .get(&devices_url)
                    .send()
                    .await
            } else {
                session.client
                    .get(&devices_url)
                    .basic_auth(username, Some(password))
                    .send()
                    .await
            }
        };
        
        let devices = match devices_response {
            Ok(resp) if resp.status().is_success() => {
                let devices_data: serde_json::Value = resp.json().await
                    .map_err(|e| AppError::internal(&format!("Failed to parse devices: {}", e)))?;
                
                if let Some(devices_array) = devices_data.as_array() {
                    let device_names: Vec<String> = devices_array.iter()
                        .filter_map(|device| device.get("id").and_then(|v| v.as_str()).map(|s| s.to_string()))
                        .collect();
                    
                    tracing::info!("Found {} devices for user: {:?}", device_names.len(), device_names);
                    device_names
                } else {
                    tracing::warn!("No devices found for user");
                    vec![]
                }
            }
            Ok(resp) => {
                tracing::warn!("Failed to get devices: {}", resp.status());
                if force {
                    return Err(AppError::internal(&format!("Force sync failed to get devices: {}", resp.status())));
                } else {
                    return Ok(false);
                }
            }
            Err(e) => {
                tracing::error!("Failed to connect to get devices: {}", e);
                if force {
                    return Err(AppError::internal(&format!("Force sync connection failed: {}", e)));
                } else {
                    return Ok(false);
                }
            }
        };
        
        tracing::info!("Found {} devices to sync from: {:?}", devices.len(), devices);
        
        // Step 2: Get subscriptions from ALL devices with timestamps (like AntennaPod sync)
        let since_timestamp = self.get_last_sync_timestamp(user_id).await?;
        let mut all_subscriptions = std::collections::HashSet::new();
        let mut all_removals = std::collections::HashSet::new();
        
        for device_id in &devices {
            tracing::info!("Getting subscriptions from device: {}", device_id);
            
            let subscriptions_url = if let Some(since) = since_timestamp {
                format!("{}/api/2/subscriptions/{}/{}.json?since={}", 
                    gpodder_url.trim_end_matches('/'), username, device_id, since.timestamp())
            } else {
                format!("{}/api/2/subscriptions/{}/{}.json?since=0", 
                    gpodder_url.trim_end_matches('/'), username, device_id)
            };
            
            let device_response = if gpodder_url == "http://localhost:8042" {
                let client = reqwest::Client::new();
                client.get(&subscriptions_url)
                    .header("X-GPodder-Token", password)
                    .send()
                    .await
            } else {
                let session = self.create_gpodder_session_with_password(gpodder_url, username, password).await?;
                if session.authenticated {
                    session.client
                        .get(&subscriptions_url)
                        .send()
                        .await
                } else {
                    session.client
                        .get(&subscriptions_url)
                        .basic_auth(username, Some(password))
                        .send()
                        .await
                }
            };
            
            match device_response {
                Ok(resp) if resp.status().is_success() => {
                    let response_text = resp.text().await
                        .map_err(|e| AppError::internal(&format!("Failed to get response text: {}", e)))?;
                    
                    let sync_response: serde_json::Value = serde_json::from_str(&response_text)
                        .map_err(|e| AppError::internal(&format!("Failed to parse gPodder sync response: {}", e)))?;
                    
                    let device_subscriptions = sync_response["add"].as_array()
                        .unwrap_or(&vec![])
                        .iter()
                        .filter_map(|v| v.as_str().map(|s| s.to_string()))
                        .collect::<Vec<String>>();
                    
                    let device_removals = sync_response["remove"].as_array()
                        .unwrap_or(&vec![])
                        .iter()
                        .filter_map(|v| v.as_str().map(|s| s.to_string()))
                        .collect::<Vec<String>>();
                    
                    tracing::info!("Device {} has {} subscriptions and {} removals", device_id, device_subscriptions.len(), device_removals.len());
                    
                    // Add to combined sets (HashSet automatically deduplicates)
                    for subscription in device_subscriptions {
                        all_subscriptions.insert(subscription);
                    }
                    
                    for removal in device_removals {
                        all_removals.insert(removal);
                    }
                }
                Ok(resp) => {
                    tracing::warn!("Device {} returned error: {}", device_id, resp.status());
                    // Continue with other devices instead of failing
                }
                Err(e) => {
                    tracing::warn!("Failed to get subscriptions from device {}: {}", device_id, e);
                    // Continue with other devices instead of failing
                }
            }
        }
        
        tracing::info!("Downloaded {} unique subscriptions and {} unique removals from ALL devices", all_subscriptions.len(), all_removals.len());
        
        // Step 3: Get episode actions from ALL devices with timestamps
        let mut all_episode_actions = Vec::new();
        
        for device_id in &devices {
            tracing::info!("Getting episode actions from device: {}", device_id);
            
            let episode_actions_url = if let Some(since) = since_timestamp {
                format!("{}/api/2/episodes/{}.json?since={}&device={}", 
                    gpodder_url.trim_end_matches('/'), username, since.timestamp(), device_id)
            } else {
                format!("{}/api/2/episodes/{}.json?since=0&device={}", 
                    gpodder_url.trim_end_matches('/'), username, device_id)
            };
            
            let device_response = if gpodder_url == "http://localhost:8042" {
                let client = reqwest::Client::new();
                client.get(&episode_actions_url)
                    .header("X-GPodder-Token", password)
                    .send()
                    .await
            } else {
                let session = self.create_gpodder_session_with_password(gpodder_url, username, password).await?;
                if session.authenticated {
                    session.client
                        .get(&episode_actions_url)
                        .send()
                        .await
                } else {
                    session.client
                        .get(&episode_actions_url)
                        .basic_auth(username, Some(password))
                        .send()
                        .await
                }
            };
            
            match device_response {
                Ok(resp) if resp.status().is_success() => {
                    let response_text = resp.text().await
                        .map_err(|e| AppError::internal(&format!("Failed to get episode actions response text: {}", e)))?;
                    
                    let episode_actions: serde_json::Value = serde_json::from_str(&response_text)
                        .map_err(|e| AppError::internal(&format!("Failed to parse episode actions response: {}", e)))?;
                    
                    if let Some(actions_array) = episode_actions["actions"].as_array() {
                        let device_actions_count = actions_array.len();
                        tracing::info!("Device {} has {} episode actions", device_id, device_actions_count);
                        
                        for action in actions_array {
                            all_episode_actions.push(action.clone());
                        }
                    }
                }
                Ok(resp) => {
                    tracing::warn!("Device {} episode actions returned error: {}", device_id, resp.status());
                }
                Err(e) => {
                    tracing::warn!("Failed to get episode actions from device {}: {}", device_id, e);
                }
            }
        }
        
        tracing::info!("Downloaded {} episode actions from ALL devices", all_episode_actions.len());
        
        // Step 4: Process all subscriptions (additions)
        let subscriptions_vec: Vec<String> = all_subscriptions.into_iter().collect();
        self.process_gpodder_subscriptions(user_id, &subscriptions_vec).await?;
        
        // Step 5: Process all subscription removals
        let removals_vec: Vec<String> = all_removals.into_iter().collect();
        if !removals_vec.is_empty() {
            self.process_gpodder_subscription_removals(user_id, &removals_vec).await?;
        }
        
        // Step 6: Process all episode actions
        if !all_episode_actions.is_empty() {
            if let Err(e) = self.apply_remote_episode_actions(user_id, &all_episode_actions).await {
                tracing::warn!("Episode actions processing failed but continuing: {}", e);
            }
        }
        
        // Step 7: Upload local subscriptions to gPodder service (to default device)
        if since_timestamp.is_none() || !subscriptions_vec.is_empty() || !removals_vec.is_empty() {
            let local_subscriptions = self.get_user_podcast_feeds(user_id).await?;
            self.upload_subscriptions_to_gpodder(gpodder_url, username, password, device_name, &local_subscriptions).await?;
        } else {
            tracing::info!("Skipping subscription upload - no changes detected in incremental sync");
        }
        
        // Step 8: Upload local episode actions to gPodder service (to default device)
        if let Err(e) = self.sync_episode_actions_with_gpodder(gpodder_url, username, password, device_name, user_id).await {
            tracing::warn!("Episode actions sync failed but continuing: {}", e);
        }
        
        // Step 9: Update last sync timestamp for next incremental sync
        self.update_last_sync_timestamp(user_id).await?;
        
        Ok(true)
    }

    // Initial full sync for GPodder - gets ALL user subscriptions from ALL devices
    pub async fn call_gpodder_initial_full_sync(&self, user_id: i32, gpodder_url: &str, username: &str, password: &str, device_name: &str) -> AppResult<bool> {
        tracing::info!("Starting initial full GPodder sync for user {} from {}", user_id, gpodder_url);
        
        // Step 1: Get ALL devices first (this is how AntennaPod and other apps do it)
        let devices_url = format!("{}/api/2/devices/{}.json", 
            gpodder_url.trim_end_matches('/'), username);
        
        let devices_response = if gpodder_url == "http://localhost:8042" {
            let client = reqwest::Client::new();
            client.get(&devices_url).header("X-GPodder-Token", password).send().await
        } else {
            let session = self.create_gpodder_session_with_password(gpodder_url, username, password).await?;
            if session.authenticated {
                session.client.get(&devices_url).send().await
            } else {
                session.client.get(&devices_url).basic_auth(username, Some(password)).send().await
            }
        };
        
        let devices = match devices_response {
            Ok(resp) if resp.status().is_success() => {
                let devices_data: serde_json::Value = resp.json().await
                    .map_err(|e| AppError::internal(&format!("Failed to parse devices: {}", e)))?;
                
                if let Some(devices_array) = devices_data.as_array() {
                    let device_names: Vec<String> = devices_array.iter()
                        .filter_map(|device| device.get("id").and_then(|v| v.as_str()).map(|s| s.to_string()))
                        .collect();
                    
                    tracing::info!("Found {} devices for user: {:?}", device_names.len(), device_names);
                    device_names
                } else {
                    tracing::warn!("No devices found for user");
                    vec![]
                }
            }
            Ok(resp) => {
                tracing::error!("Failed to get devices: {}", resp.status());
                return Ok(false);
            }
            Err(e) => {
                tracing::error!("Failed to connect for devices: {}", e);
                return Ok(false);
            }
        };
        
        // Step 2: Get subscriptions from ALL devices (like AntennaPod does)
        let mut all_subscriptions = std::collections::HashSet::new();
        
        for device_id in &devices {
            tracing::info!("Getting subscriptions from device: {}", device_id);
            
            let subscriptions_url = format!("{}/api/2/subscriptions/{}/{}.json?since=0", 
                gpodder_url.trim_end_matches('/'), username, device_id);
            
            let response = if gpodder_url == "http://localhost:8042" {
                let client = reqwest::Client::new();
                client.get(&subscriptions_url).header("X-GPodder-Token", password).send().await
            } else {
                let session = self.create_gpodder_session_with_password(gpodder_url, username, password).await?;
                if session.authenticated {
                    session.client.get(&subscriptions_url).send().await
                } else {
                    session.client.get(&subscriptions_url).basic_auth(username, Some(password)).send().await
                }
            };
            
            match response {
                Ok(resp) if resp.status().is_success() => {
                    let response_text = resp.text().await
                        .map_err(|e| AppError::internal(&format!("Failed to get response text: {}", e)))?;
                    
                    let sync_response: serde_json::Value = serde_json::from_str(&response_text)
                        .map_err(|e| AppError::internal(&format!("Failed to parse gPodder sync response: {}", e)))?;
                    
                    let device_subscriptions = sync_response["add"].as_array()
                        .unwrap_or(&vec![])
                        .iter()
                        .filter_map(|v| v.as_str().map(|s| s.to_string()))
                        .collect::<Vec<String>>();
                    
                    tracing::info!("Device {} has {} subscriptions", device_id, device_subscriptions.len());
                    
                    // Add all subscriptions to our set (deduplicates automatically)
                    for subscription in device_subscriptions {
                        all_subscriptions.insert(subscription);
                    }
                }
                Ok(resp) => {
                    tracing::warn!("Failed to get subscriptions from device {}: {}", device_id, resp.status());
                    // Continue with other devices
                }
                Err(e) => {
                    tracing::warn!("Error getting subscriptions from device {}: {}", device_id, e);
                    // Continue with other devices
                }
            }
        }
        
        let all_subscriptions: Vec<String> = all_subscriptions.into_iter().collect();
        tracing::info!("Total unique subscriptions from all devices: {}", all_subscriptions.len());
        
        // Step 2: Get episode actions from ALL devices (like subscription sync)
        let mut all_episode_actions = Vec::new();
        
        for device_id in &devices {
            tracing::info!("Getting episode actions from device: {}", device_id);
            
            let episode_actions_url = format!("{}/api/2/episodes/{}.json?since=0&device={}", 
                gpodder_url.trim_end_matches('/'), username, device_id);
            
            let response = if gpodder_url == "http://localhost:8042" {
                let client = reqwest::Client::new();
                client.get(&episode_actions_url).header("X-GPodder-Token", password).send().await
            } else {
                let session = self.create_gpodder_session_with_password(gpodder_url, username, password).await?;
                if session.authenticated {
                    session.client.get(&episode_actions_url).send().await
                } else {
                    session.client.get(&episode_actions_url).basic_auth(username, Some(password)).send().await
                }
            };
            
            match response {
                Ok(resp) if resp.status().is_success() => {
                    let episode_data: serde_json::Value = resp.json().await
                        .map_err(|e| AppError::internal(&format!("Failed to parse episode actions for device {}: {}", device_id, e)))?;
                    
                    if let Some(actions) = episode_data.get("actions").and_then(|v| v.as_array()) {
                        tracing::info!("Device {} has {} episode actions", device_id, actions.len());
                        
                        // Add all episode actions from this device
                        for action in actions {
                            all_episode_actions.push(action.clone());
                        }
                    }
                }
                Ok(resp) => {
                    tracing::warn!("Failed to get episode actions from device {}: {}", device_id, resp.status());
                    // Continue with other devices
                }
                Err(e) => {
                    tracing::warn!("Error getting episode actions from device {}: {}", device_id, e);
                    // Continue with other devices
                }
            }
        }
        
        tracing::info!("Total episode actions from all devices: {}", all_episode_actions.len());
        
        // Process all episode actions and apply them locally
        if !all_episode_actions.is_empty() {
            if let Err(e) = self.apply_remote_episode_actions(user_id, &all_episode_actions).await {
                tracing::warn!("Failed to apply remote episode actions: {}", e);
            }
        }
        
        // Step 3: Process all subscriptions and add missing podcasts
        self.process_gpodder_subscriptions(user_id, &all_subscriptions).await?;
        
        // Step 4: Upload local subscriptions to GPodder service
        let local_subscriptions = self.get_user_podcast_feeds(user_id).await?;
        self.upload_subscriptions_to_gpodder(gpodder_url, username, password, device_name, &local_subscriptions).await?;
        
        // Step 5: Upload local episode actions to GPodder service
        let local_episode_actions = self.get_user_episode_actions(user_id).await?;
        if !local_episode_actions.is_empty() {
            if let Err(e) = self.upload_episode_actions_to_gpodder(gpodder_url, username, password, &local_episode_actions).await {
                tracing::warn!("Failed to upload local episode actions to GPodder: {}", e);
                // Don't fail the sync if episode actions upload fails
            }
        }
        
        // Step 5: Clear any existing sync timestamp to start fresh for incremental syncs
        self.clear_last_sync_timestamp(user_id).await?;
        
        tracing::info!("Initial full GPodder sync completed for user {}", user_id);
        Ok(true)
    }
    
    // Initial full sync for Nextcloud - gets ALL user subscriptions 
    pub async fn call_nextcloud_initial_full_sync(&self, user_id: i32, nextcloud_url: &str, username: &str, password: &str) -> AppResult<bool> {
        tracing::info!("Starting initial full Nextcloud sync for user {} from {}", user_id, nextcloud_url);
        
        let client = reqwest::Client::new();
        
        // Get ALL subscriptions from Nextcloud gPodder Sync app
        let subscriptions_url = format!("{}/index.php/apps/gpoddersync/subscriptions", nextcloud_url.trim_end_matches('/'));
        
        let response = client
            .get(&subscriptions_url)
            .basic_auth(username, Some(password))
            .send()
            .await
            .map_err(|e| AppError::internal(&format!("Failed to get Nextcloud subscriptions: {}", e)))?;
        
        if !response.status().is_success() {
            tracing::error!("Failed to get Nextcloud subscriptions: {}", response.status());
            return Ok(false);
        }
        
        let subscriptions: serde_json::Value = response.json().await
            .map_err(|e| AppError::internal(&format!("Failed to parse Nextcloud subscriptions: {}", e)))?;
        
        // Process subscriptions - Nextcloud returns array of feed URLs
        let feed_urls = if let Some(feeds) = subscriptions.as_array() {
            let urls: Vec<String> = feeds.iter()
                .filter_map(|f| f.as_str().map(|s| s.to_string()))
                .collect();
            
            tracing::info!("Downloaded {} subscriptions from Nextcloud", urls.len());
            urls
        } else {
            tracing::warn!("No subscriptions found in Nextcloud response");
            vec![]
        };
        
        // Get ALL episode actions from Nextcloud
        let episode_actions_url = format!("{}/index.php/apps/gpoddersync/episode_action", nextcloud_url.trim_end_matches('/'));
        
        let episode_response = client
            .get(&episode_actions_url)
            .basic_auth(username, Some(password))
            .send()
            .await;
        
        let mut all_episode_actions = Vec::new();
        
        match episode_response {
            Ok(resp) if resp.status().is_success() => {
                let episode_data: serde_json::Value = resp.json().await
                    .map_err(|e| AppError::internal(&format!("Failed to parse Nextcloud episode actions: {}", e)))?;
                
                if let Some(actions) = episode_data.get("actions").and_then(|v| v.as_array()) {
                    tracing::info!("Downloaded {} episode actions from Nextcloud", actions.len());
                    
                    // Add all episode actions
                    for action in actions {
                        all_episode_actions.push(action.clone());
                    }
                }
            }
            Ok(resp) => {
                tracing::warn!("Failed to get Nextcloud episode actions: {}", resp.status());
                // Continue even if episode actions fail
            }
            Err(e) => {
                tracing::warn!("Error getting Nextcloud episode actions: {}", e);
                // Continue even if episode actions fail
            }
        }
        
        // Process all episode actions and apply them locally
        if !all_episode_actions.is_empty() {
            if let Err(e) = self.apply_remote_episode_actions(user_id, &all_episode_actions).await {
                tracing::warn!("Failed to apply remote episode actions from Nextcloud: {}", e);
            }
        }
        
        // Process all subscriptions and add missing podcasts
        self.process_gpodder_subscriptions(user_id, &feed_urls).await?;
        
        // Upload local subscriptions to Nextcloud
        let local_subscriptions = self.get_user_podcast_feeds(user_id).await?;
        self.upload_subscriptions_to_nextcloud(nextcloud_url, username, password, &local_subscriptions).await?;
        
        // Upload local episode actions to Nextcloud
        let local_episode_actions = self.get_user_episode_actions(user_id).await?;
        if !local_episode_actions.is_empty() {
            if let Err(e) = self.upload_episode_actions_to_nextcloud(nextcloud_url, username, password, &local_episode_actions).await {
                tracing::warn!("Failed to upload local episode actions to Nextcloud: {}", e);
                // Don't fail the sync if episode actions upload fails
            }
        }
        
        // Clear any existing sync timestamp to start fresh for incremental syncs
        self.clear_last_sync_timestamp(user_id).await?;
        
        tracing::info!("Initial full Nextcloud sync completed for user {}", user_id);
        Ok(true)
    }
    
    // Upload episode actions to GPodder server for initial sync
    async fn upload_episode_actions_to_gpodder(&self, gpodder_url: &str, username: &str, password: &str, episode_actions: &[serde_json::Value]) -> AppResult<()> {
        let upload_url = format!("{}/api/2/episodes/{}.json", gpodder_url.trim_end_matches('/'), username);
        
        // Use correct authentication based on internal vs external
        let response = if gpodder_url == "http://localhost:8042" {
            // Internal GPodder API - use X-GPodder-Token header
            let client = reqwest::Client::new();
            client.post(&upload_url)
                .header("X-GPodder-Token", password)
                .json(episode_actions)
                .send()
                .await
        } else {
            // External GPodder API - use session auth with basic fallback
            let session = self.create_gpodder_session_with_password(gpodder_url, username, password).await?;
            if session.authenticated {
                // Use session-based authentication
                session.client
                    .post(&upload_url)
                    .json(episode_actions)
                    .send()
                    .await
            } else {
                // Fallback to basic auth
                session.client
                    .post(&upload_url)
                    .basic_auth(username, Some(password))
                    .json(episode_actions)
                    .send()
                    .await
            }
        };
        
        match response {
            Ok(resp) if resp.status().is_success() => {
                tracing::info!("Successfully uploaded {} episode actions to GPodder", episode_actions.len());
                Ok(())
            }
            Ok(resp) => {
                tracing::warn!("Failed to upload episode actions to GPodder: {}", resp.status());
                Ok(()) // Don't fail the whole sync if upload fails
            }
            Err(e) => {
                Err(AppError::internal(&format!("Failed to upload episode actions to GPodder: {}", e)))
            }
        }
    }
    
    // Upload subscriptions to Nextcloud using the gPodder Sync app endpoint
    async fn upload_subscriptions_to_nextcloud(&self, nextcloud_url: &str, username: &str, password: &str, subscriptions: &[String]) -> AppResult<()> {
        let client = reqwest::Client::new();
        // Nextcloud gPodder Sync app uses the subscription_change endpoint
        let upload_url = format!("{}/index.php/apps/gpoddersync/subscription_change/upload", nextcloud_url.trim_end_matches('/'));
        
        let response = client
            .post(&upload_url)
            .basic_auth(username, Some(password))
            .json(subscriptions)
            .send()
            .await
            .map_err(|e| AppError::internal(&format!("Failed to upload subscriptions to Nextcloud: {}", e)))?;
        
        if response.status().is_success() {
            tracing::info!("Successfully uploaded {} subscriptions to Nextcloud", subscriptions.len());
            Ok(())
        } else {
            tracing::warn!("Failed to upload subscriptions to Nextcloud: {}", response.status());
            Ok(()) // Don't fail the whole sync if upload fails
        }
    }
    
    // Upload episode actions to Nextcloud using the gPodder Sync app endpoint
    async fn upload_episode_actions_to_nextcloud(&self, nextcloud_url: &str, username: &str, password: &str, episode_actions: &[serde_json::Value]) -> AppResult<()> {
        let client = reqwest::Client::new();
        // Nextcloud gPodder Sync app uses the episode_action endpoint  
        let upload_url = format!("{}/index.php/apps/gpoddersync/episode_action/create", nextcloud_url.trim_end_matches('/'));
        
        let response = client
            .post(&upload_url)
            .basic_auth(username, Some(password))
            .json(episode_actions)
            .send()
            .await
            .map_err(|e| AppError::internal(&format!("Failed to upload episode actions to Nextcloud: {}", e)))?;
        
        if response.status().is_success() {
            tracing::info!("Successfully uploaded {} episode actions to Nextcloud", episode_actions.len());
            Ok(())
        } else {
            tracing::warn!("Failed to upload episode actions to Nextcloud: {}", response.status());
            Ok(()) // Don't fail the whole sync if upload fails
        }
    }
    
    // Upload subscriptions with session support and error handling
    async fn upload_subscriptions_to_gpodder_with_session(&self, session: &GpodderSession, gpodder_url: &str, username: &str, password: &str, device_name: &str, subscriptions: &[String]) -> AppResult<()> {
        let upload_url = format!("{}/api/2/subscriptions/{}/{}.json", gpodder_url.trim_end_matches('/'), username, device_name);
        
        // Format subscription changes according to GPodder API spec
        let subscription_changes = serde_json::json!({
            "add": subscriptions,
            "remove": []
        });
        
        let response = if session.authenticated {
            session.client
                .post(&upload_url)
                .json(&subscription_changes)
                .send()
                .await
        } else {
            session.client
                .post(&upload_url)
                .basic_auth(username, Some(password))
                .json(&subscription_changes)
                .send()
                .await
        };
        
        match response {
            Ok(resp) if resp.status().is_success() => {
                tracing::info!("Successfully uploaded {} subscriptions to gPodder service", subscriptions.len());
                Ok(())
            }
            Ok(resp) => {
                tracing::warn!("Failed to upload subscriptions: {}", resp.status());
                // Don't fail sync for upload failures - log and continue
                Ok(())
            }
            Err(e) => {
                tracing::warn!("Error uploading subscriptions: {}", e);
                // Don't fail sync for upload failures - log and continue
                Ok(())
            }
        }
    }

    // Process gPodder subscriptions and add missing podcasts
    async fn process_gpodder_subscriptions(&self, user_id: i32, subscriptions: &[String]) -> AppResult<()> {
        let mut added = 0;
        let mut skipped = 0;
        let mut failed = 0;
        
        for feed_url in subscriptions {
            tracing::info!("Processing podcast {} for user {}", feed_url, user_id);
            
            // Use the existing add_podcast_from_url function which handles duplicates and fetching
            match self.add_podcast_from_url(user_id, feed_url, None).await {
                Ok(_) => {
                    added += 1;
                    tracing::info!("Successfully added podcast: {}", feed_url);
                }
                Err(e) => {
                    // Check if it failed because it already exists
                    if self.podcast_exists_for_user(user_id, feed_url).await.unwrap_or(false) {
                        skipped += 1;
                        tracing::debug!("Podcast {} already exists for user {}", feed_url, user_id);
                    } else {
                        failed += 1;
                        tracing::warn!("Failed to add podcast {}: {}", feed_url, e);
                    }
                }
            }
        }
        
        tracing::info!("GPodder subscription sync completed: {} added, {} skipped, {} failed", added, skipped, failed);
        Ok(())
    }

    // Process subscription removals from gPodder service
    async fn process_gpodder_subscription_removals(&self, user_id: i32, removals: &[String]) -> AppResult<()> {
        let mut removed = 0;
        let mut not_found = 0;
        let mut failed = 0;
        
        for feed_url in removals {
            tracing::info!("Processing podcast removal {} for user {}", feed_url, user_id);
            
            // Check if the podcast exists locally first
            if self.podcast_exists_for_user(user_id, feed_url).await.unwrap_or(false) {
                // Remove the podcast using existing function
                match self.remove_podcast_by_url(user_id, feed_url).await {
                    Ok(_) => {
                        removed += 1;
                        tracing::info!("Successfully removed podcast: {}", feed_url);
                    }
                    Err(e) => {
                        failed += 1;
                        tracing::warn!("Failed to remove podcast {}: {}", feed_url, e);
                    }
                }
            } else {
                not_found += 1;
                tracing::debug!("Podcast {} not found locally for user {} (already removed)", feed_url, user_id);
            }
        }
        
        tracing::info!("GPodder subscription removal completed: {} removed, {} not found, {} failed", removed, not_found, failed);
        Ok(())
    }

    // Detect and remove orphaned local podcasts that are not in the remote subscription list
    async fn sync_local_podcast_removals(&self, user_id: i32, remote_subscriptions: &[String]) -> AppResult<Vec<String>> {
        let local_subscriptions = self.get_user_podcast_feeds(user_id).await?;
        let remote_set: std::collections::HashSet<String> = remote_subscriptions.iter().cloned().collect();
        
        let mut removed_podcasts = Vec::new();
        
        // Find podcasts that exist locally but not in remote subscriptions
        for local_feed in &local_subscriptions {
            if !remote_set.contains(local_feed) {
                tracing::info!("Local podcast {} not found in remote subscriptions, removing", local_feed);
                
                match self.remove_podcast_by_url(user_id, local_feed).await {
                    Ok(_) => {
                        removed_podcasts.push(local_feed.clone());
                        tracing::info!("Successfully removed orphaned local podcast: {}", local_feed);
                    }
                    Err(e) => {
                        tracing::warn!("Failed to remove orphaned local podcast {}: {}", local_feed, e);
                    }
                }
            }
        }
        
        if !removed_podcasts.is_empty() {
            tracing::info!("Removed {} orphaned local podcasts", removed_podcasts.len());
        }
        
        Ok(removed_podcasts)
    }

    // Upload local subscriptions to gPodder service - matches GPodder API spec POST /api/2/subscriptions/{username}/{device}.json
    async fn upload_subscriptions_to_gpodder(&self, gpodder_url: &str, username: &str, password: &str, device_name: &str, subscriptions: &[String]) -> AppResult<()> {
        let upload_url = format!("{}/api/2/subscriptions/{}/{}.json", gpodder_url.trim_end_matches('/'), username, device_name);
        
        // Format subscription changes according to GPodder API spec
        let subscription_changes = serde_json::json!({
            "add": subscriptions,
            "remove": []
        });
        
        // Use correct authentication based on internal vs external
        let response = if gpodder_url == "http://localhost:8042" {
            // Internal GPodder API - use X-GPodder-Token header
            let client = reqwest::Client::new();
            client.post(&upload_url)
                .header("X-GPodder-Token", password)
                .json(&subscription_changes)
                .send()
                .await
        } else {
            // External GPodder API - use session auth with basic fallback
            let session = self.create_gpodder_session_with_password(gpodder_url, username, password).await?;
            if session.authenticated {
                session.client
                    .post(&upload_url)
                    .json(&subscription_changes)
                    .send()
                    .await
            } else {
                session.client
                    .post(&upload_url)
                    .basic_auth(username, Some(password))
                    .json(&subscription_changes)
                    .send()
                    .await
            }
        };
        
        match response {
            Ok(resp) if resp.status().is_success() => {
                tracing::info!("Successfully uploaded {} subscriptions to gPodder service", subscriptions.len());
                Ok(())
            }
            Ok(resp) => {
                tracing::warn!("Failed to upload subscriptions to gPodder service: {}", resp.status());
                Ok(()) // Don't fail sync for upload failures
            }
            Err(e) => {
                tracing::warn!("Failed to upload subscriptions: {}", e);
                Ok(()) // Don't fail sync for upload failures
            }
        }
    }

    // Sync episode actions with gPodder service - matches Python episode actions sync with timestamp support
    async fn sync_episode_actions_with_gpodder(&self, gpodder_url: &str, username: &str, password: &str, device_name: &str, user_id: i32) -> AppResult<()> {
        // Get last sync timestamp for incremental sync (BETTER than Python - follows GPodder spec)
        let since_timestamp = self.get_last_sync_timestamp(user_id).await?;
        
        // Get local episode actions since last sync for efficient incremental sync
        let local_actions = if let Some(since) = since_timestamp {
            self.get_user_episode_actions_since(user_id, since).await?
        } else {
            self.get_user_episode_actions(user_id).await?
        };
        
        // Upload local actions to gPodder service - matches Python POST /api/2/episodes/{username}.json
        if !local_actions.is_empty() {
            let upload_url = format!("{}/api/2/episodes/{}.json", gpodder_url.trim_end_matches('/'), username);
            
            // Use correct authentication based on internal vs external
            let response = if gpodder_url == "http://localhost:8042" {
                // Internal GPodder API - use X-GPodder-Token header
                let client = reqwest::Client::new();
                client.post(&upload_url)
                    .header("X-GPodder-Token", password)
                    .json(&local_actions)
                    .send()
                    .await
            } else {
                // External GPodder API - use session auth with basic fallback
                let session = self.create_gpodder_session_with_password(gpodder_url, username, password).await?;
                if session.authenticated {
                    // Use session-based authentication
                    session.client
                        .post(&upload_url)
                        .json(&local_actions)
                        .send()
                        .await
                } else {
                    // Fallback to basic auth
                    session.client
                        .post(&upload_url)
                        .basic_auth(username, Some(password))
                        .json(&local_actions)
                        .send()
                        .await
                }
            };
            
            match response {
                Ok(resp) if resp.status().is_success() => {
                    tracing::info!("Successfully uploaded {} episode actions", local_actions.len());
                }
                Ok(resp) => {
                    tracing::warn!("Failed to upload episode actions: {}", resp.status());
                }
                Err(e) => {
                    return Err(AppError::internal(&format!("Failed to upload episode actions: {}", e)));
                }
            }
        }
        
        // Download remote actions from gPodder service with timestamp support - use same format as working sync
        let download_url = if let Some(since) = since_timestamp {
            format!("{}/api/2/episodes/{}.json?since={}&device={}", 
                gpodder_url.trim_end_matches('/'), username, since.timestamp(), device_name)
        } else {
            format!("{}/api/2/episodes/{}.json?since=0&device={}", 
                gpodder_url.trim_end_matches('/'), username, device_name)
        };
        
        // Use correct authentication based on internal vs external for download
        let response = if gpodder_url == "http://localhost:8042" {
            // Internal GPodder API - use X-GPodder-Token header
            let client = reqwest::Client::new();
            client.get(&download_url)
                .header("X-GPodder-Token", password)
                .send()
                .await
        } else {
            // External GPodder API - use session auth with basic fallback
            let session = self.create_gpodder_session_with_password(gpodder_url, username, password).await?;
            if session.authenticated {
                session.client
                    .get(&download_url)
                    .send()
                    .await
            } else {
                session.client
                    .get(&download_url)
                    .basic_auth(username, Some(password))
                    .send()
                    .await
            }
        };
        
        match response {
            Ok(resp) if resp.status().is_success() => {
                let episode_response: serde_json::Value = resp.json().await
                    .map_err(|e| AppError::internal(&format!("Failed to parse episode actions response: {}", e)))?;
                
                let remote_actions = episode_response.get("actions")
                    .and_then(|v| v.as_array())
                    .cloned()
                    .unwrap_or_default();
                
                tracing::info!("Downloaded {} remote episode actions", remote_actions.len());
                
                // Apply remote actions locally
                self.apply_remote_episode_actions(user_id, &remote_actions).await?;
                
                // Update last sync timestamp for incremental sync (BETTER than Python)
                self.update_last_sync_timestamp(user_id).await?;
            }
            Ok(resp) => {
                tracing::warn!("Failed to download episode actions: {}", resp.status());
            }
            Err(e) => {
                return Err(AppError::internal(&format!("Failed to download episode actions: {}", e)));
            }
        }
        
        Ok(())
    }

    // Get user podcast feeds for sync
    async fn get_user_podcast_feeds(&self, user_id: i32) -> AppResult<Vec<String>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(r#"SELECT feedurl FROM "Podcasts" WHERE userid = $1 AND (username IS NULL OR username = '') AND (password IS NULL OR password = '')"#)
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;
                
                let mut feeds = Vec::new();
                for row in rows {
                    feeds.push(row.try_get::<String, _>("feedurl")?);
                }
                Ok(feeds)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query("SELECT FeedURL FROM Podcasts WHERE UserID = ? AND (Username IS NULL OR Username = '') AND (Password IS NULL OR Password = '')")
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;
                
                let mut feeds = Vec::new();
                for row in rows {
                    feeds.push(row.try_get::<String, _>("FeedURL")?);
                }
                Ok(feeds)
            }
        }
    }

    // Apply remote episode actions locally - matches Python apply_episode_actions function exactly
    async fn apply_remote_episode_actions(&self, user_id: i32, actions: &[serde_json::Value]) -> AppResult<()> {
        tracing::info!("Processing {} episode actions for user {}", actions.len(), user_id);
        let mut applied_count = 0;
        let mut not_found_count = 0;
        
        for action in actions {
            if let (Some(episode_url), Some(action_type)) = (
                action["episode"].as_str(),
                action["action"].as_str()
            ) {
                match action_type {
                    "play" => {
                        if let (Some(position), Some(timestamp_str)) = (
                            action["position"].as_i64(),
                            action["timestamp"].as_str()
                        ) {
                            // Find local episode by URL
                            if let Some(episode_id) = self.find_episode_by_url(user_id, episode_url).await? {
                                // Parse timestamp - handle both RFC3339 and simple datetime formats
                                let timestamp = if let Ok(parsed) = chrono::DateTime::parse_from_rfc3339(timestamp_str) {
                                    parsed.naive_utc()
                                } else if let Ok(parsed) = chrono::NaiveDateTime::parse_from_str(timestamp_str, "%Y-%m-%dT%H:%M:%S") {
                                    parsed
                                } else {
                                    tracing::warn!("Failed to parse timestamp for episode action: {}", timestamp_str);
                                    continue;
                                };
                                
                                // Get episode duration to check if it should be marked as complete
                                if let Ok(episode_duration) = self.get_episode_duration(episode_id).await {
                                    let position_sec = position as i32;
                                    let remaining_time = episode_duration - position_sec;
                                    
                                    // Always log for debugging GPodder sync completion issues
                                    println!("GPodder sync episode: {} - Duration: {}s, Position: {}s, Remaining: {}s", 
                                             episode_url, episode_duration, position_sec, remaining_time);
                                    
                                    // Mark complete if position is at/beyond the end OR within 60 seconds of completion
                                    if episode_duration > 0 && (position_sec >= episode_duration || remaining_time <= 60) {
                                        // At end or within 1 minute of completion - mark as complete
                                        // GPodder sync only handles regular podcast episodes, never YouTube videos
                                        self.mark_episode_completed(episode_id, user_id, false).await?;
                                        applied_count += 1;
                                        println!("✓ Marked episode as completed via GPodder sync: {} ({}s of {}s)", 
                                                episode_url, position_sec, episode_duration);
                                    } else {
                                        // Update progress normally
                                        self.update_episode_progress(user_id, episode_id, position_sec, timestamp).await?;
                                        applied_count += 1;
                                        println!("Updated episode progress: {} -> {}s/{}s", episode_url, position_sec, episode_duration);
                                    }
                                } else {
                                    // Fallback to normal progress update if duration unavailable
                                    self.update_episode_progress(user_id, episode_id, position as i32, timestamp).await?;
                                    applied_count += 1;
                                    tracing::debug!("Applied episode action (no duration): {} -> position {}", episode_url, position);
                                }
                            } else {
                                not_found_count += 1;
                                tracing::debug!("Episode not found in local database: {}", episode_url);
                            }
                        }
                    }
                    "download" => {
                        // Handle download actions if needed
                        tracing::info!("Download action for episode: {}", episode_url);
                    }
                    "delete" => {
                        // Handle delete actions if needed
                        tracing::info!("Delete action for episode: {}", episode_url);
                    }
                    _ => {
                        tracing::warn!("Unknown action type: {}", action_type);
                    }
                }
            }
        }
        
        tracing::info!("Episode actions processing complete: {} applied, {} not found in local database", 
                      applied_count, not_found_count);
        Ok(())
    }
    
    // Find episode ID by URL for user
    async fn find_episode_by_url(&self, user_id: i32, episode_url: &str) -> AppResult<Option<i32>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"
                    SELECT e.episodeid 
                    FROM "Episodes" e 
                    JOIN "Podcasts" p ON e.podcastid = p.podcastid 
                    WHERE e.episodeurl = $1 AND p.userid = $2
                "#)
                    .bind(episode_url)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(Some(row.try_get("episodeid")?))
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("
                    SELECT e.EpisodeID 
                    FROM Episodes e 
                    JOIN Podcasts p ON e.PodcastID = p.PodcastID 
                    WHERE e.EpisodeURL = ? AND p.UserID = ?
                ")
                    .bind(episode_url)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(Some(row.try_get("EpisodeID")?))
                } else {
                    Ok(None)
                }
            }
        }
    }
    
    // Update episode progress from remote sync
    async fn update_episode_progress(&self, user_id: i32, episode_id: i32, position: i32, timestamp: chrono::NaiveDateTime) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Insert or update episode history
                sqlx::query(r#"
                    INSERT INTO "UserEpisodeHistory" (userid, episodeid, listenduration, listendate)
                    VALUES ($1, $2, $3, $4)
                    ON CONFLICT (userid, episodeid)
                    DO UPDATE SET listenduration = GREATEST("UserEpisodeHistory".listenduration, $3), listendate = $4
                "#)
                    .bind(user_id)
                    .bind(episode_id)
                    .bind(position)
                    .bind(timestamp)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                // Insert or update episode history
                sqlx::query("
                    INSERT INTO UserEpisodeHistory (UserID, EpisodeID, ListenDuration, ListenDate)
                    VALUES (?, ?, ?, ?)
                    ON DUPLICATE KEY UPDATE 
                        ListenDuration = GREATEST(ListenDuration, VALUES(ListenDuration)), 
                        ListenDate = VALUES(ListenDate)
                ")
                    .bind(user_id)
                    .bind(episode_id)
                    .bind(position)
                    .bind(timestamp)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Get gPodder status - matches Python get_user_gpodder_status function exactly
    pub async fn gpodder_get_status(&self, user_id: i32) -> AppResult<GpodderStatus> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT pod_sync_type, gpodderurl, gpodderloginname FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let sync_type: Option<String> = row.try_get("pod_sync_type")?;
                    let gpodder_url: Option<String> = row.try_get("gpodderurl")?;
                    let gpodder_login: Option<String> = row.try_get("gpodderloginname")?;
                    
                    let sync_type = sync_type.unwrap_or_else(|| "None".to_string());
                    
                    Ok(GpodderStatus {
                        sync_type: sync_type.clone(),
                        gpodder_url,
                        gpodder_login,
                    })
                } else {
                    Ok(GpodderStatus {
                        sync_type: "None".to_string(),
                        gpodder_url: None,
                        gpodder_login: None,
                    })
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT Pod_Sync_Type, GpodderUrl, GpodderLoginName FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let sync_type: Option<String> = row.try_get("Pod_Sync_Type")?;
                    let gpodder_url: Option<String> = row.try_get("GpodderUrl")?;
                    let gpodder_login: Option<String> = row.try_get("GpodderLoginName")?;
                    
                    let sync_type = sync_type.unwrap_or_else(|| "None".to_string());
                    
                    Ok(GpodderStatus {
                        sync_type: sync_type.clone(),
                        gpodder_url,
                        gpodder_login,
                    })
                } else {
                    Ok(GpodderStatus {
                        sync_type: "None".to_string(),
                        gpodder_url: None,
                        gpodder_login: None,
                    })
                }
            }
        }
    }

    // Toggle gPodder sync - matches Python toggle_gpodder function exactly
    pub async fn gpodder_toggle_sync(&self, user_id: i32) -> AppResult<bool> {
        let current_status = self.gpodder_get_status(user_id).await?;
        let current_enabled = current_status.sync_type != "None" && !current_status.sync_type.is_empty();
        let new_enabled = !current_enabled;
        
        let new_sync_type = if new_enabled {
            // Restore previous sync type or default to "external"
            if !current_status.sync_type.is_empty() && current_status.sync_type != "None" {
                current_status.sync_type
            } else {
                "external".to_string()
            }
        } else {
            "None".to_string()
        };
        
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Users" SET pod_sync_type = $1 WHERE userid = $2"#)
                    .bind(&new_sync_type)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Users SET Pod_Sync_Type = ? WHERE UserID = ?")
                    .bind(&new_sync_type)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        
        Ok(new_enabled)
    }

    // Helper function to get user sync settings
    pub async fn get_user_sync_settings(&self, user_id: i32) -> AppResult<Option<UserSyncSettings>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT gpodderurl, gpodderloginname, gpoddertoken, pod_sync_type FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let url: Option<String> = row.try_get("gpodderurl")?;
                    let username: Option<String> = row.try_get("gpodderloginname")?;
                    let token: Option<String> = row.try_get("gpoddertoken")?;
                    let sync_type: Option<String> = row.try_get("pod_sync_type")?;
                    
                    if url.is_some() && username.is_some() && token.is_some() {
                        Ok(Some(UserSyncSettings {
                            url: url.unwrap(),
                            username: username.unwrap(),
                            token: token.unwrap(),
                            sync_type: sync_type.unwrap_or_default()
                        }))
                    } else {
                        Ok(None)
                    }
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT GpodderUrl, GpodderLoginName, GpodderToken, Pod_Sync_Type FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let url: Option<String> = row.try_get("GpodderUrl")?;
                    let username: Option<String> = row.try_get("GpodderLoginName")?;
                    let token: Option<String> = row.try_get("GpodderToken")?;
                    let sync_type: Option<String> = row.try_get("Pod_Sync_Type")?;
                    
                    if url.is_some() && username.is_some() && token.is_some() {
                        Ok(Some(UserSyncSettings {
                            url: url.unwrap(),
                            username: username.unwrap(),
                            token: token.unwrap(),
                            sync_type: sync_type.unwrap_or_default()
                        }))
                    } else {
                        Ok(None)
                    }
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Check if podcast exists for user
    async fn podcast_exists_for_user(&self, user_id: i32, feed_url: &str) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT podcastid FROM "Podcasts" WHERE feedurl = $1 AND userid = $2"#)
                    .bind(feed_url)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(row.is_some())
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT PodcastID FROM Podcasts WHERE FeedURL = ? AND UserID = ?")
                    .bind(feed_url)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(row.is_some())
            }
        }
    }

    // Get or create default device - matches Python device handling
    pub async fn get_or_create_default_device(&self, user_id: i32) -> AppResult<String> {
        // Get the default device name from Users table - this is where PinePods tracks user preferences
        let default_device_name = match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT defaultgpodderdevice FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                row.and_then(|r| r.try_get::<Option<String>, _>("defaultgpodderdevice").ok().flatten())
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT DefaultGpodderDevice FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                row.and_then(|r| r.try_get::<Option<String>, _>("DefaultGpodderDevice").ok().flatten())
            }
        };
        
        // If we have a default device name from Users table, use it
        if let Some(device_name) = default_device_name {
            return Ok(device_name);
        }
        
        // Fallback: check sync settings to determine appropriate default
        if let Some(sync_settings) = self.get_user_sync_settings(user_id).await? {
            match sync_settings.sync_type.as_str() {
                "external" => {
                    // For external servers, we should not create devices - they must exist on the external server
                    return Err(AppError::BadRequest("No default device configured for external GPodder sync. Please configure a default device.".to_string()));
                }
                "gpodder" | "both" => {
                    // For internal sync, create a default internal device
                    let device_name = format!("pinepods-internal-{}", user_id);
                    let device_type = "server";
                    self.create_gpodder_device(user_id, &device_name, device_type, true).await?;
                    
                    // Set this as the default in Users table
                    match self {
                        DatabasePool::Postgres(pool) => {
                            sqlx::query(r#"UPDATE "Users" SET defaultgpodderdevice = $1 WHERE userid = $2"#)
                                .bind(&device_name)
                                .bind(user_id)
                                .execute(pool)
                                .await?;
                        }
                        DatabasePool::MySQL(pool) => {
                            sqlx::query("UPDATE Users SET DefaultGpodderDevice = ? WHERE UserID = ?")
                                .bind(&device_name)
                                .bind(user_id)
                                .execute(pool)
                                .await?;
                        }
                    }
                    
                    return Ok(device_name);
                }
                _ => {
                    return Err(AppError::BadRequest("GPodder sync not properly configured".to_string()));
                }
            }
        }
        
        Err(AppError::BadRequest("No GPodder sync configured".to_string()))
    }

    // Create gPodder device - matches Python device creation
    async fn create_gpodder_device(&self, user_id: i32, device_name: &str, device_type: &str, is_default: bool) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Use INSERT ... ON CONFLICT to handle existing devices
                sqlx::query(r#"
                    INSERT INTO "GpodderDevices" (userid, devicename, devicetype, isdefault) 
                    VALUES ($1, $2, $3, $4)
                    ON CONFLICT (userid, devicename) 
                    DO UPDATE SET devicetype = EXCLUDED.devicetype, isdefault = EXCLUDED.isdefault
                "#)
                    .bind(user_id)
                    .bind(device_name)
                    .bind(device_type)
                    .bind(is_default)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                // Use INSERT ... ON DUPLICATE KEY UPDATE to handle existing devices
                sqlx::query("
                    INSERT INTO GpodderDevices (UserID, DeviceName, DeviceType, IsDefault) 
                    VALUES (?, ?, ?, ?)
                    ON DUPLICATE KEY UPDATE DeviceType = VALUES(DeviceType), IsDefault = VALUES(IsDefault)
                ")
                    .bind(user_id)
                    .bind(device_name)
                    .bind(device_type)
                    .bind(is_default)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }
    
    // Create gPodder device with caption - matches Python create_device with all fields
    pub async fn gpodder_create_device_with_caption(&self, user_id: i32, device_name: &str, device_type: &str, device_caption: Option<&str>, is_default: bool) -> AppResult<i32> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"
                    INSERT INTO "GpodderDevices" (userid, devicename, devicetype, devicecaption, isdefault) 
                    VALUES ($1, $2, $3, $4, $5) 
                    RETURNING deviceid"#)
                    .bind(user_id)
                    .bind(device_name)
                    .bind(device_type)
                    .bind(device_caption)
                    .bind(is_default)
                    .fetch_one(pool)
                    .await?;
                
                Ok(row.try_get("deviceid")?)
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("INSERT INTO GpodderDevices (UserID, DeviceName, DeviceType, DeviceCaption, IsDefault) VALUES (?, ?, ?, ?, ?)")
                    .bind(user_id)
                    .bind(device_name)
                    .bind(device_type)
                    .bind(device_caption)
                    .bind(is_default)
                    .execute(pool)
                    .await?;
                
                Ok(result.last_insert_id() as i32)
            }
        }
    }
    
    // Get default gPodder device - matches Python get_default_device function exactly
    pub async fn gpodder_get_default_device(&self, user_id: i32) -> AppResult<Option<serde_json::Value>> {
        // Get the default device name from Users table
        let default_device_name = match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT defaultgpodderdevice FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                row.and_then(|r| r.try_get::<Option<String>, _>("defaultgpodderdevice").ok().flatten())
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT DefaultGpodderDevice FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                row.and_then(|r| r.try_get::<Option<String>, _>("DefaultGpodderDevice").ok().flatten())
            }
        };
        
        if let Some(device_name) = default_device_name {
            // Get all devices from GPodder API and find the one with matching name
            let devices = self.gpodder_get_user_devices(user_id).await?;
            
            for device in devices {
                if device.get("name").and_then(|v| v.as_str()) == Some(&device_name) {
                    // Mark this device as default and return it
                    let mut default_device = device;
                    default_device["is_default"] = serde_json::Value::Bool(true);
                    return Ok(Some(default_device));
                }
            }
        }
        
        // If no default device found, return None
        Ok(None)
    }


    // Sync with Nextcloud - matches Python refresh_nextcloud_subscription function exactly
    async fn sync_with_nextcloud(&self, user_id: i32, settings: &UserSyncSettings, _force: bool) -> AppResult<bool> {
        let client = reqwest::Client::new();
        let decrypted_password = self.decrypt_password(&settings.token).await?;
        
        // Step 1: Get last sync timestamp for incremental sync
        let since_timestamp = self.get_last_sync_timestamp(user_id).await?;
        
        // Step 2: Get subscriptions from Nextcloud gPodder Sync app with timestamp
        let subscriptions_url = if let Some(since) = since_timestamp {
            format!("{}/index.php/apps/gpoddersync/subscription_changes/{}?since={}", 
                settings.url.trim_end_matches('/'), since.timestamp(), since.timestamp())
        } else {
            format!("{}/index.php/apps/gpoddersync/subscriptions", settings.url.trim_end_matches('/'))
        };
        
        tracing::info!("Getting Nextcloud subscriptions from: {}", subscriptions_url);
        
        let response = client
            .get(&subscriptions_url)
            .basic_auth(&settings.username, Some(&decrypted_password))
            .send()
            .await
            .map_err(|e| AppError::internal(&format!("Failed to sync with Nextcloud: {}", e)))?;
        
        let mut subscriptions_processed = false;
        
        if response.status().is_success() {
            let subscriptions_response: serde_json::Value = response.json().await
                .map_err(|e| AppError::internal(&format!("Failed to parse Nextcloud subscriptions: {}", e)))?;
            
            // Handle both subscription change format and direct subscription list
            let (subscriptions, removals) = if since_timestamp.is_some() {
                // Incremental sync - expect {add: [], remove: [], timestamp: N} format
                let adds = subscriptions_response["add"].as_array()
                    .unwrap_or(&vec![])
                    .iter()
                    .filter_map(|f| f.as_str().map(|s| s.to_string()))
                    .collect::<Vec<String>>();
                
                let removes = subscriptions_response["remove"].as_array()
                    .unwrap_or(&vec![])
                    .iter()
                    .filter_map(|f| f.as_str().map(|s| s.to_string()))
                    .collect::<Vec<String>>();
                
                (adds, removes)
            } else {
                // Full sync - expect direct array of subscription URLs, no removals in this format
                let adds = subscriptions_response.as_array()
                    .unwrap_or(&vec![])
                    .iter()
                    .filter_map(|f| f.as_str().map(|s| s.to_string()))
                    .collect::<Vec<String>>();
                
                (adds, Vec::new())
            };
            
            tracing::info!("Downloaded {} subscriptions and {} removals from Nextcloud", subscriptions.len(), removals.len());
            
            // Process subscriptions and add missing podcasts
            if !subscriptions.is_empty() {
                self.process_gpodder_subscriptions(user_id, &subscriptions).await?;
                subscriptions_processed = true;
            }
            
            // Process subscription removals
            if !removals.is_empty() {
                self.process_gpodder_subscription_removals(user_id, &removals).await?;
                subscriptions_processed = true; // Mark as processed since we did work
            }
        }
        
        // Step 3: Get episode actions from Nextcloud with timestamp
        let episode_actions_url = if let Some(since) = since_timestamp {
            format!("{}/index.php/apps/gpoddersync/episode_action?since={}", 
                settings.url.trim_end_matches('/'), since.timestamp())
        } else {
            format!("{}/index.php/apps/gpoddersync/episode_action", settings.url.trim_end_matches('/'))
        };
        
        tracing::info!("Getting Nextcloud episode actions from: {}", episode_actions_url);
        
        let episode_response = client
            .get(&episode_actions_url)
            .basic_auth(&settings.username, Some(&decrypted_password))
            .send()
            .await;
        
        let mut episode_actions_processed = false;
        
        if let Ok(resp) = episode_response {
            if resp.status().is_success() {
                let episode_actions: serde_json::Value = resp.json().await
                    .map_err(|e| AppError::internal(&format!("Failed to parse Nextcloud episode actions: {}", e)))?;
                
                if let Some(actions_array) = episode_actions["actions"].as_array() {
                    tracing::info!("Downloaded {} episode actions from Nextcloud", actions_array.len());
                    
                    if !actions_array.is_empty() {
                        if let Err(e) = self.apply_remote_episode_actions(user_id, actions_array).await {
                            tracing::warn!("Nextcloud episode actions processing failed but continuing: {}", e);
                        } else {
                            episode_actions_processed = true;
                        }
                    }
                } else if let Some(actions_array) = episode_actions.as_array() {
                    // Some Nextcloud implementations return direct array
                    tracing::info!("Downloaded {} episode actions from Nextcloud (direct)", actions_array.len());
                    
                    if !actions_array.is_empty() {
                        if let Err(e) = self.apply_remote_episode_actions(user_id, actions_array).await {
                            tracing::warn!("Nextcloud episode actions processing failed but continuing: {}", e);
                        } else {
                            episode_actions_processed = true;
                        }
                    }
                }
            } else {
                tracing::warn!("Nextcloud episode actions returned error: {}", resp.status());
            }
        } else {
            tracing::warn!("Failed to get episode actions from Nextcloud");
        }
        
        // Step 4: Upload local subscriptions to Nextcloud (if needed)
        if since_timestamp.is_none() || subscriptions_processed {
            let local_subscriptions = self.get_user_podcast_feeds(user_id).await?;
            if let Err(e) = self.upload_subscriptions_to_nextcloud(&settings.url, &settings.username, &decrypted_password, &local_subscriptions).await {
                tracing::warn!("Failed to upload subscriptions to Nextcloud: {}", e);
            } else {
                tracing::info!("Successfully uploaded {} subscriptions to Nextcloud", local_subscriptions.len());
            }
        } else {
            tracing::info!("Skipping subscription upload to Nextcloud - no changes detected");
        }
        
        // Step 5: Upload local episode actions to Nextcloud
        let local_episode_actions = self.get_user_episode_actions(user_id).await?;
        if !local_episode_actions.is_empty() {
            if let Err(e) = self.upload_episode_actions_to_nextcloud(&settings.url, &settings.username, &decrypted_password, &local_episode_actions).await {
                tracing::warn!("Failed to upload episode actions to Nextcloud: {}", e);
            } else {
                tracing::info!("Successfully uploaded {} episode actions to Nextcloud", local_episode_actions.len());
            }
        } else {
            tracing::info!("No local episode actions to upload to Nextcloud");
        }
        
        // Step 6: Update last sync timestamp for next incremental sync
        self.update_last_sync_timestamp(user_id).await?;
        
        Ok(subscriptions_processed || episode_actions_processed)
    }

    // Decrypt password using Fernet - matches Python encryption
    pub async fn decrypt_password(&self, encrypted_password: &str) -> AppResult<String> {
        use fernet::Fernet;
        
        // Get encryption key from app settings (base64 string)
        let encryption_key = self.get_encryption_key().await?;
        let fernet = Fernet::new(&encryption_key)
            .ok_or_else(|| AppError::internal("Failed to create Fernet cipher"))?;
        
        let decrypted = fernet.decrypt(encrypted_password)
            .map_err(|_e| AppError::internal(&format!("Failed to decrypt password: Fernet decryption error")))?;
        
        String::from_utf8(decrypted)
            .map_err(|e| AppError::internal(&format!("Invalid UTF-8 in decrypted password: {}", e)))
    }
    
    // Get all users with podcasts - for admin refresh
    pub async fn get_all_users_with_podcasts(&self) -> AppResult<Vec<i32>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(r#"SELECT DISTINCT userid FROM "Podcasts" ORDER BY userid"#)
                    .fetch_all(pool)
                    .await?;
                
                let mut users = Vec::new();
                for row in rows {
                    users.push(row.try_get("userid")?);
                }
                Ok(users)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query("SELECT DISTINCT UserID FROM Podcasts ORDER BY UserID")
                    .fetch_all(pool)
                    .await?;
                
                let mut users = Vec::new();
                for row in rows {
                    users.push(row.try_get("UserID")?);
                }
                Ok(users)
            }
        }
    }
    
    // Get all users with gPodder sync enabled - for admin gPodder sync
    pub async fn get_all_users_with_gpodder_sync(&self) -> AppResult<Vec<i32>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(r#"
                    SELECT userid FROM "Users" 
                    WHERE pod_sync_type IS NOT NULL 
                    AND pod_sync_type != 'None' 
                    AND pod_sync_type != 'nextcloud'
                    AND gpodderurl IS NOT NULL 
                    AND gpodderloginname IS NOT NULL 
                    AND gpoddertoken IS NOT NULL
                    ORDER BY userid
                "#)
                    .fetch_all(pool)
                    .await?;
                
                let mut users = Vec::new();
                for row in rows {
                    users.push(row.try_get("userid")?);
                }
                Ok(users)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query("
                    SELECT UserID FROM Users 
                    WHERE Pod_Sync_Type IS NOT NULL 
                    AND Pod_Sync_Type != 'None' 
                    AND Pod_Sync_Type != 'nextcloud'
                    AND GpodderUrl IS NOT NULL 
                    AND GpodderLoginName IS NOT NULL 
                    AND GpodderToken IS NOT NULL
                    ORDER BY UserID
                ")
                    .fetch_all(pool)
                    .await?;
                
                let mut users = Vec::new();
                for row in rows {
                    users.push(row.try_get("UserID")?);
                }
                Ok(users)
            }
        }
    }
    
    // Get stored authentication credentials for a feed URL
    pub async fn get_feed_auth_credentials(&self, feed_url: &str, user_id: i32) -> AppResult<(Option<String>, Option<String>)> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"SELECT username, password FROM "Podcasts" WHERE feedurl = $1 AND userid = $2 LIMIT 1"#)
                    .bind(feed_url)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = result {
                    let username: Option<String> = row.try_get("username").ok().flatten();
                    let password: Option<String> = row.try_get("password").ok().flatten();
                    Ok((username, password))
                } else {
                    Ok((None, None))
                }
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("SELECT Username, Password FROM Podcasts WHERE FeedURL = ? AND UserID = ? LIMIT 1")
                    .bind(feed_url)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = result {
                    let username: Option<String> = row.try_get("Username").ok().flatten();
                    let password: Option<String> = row.try_get("Password").ok().flatten();
                    Ok((username, password))
                } else {
                    Ok((None, None))
                }
            }
        }
    }
    
    // Get podcast values from RSS feed - matches Python get_podcast_values function exactly
    pub async fn get_podcast_values(&self, feed_url: &str, user_id: i32, username: Option<&str>, password: Option<&str>) -> AppResult<std::collections::HashMap<String, String>> {
        use reqwest::header::AUTHORIZATION;
        use feed_rs::parser;
        
        println!("Fetching podcast values from feed URL: {}", feed_url);
        
        // Build HTTP client with proper configuration for container environment
        let client = reqwest::Client::builder()
            .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
            .timeout(std::time::Duration::from_secs(30))
            .build()
            .map_err(|e| {
                println!("Failed to build HTTP client in get_podcast_values: {}", e);
                AppError::Http(e)
            })?;
        
        let mut request = client.get(feed_url);
        
        if let (Some(user), Some(pass)) = (username, password) {
            println!("Using basic authentication for feed: {}", feed_url);
            use base64::Engine;
            let encoded = base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", user, pass));
            request = request.header(AUTHORIZATION, format!("Basic {}", encoded));
        }
        
        // Fetch RSS feed with better error handling
        println!("Sending HTTP request to: {}", feed_url);
        let response = request.send().await
            .map_err(|e| {
                println!("HTTP request failed for {}: {}", feed_url, e);
                AppError::Http(e)
            })?;
        
        println!("Received response with status: {}", response.status());
        if !response.status().is_success() {
            // If we get a 403, the server might be blocking browser User-Agents
            // Try with a podcast client User-Agent
            if response.status() == 403 {
                println!("Got 403 Forbidden, trying with podcast client User-Agent");
                
                let podcast_client = reqwest::Client::builder()
                    .user_agent("PinePods/1.0")
                    .timeout(std::time::Duration::from_secs(30))
                    .build()
                    .map_err(|e| {
                        println!("Failed to build podcast client in get_podcast_values: {}", e);
                        AppError::Http(e)
                    })?;
                
                let mut podcast_request = podcast_client.get(feed_url);
                
                if let (Some(user), Some(pass)) = (username, password) {
                    println!("Using basic authentication for podcast client request: {}", feed_url);
                    use base64::Engine;
                    let encoded = base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", user, pass));
                    podcast_request = podcast_request.header(AUTHORIZATION, format!("Basic {}", encoded));
                }
                
                let podcast_response = podcast_request.send().await
                    .map_err(|e| {
                        println!("Podcast client request failed for {}: {}", feed_url, e);
                        AppError::Http(e)
                    })?;
                
                if podcast_response.status().is_success() {
                    println!("Podcast client request succeeded with status: {}", podcast_response.status());
                    let content = podcast_response.text().await?;
                    
                    // Continue with the same parsing logic
                    return self.parse_feed_content_to_values(content, feed_url, user_id).await;
                }
                
                println!("Podcast client request also failed with status: {}", podcast_response.status());
            }
            
            return Err(AppError::bad_request(&format!("Feed request failed: HTTP {}", response.status())));
        }
        
        let content = response.text().await?;
        
        self.parse_feed_content_to_values(content, feed_url, user_id).await
    }
    
    // Helper function to parse feed content into podcast values (extracted to avoid duplication)
    async fn parse_feed_content_to_values(&self, content: String, feed_url: &str, user_id: i32) -> AppResult<std::collections::HashMap<String, String>> {
        use feed_rs::parser;
        
        // Parse RSS feed using feed-rs
        let feed = parser::parse(content.as_bytes())
            .map_err(|e| AppError::external_error(&format!("Failed to parse RSS feed: {}", e)))?;
        
        // Extract podcast metadata exactly as Python implementation
        let mut podcast_values = std::collections::HashMap::new();
        
        podcast_values.insert("feedurl".to_string(), feed_url.to_string());
        podcast_values.insert("userid".to_string(), user_id.to_string());
        podcast_values.insert("podcastname".to_string(), feed.title.as_ref().map(|t| t.content.clone()).unwrap_or_default());
        podcast_values.insert("description".to_string(), feed.description.as_ref().map(|d| d.content.clone()).unwrap_or_default());
        podcast_values.insert("author".to_string(), feed.authors.first().map(|a| a.name.clone()).unwrap_or_default());
        podcast_values.insert("websiteurl".to_string(), feed.links.first().map(|l| l.href.clone()).unwrap_or_default());
        podcast_values.insert("explicit".to_string(), "False".to_string()); // Default to False
        podcast_values.insert("episodecount".to_string(), feed.entries.len().to_string());
        
        // Extract artwork URL - check feed image and iTunes image
        let artwork_url = feed.logo.as_ref().map(|l| l.uri.clone())
            .or_else(|| feed.icon.as_ref().map(|i| i.uri.clone()))
            .unwrap_or_default();
        podcast_values.insert("artworkurl".to_string(), artwork_url);
        
        // Extract categories - convert to dict format like Python
        let categories = if !feed.categories.is_empty() {
            let cat_dict: std::collections::HashMap<String, String> = feed.categories
                .iter()
                .enumerate()
                .map(|(i, cat)| (i.to_string(), cat.term.clone()))
                .collect();
            serde_json::to_string(&cat_dict).unwrap_or_default()
        } else {
            "{}".to_string()
        };
        podcast_values.insert("categories".to_string(), categories);
        
        // Set default values for fields not in RSS
        podcast_values.insert("podcastindexid".to_string(), "0".to_string());
        podcast_values.insert("episodeupdatecount".to_string(), "0".to_string());
        
        println!("Successfully extracted podcast values: {}", podcast_values.get("podcastname").unwrap_or(&"Unknown".to_string()));
        
        Ok(podcast_values)
    }

    // Parse feed episodes using feed-rs and return JSON data (for frontend consumption)
    pub async fn parse_feed_episodes(&self, feed_url: &str, user_id: i32) -> AppResult<Vec<serde_json::Value>> {
        use feed_rs::parser;
        use reqwest::header::AUTHORIZATION;
        
        // Get stored authentication credentials for this feed
        let (username, password) = self.get_feed_auth_credentials(feed_url, user_id).await?;
        
        // Build HTTP client with proper configuration
        let client = reqwest::Client::builder()
            .user_agent("PinePods/1.0")
            .timeout(std::time::Duration::from_secs(30))
            .build()
            .map_err(|e| AppError::external_error(&format!("Failed to build HTTP client: {}", e)))?;
        
        let mut request = client.get(feed_url);
        
        // Add authentication if available
        if let (Some(user), Some(pass)) = (username.as_deref(), password.as_deref()) {
            println!("Using stored authentication for feed: {}", feed_url);
            use base64::Engine;
            let encoded = base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", user, pass));
            request = request.header(AUTHORIZATION, format!("Basic {}", encoded));
        }
        
        // Fetch RSS feed
        let response = request.send().await?;
        if !response.status().is_success() {
            return Err(AppError::external_error(&format!("Failed to fetch RSS feed: {}", response.status())));
        }
        
        let content = response.text().await?;
        
        // Parse RSS feed using feed-rs
        let feed = parser::parse(content.as_bytes())
            .map_err(|e| AppError::external_error(&format!("Failed to parse RSS feed: {}", e)))?;
        
        // Extract podcast-level artwork as fallback
        let podcast_artwork = feed.logo.as_ref().map(|l| l.uri.clone())
            .or_else(|| feed.icon.as_ref().map(|i| i.uri.clone()))
            .unwrap_or_default();
        
        let mut episodes = Vec::new();
        
        for entry in feed.entries.iter() {
            // Skip episodes without titles
            if entry.title.is_none() {
                continue;
            }
            
            // Extract episode data using same logic as add_episodes
            let mut episode_data = std::collections::HashMap::new();
            
            if let Some(title) = &entry.title {
                episode_data.insert("title".to_string(), title.content.clone());
            }
            
            if let Some(summary) = &entry.summary {
                episode_data.insert("summary".to_string(), summary.content.clone());
            }
            
            if let Some(content) = &entry.content {
                if let Some(body) = &content.body {
                    episode_data.insert("content:encoded".to_string(), body.clone());
                }
            }
            
            // Extract audio URL from media or links
            let mut audio_url = String::new();
            let mut enclosure_length = String::new();
            
            // Check media objects first
            for media in &entry.media {
                for content in &media.content {
                    if let Some(url) = &content.url {
                        if let Some(media_type) = &content.content_type {
                            if media_type.to_string().starts_with("audio/") {
                                audio_url = url.to_string();
                                if let Some(size) = content.size {
                                    enclosure_length = size.to_string();
                                }
                                break;
                            }
                        }
                    }
                }
                if !audio_url.is_empty() { break; }
            }
            
            // Fallback to links if no media found
            if audio_url.is_empty() {
                for link in &entry.links {
                    if let Some(media_type) = &link.media_type {
                        if media_type.starts_with("audio/") {
                            audio_url = link.href.clone();
                            if let Some(length) = &link.length {
                                enclosure_length = length.to_string();
                            }
                            break;
                        }
                    }
                }
            }
            
            // Extract episode artwork (with fallback to podcast artwork)
            let mut episode_artwork = podcast_artwork.clone();
            for media in &entry.media {
                if !media.thumbnails.is_empty() {
                    episode_artwork = media.thumbnails[0].image.uri.clone();
                    break;
                }
            }
            
            // Extract duration from media or iTunes tags
            let mut duration_seconds = 0;
            
            // First try media objects (works for some feeds)
            for media in &entry.media {
                if let Some(duration) = &media.duration {
                    duration_seconds = duration.as_secs() as i32;
                    break;
                }
            }
            
            // If media duration is suspicious (< 3600 seconds = 1 hour), 
            // try to extract iTunes duration from raw RSS to work around feed-rs parsing bugs
            if duration_seconds > 0 && duration_seconds < 3600 {
                if let Some(title) = &entry.title {
                    if let Some(corrected_duration) = Self::extract_itunes_duration_from_raw(&content, &title.content) {
                        if corrected_duration != duration_seconds as u64 {
                            println!("🔧 DURATION CORRECTED: '{}' feed-rs={} -> iTunes={}", 
                                title.content, duration_seconds, corrected_duration);
                            duration_seconds = corrected_duration as i32;
                        }
                    }
                }
            }
            
            // Extract publication date
            let pub_date = if let Some(published) = &entry.published {
                published.to_rfc3339()
            } else {
                chrono::Utc::now().to_rfc3339()
            };
            
            // Create episode JSON object matching frontend Episode structure
            let episode_json = serde_json::json!({
                "title": entry.title.as_ref().map(|t| t.content.clone()),
                "description": entry.summary.as_ref().map(|s| s.content.clone()),
                "pub_date": pub_date,
                "enclosure_url": if audio_url.is_empty() { None } else { Some(audio_url) },
                "enclosure_length": if enclosure_length.is_empty() { None } else { Some(enclosure_length) },
                "artwork": if episode_artwork.is_empty() { None } else { Some(episode_artwork) },
                "content": entry.content.as_ref().and_then(|c| c.body.clone()),
                "duration": duration_seconds,
                "guid": entry.id.clone()
            });
            
            episodes.push(episode_json);
        }
        
        Ok(episodes)
    }

    // Extract iTunes duration from raw RSS for a specific episode title (workaround for feed-rs bugs)
    fn extract_itunes_duration_from_raw(raw_rss: &str, episode_title: &str) -> Option<u64> {
        // HTML encode the episode title to match raw RSS format
        // IMPORTANT: encode & first, otherwise it will double-encode other entities
        let html_encoded_title = episode_title
            .replace("&", "&amp;")
            .replace("'", "&apos;")
            .replace("\"", "&quot;")
            .replace("<", "&lt;")
            .replace(">", "&gt;");
        
        // Try different title formats - RSS titles can vary from feed-rs parsed titles
        let search_patterns = vec![
            format!("<title><![CDATA[{}]]></title>", episode_title),
            format!("<title>{}</title>", episode_title),
            format!("<title><![CDATA[{}]]></title>", html_encoded_title),
            format!("<title>{}</title>", html_encoded_title),
            // Also try searching for the core title without extra formatting
            episode_title.split(" (").next().unwrap_or(episode_title).to_string(),
            html_encoded_title.split(" (").next().unwrap_or(&html_encoded_title).to_string(),
        ];
        
        let mut item_pos = None;
        let mut item_end_pos = None;
        
        // Find any item that contains this title or a partial match
        for pattern in &search_patterns {
            if let Some(pos) = raw_rss.find(pattern) {
                // Find the start of the <item> block containing this title
                let item_start = raw_rss[..pos].rfind("<item>").unwrap_or(0);
                if let Some(end) = raw_rss[pos..].find("</item>") {
                    item_pos = Some(item_start);
                    item_end_pos = Some(pos + end + "</item>".len());
                    break;
                }
            }
        }
        
        // If exact matches failed, try a broader search by looking through all items
        if item_pos.is_none() {
            let mut start = 0;
            while let Some(item_start) = raw_rss[start..].find("<item>") {
                let absolute_start = start + item_start;
                if let Some(item_end) = raw_rss[absolute_start..].find("</item>") {
                    let absolute_end = absolute_start + item_end + "</item>".len();
                    let item_block = &raw_rss[absolute_start..absolute_end];
                    
                    // Check if this item contains any part of our episode title
                    let title_core = episode_title.split(" (").next().unwrap_or(episode_title);
                    if item_block.contains(title_core) {
                        item_pos = Some(absolute_start);
                        item_end_pos = Some(absolute_end);
                        break;
                    }
                    
                    start = absolute_end;
                } else {
                    break;
                }
            }
        }
        
        // Extract duration from the found item block
        if let (Some(start), Some(end)) = (item_pos, item_end_pos) {
            let item_block = &raw_rss[start..end];
            
            // Look for <itunes:duration> in this item block
            if let Some(duration_start) = item_block.find("<itunes:duration>") {
                let duration_content_start = duration_start + "<itunes:duration>".len();
                if let Some(duration_end) = item_block[duration_content_start..].find("</itunes:duration>") {
                    let duration_str = &item_block[duration_content_start..duration_content_start + duration_end];
                    return Self::parse_itunes_duration(duration_str.trim());
                }
            }
        }
        
        None
    }

    // Parse iTunes duration string (HH:MM:SS, MM:SS, or seconds) to total seconds
    fn parse_itunes_duration(duration_str: &str) -> Option<u64> {
        if duration_str.is_empty() {
            return None;
        }
        
        // If it's just a number, treat as seconds
        if let Ok(seconds) = duration_str.parse::<u64>() {
            return Some(seconds);
        }
        
        // Split by colons for time format
        let parts: Vec<&str> = duration_str.split(':').collect();
        
        match parts.len() {
            1 => {
                // Just seconds
                parts[0].parse::<u64>().ok()
            },
            2 => {
                // MM:SS
                let minutes = parts[0].parse::<u64>().ok()?;
                let seconds = parts[1].parse::<u64>().ok()?;
                Some(minutes * 60 + seconds)
            },
            3 => {
                // HH:MM:SS
                let hours = parts[0].parse::<u64>().ok()?;
                let minutes = parts[1].parse::<u64>().ok()?;
                let seconds = parts[2].parse::<u64>().ok()?;
                Some(hours * 3600 + minutes * 60 + seconds)
            },
            _ => None
        }
    }

    // Add podcast from RSS values - wrapper function for custom podcast addition
    pub async fn add_podcast_from_values(&self, podcast_values: &std::collections::HashMap<String, String>, user_id: i32, _feed_cutoff: i32, username: Option<&str>, password: Option<&str>) -> AppResult<(i32, Option<i32>)> {
        // Convert HashMap values to PodcastValues struct
        let podcast_data = crate::handlers::podcasts::PodcastValues {
            user_id,
            pod_title: podcast_values.get("podcastname").unwrap_or(&"".to_string()).clone(),
            pod_artwork: podcast_values.get("artworkurl").unwrap_or(&"".to_string()).clone(),
            pod_author: podcast_values.get("author").unwrap_or(&"".to_string()).clone(),
            categories: std::collections::HashMap::new(), // Parse from string if needed
            pod_description: podcast_values.get("description").unwrap_or(&"".to_string()).clone(),
            pod_episode_count: podcast_values.get("episodecount").unwrap_or(&"0".to_string()).parse().unwrap_or(0),
            pod_feed_url: podcast_values.get("feedurl").unwrap_or(&"".to_string()).clone(),
            pod_website: podcast_values.get("websiteurl").unwrap_or(&"".to_string()).clone(),
            pod_explicit: podcast_values.get("explicit").unwrap_or(&"False".to_string()) == "True",
        };

        let podcast_index_id = podcast_values.get("podcastindexid")
            .unwrap_or(&"0".to_string())
            .parse::<i64>()
            .unwrap_or(0);

        self.add_podcast(&podcast_data, podcast_index_id, username, password).await
    }
    
    // // Get podcast details - matches Python get_podcast_details function exactly
    // pub async fn get_podcast_details(&self, user_id: i32, podcast_id: i32) -> AppResult<Option<serde_json::Value>> {
    //     println!("Getting podcast details for podcast {} and user {}", podcast_id, user_id);
        
    //     let details = match self {
    //         DatabasePool::Postgres(pool) => {
    //             // Try to get podcast for user first, then fall back to UserID=1
    //             let mut row = sqlx::query(r#"SELECT * FROM "Podcasts" WHERE podcastid = $1 AND userid = $2"#)
    //                 .bind(podcast_id)
    //                 .bind(user_id)
    //                 .fetch_optional(pool)
    //                 .await?;
                
    //             if row.is_none() {
    //                 row = sqlx::query(r#"SELECT * FROM "Podcasts" WHERE podcastid = $1 AND userid = 1"#)
    //                     .bind(podcast_id)
    //                     .fetch_optional(pool)
    //                     .await?;
    //             }
                
    //             if let Some(row) = row {
    //                 // Get episode count from YouTubeVideos table if this is a YouTube channel
    //                 let mut episode_count = row.try_get::<i32, _>("episodecount")?;
    //                 let is_youtube_channel = row.try_get::<bool, _>("isyoutubechannel").unwrap_or(false);
                    
    //                 if is_youtube_channel {
    //                     let count_result = sqlx::query(r#"SELECT COUNT(*) as count FROM "YouTubeVideos" WHERE podcastid = $1"#)
    //                         .bind(podcast_id)
    //                         .fetch_one(pool)
    //                         .await?;
    //                     episode_count = count_result.try_get::<i64, _>("count")? as i32;
    //                 }

    //                 Some(serde_json::json!({
    //                     "podcastid": row.try_get::<i32, _>("podcastid")?,
    //                     "podcastindexid": row.try_get::<Option<i32>, _>("podcastindexid")?,
    //                     "podcastname": row.try_get::<String, _>("podcastname")?,
    //                     "artworkurl": row.try_get::<Option<String>, _>("artworkurl").unwrap_or_default().unwrap_or_default(),
    //                     "author": row.try_get::<String, _>("author")?,
    //                     "categories": row.try_get::<Option<String>, _>("categories")?,
    //                     "description": row.try_get::<String, _>("description")?,
    //                     "episodecount": episode_count,
    //                     "feedurl": row.try_get::<String, _>("feedurl")?,
    //                     "websiteurl": row.try_get::<String, _>("websiteurl")?,
    //                     "explicit": row.try_get::<bool, _>("explicit")?,
    //                     "userid": row.try_get::<i32, _>("userid")?,
    //                     "autodownload": row.try_get::<bool, _>("autodownload").unwrap_or(false),
    //                     "startskip": row.try_get::<i32, _>("startskip").unwrap_or(0),
    //                     "endskip": row.try_get::<i32, _>("endskip").unwrap_or(0),
    //                     "username": row.try_get::<Option<String>, _>("username")?,
    //                     "password": row.try_get::<Option<String>, _>("password")?,
    //                     "isyoutubechannel": is_youtube_channel,
    //                     "notificationsenabled": row.try_get::<bool, _>("notificationsenabled").unwrap_or(false),
    //                     "feedcutoffdays": row.try_get::<i32, _>("feedcutoffdays").unwrap_or(0),
    //                 }))
    //             } else {
    //                 None
    //             }
    //         }
    //         DatabasePool::MySQL(pool) => {
    //             // Try to get podcast for user first, then fall back to UserID=1
    //             let mut row = sqlx::query("SELECT * FROM Podcasts WHERE PodcastID = ? AND UserID = ?")
    //                 .bind(podcast_id)
    //                 .bind(user_id)
    //                 .fetch_optional(pool)
    //                 .await?;
                
    //             if row.is_none() {
    //                 row = sqlx::query("SELECT * FROM Podcasts WHERE PodcastID = ? AND UserID = 1")
    //                     .bind(podcast_id)
    //                     .fetch_optional(pool)
    //                     .await?;
    //             }
                
    //             if let Some(row) = row {
    //                 // Get episode count from YouTubeVideos table if this is a YouTube channel
    //                 let mut episode_count = row.try_get::<i32, _>("EpisodeCount")?;
    //                 let is_youtube_channel = row.try_get::<i8, _>("IsYouTubeChannel").unwrap_or(0) != 0;
                    
    //                 if is_youtube_channel {
    //                     let count_result = sqlx::query("SELECT COUNT(*) as count FROM YouTubeVideos WHERE PodcastID = ?")
    //                         .bind(podcast_id)
    //                         .fetch_one(pool)
    //                         .await?;
    //                     episode_count = count_result.try_get::<i64, _>("count")? as i32;
    //                 }

    //                 Some(serde_json::json!({
    //                     "podcastid": row.try_get::<i32, _>("PodcastID")?,
    //                     "podcastindexid": row.try_get::<Option<i32>, _>("PodcastIndexID")?,
    //                     "podcastname": row.try_get::<String, _>("PodcastName")?,
    //                     "artworkurl": row.try_get::<Option<String>, _>("ArtworkURL").unwrap_or_default().unwrap_or_default(),
    //                     "author": row.try_get::<String, _>("Author")?,
    //                     "categories": row.try_get::<Option<String>, _>("Categories")?,
    //                     "description": row.try_get::<String, _>("Description")?,
    //                     "episodecount": episode_count,
    //                     "feedurl": row.try_get::<String, _>("FeedURL")?,
    //                     "websiteurl": row.try_get::<String, _>("WebsiteURL")?,
    //                     "explicit": row.try_get::<i8, _>("Explicit").unwrap_or(0) != 0,
    //                     "userid": row.try_get::<i32, _>("UserID")?,
    //                     "autodownload": row.try_get::<i8, _>("AutoDownload").unwrap_or(0) != 0,
    //                     "startskip": row.try_get::<i32, _>("StartSkip").unwrap_or(0),
    //                     "endskip": row.try_get::<i32, _>("EndSkip").unwrap_or(0),
    //                     "username": row.try_get::<Option<String>, _>("Username")?,
    //                     "password": row.try_get::<Option<String>, _>("Password")?,
    //                     "isyoutubechannel": is_youtube_channel,
    //                     "notificationsenabled": row.try_get::<i8, _>("NotificationsEnabled").unwrap_or(0) != 0,
    //                     "feedcutoffdays": row.try_get::<i32, _>("FeedCutoffDays").unwrap_or(0),
    //                 }))
    //             } else {
    //                 None
    //             }
    //         }
    //     };
        
    //     if let Some(ref result) = details {
    //         println!("Found podcast details for: {}", result["podcast_name"]);
    //     } else {
    //         println!("No podcast found with ID {} for user {}", podcast_id, user_id);
    //     }
        
    //     Ok(details)
    // }
    
    // Get notification settings - matches Python get_notification_settings function exactly
    pub async fn get_notification_settings(&self, user_id: i32) -> AppResult<Vec<serde_json::Value>> {
        println!("Getting notification settings for user {}", user_id);
        
        let settings = match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(r#"
                    SELECT platform, enabled, ntfytopic, ntfyserverurl, ntfyusername, ntfypassword, ntfyaccesstoken, gotifyurl, gotifytoken, httpurl, httptoken, httpmethod
                    FROM "UserNotificationSettings" 
                    WHERE userid = $1
                "#)
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;
                
                let mut settings = Vec::new();
                for row in rows {
                    let setting = serde_json::json!({
                        "platform": row.try_get::<String, _>("platform")?,
                        "enabled": row.try_get::<bool, _>("enabled")?,
                        "ntfy_topic": row.try_get::<Option<String>, _>("ntfytopic")?,
                        "ntfy_server_url": row.try_get::<Option<String>, _>("ntfyserverurl")?,
                        "ntfy_username": row.try_get::<Option<String>, _>("ntfyusername")?,
                        "ntfy_password": row.try_get::<Option<String>, _>("ntfypassword")?,
                        "ntfy_access_token": row.try_get::<Option<String>, _>("ntfyaccesstoken")?,
                        "gotify_url": row.try_get::<Option<String>, _>("gotifyurl")?,
                        "gotify_token": row.try_get::<Option<String>, _>("gotifytoken")?,
                        "http_url": row.try_get::<Option<String>, _>("httpurl")?,
                        "http_token": row.try_get::<Option<String>, _>("httptoken")?,
                        "http_method": row.try_get::<Option<String>, _>("httpmethod")?
                    });
                    settings.push(setting);
                }
                settings
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query("
                    SELECT Platform, Enabled, NtfyTopic, NtfyServerURL, NtfyUsername, NtfyPassword, NtfyAccessToken, GotifyURL, GotifyToken, HttpUrl, HttpToken, HttpMethod
                    FROM UserNotificationSettings 
                    WHERE UserID = ?
                    ORDER BY Platform
                ")
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;
                
                let mut settings = Vec::new();
                for row in rows {
                    let setting = serde_json::json!({
                        "platform": row.try_get::<String, _>("Platform")?,
                        "enabled": row.try_get::<bool, _>("Enabled")?,
                        "ntfy_topic": row.try_get::<Option<String>, _>("NtfyTopic")?,
                        "ntfy_server_url": row.try_get::<Option<String>, _>("NtfyServerURL")?,
                        "ntfy_username": row.try_get::<Option<String>, _>("NtfyUsername")?,
                        "ntfy_password": row.try_get::<Option<String>, _>("NtfyPassword")?,
                        "ntfy_access_token": row.try_get::<Option<String>, _>("NtfyAccessToken")?,
                        "gotify_url": row.try_get::<Option<String>, _>("GotifyURL")?,
                        "gotify_token": row.try_get::<Option<String>, _>("GotifyToken")?,
                        "http_url": row.try_get::<Option<String>, _>("HttpUrl")?,
                        "http_token": row.try_get::<Option<String>, _>("HttpToken")?,
                        "http_method": row.try_get::<Option<String>, _>("HttpMethod")?
                    });
                    settings.push(setting);
                }
                settings
            }
        };
        
        println!("Found {} notification settings for user {}", settings.len(), user_id);
        Ok(settings)
    }
    
    // Update notification settings - matches Python update_notification_settings function exactly
    pub async fn update_notification_settings(&self, user_id: i32, platform: &str, enabled: bool, ntfy_topic: Option<&str>, ntfy_server_url: Option<&str>, ntfy_username: Option<&str>, ntfy_password: Option<&str>, ntfy_access_token: Option<&str>, gotify_url: Option<&str>, gotify_token: Option<&str>, http_url: Option<&str>, http_token: Option<&str>, http_method: Option<&str>) -> AppResult<bool> {
        println!("Updating notification settings for user {} platform {}", user_id, platform);
        
        // Check if settings exist for this user/platform combination and perform update/insert
        let success = match self {
            DatabasePool::Postgres(pool) => {
                let existing = sqlx::query(r#"SELECT COUNT(*) as count FROM "UserNotificationSettings" WHERE userid = $1 AND platform = $2"#)
                    .bind(user_id)
                    .bind(platform)
                    .fetch_one(pool)
                    .await?;
                
                let count: i64 = existing.try_get("count")?;
                
                if count > 0 {
                    // Update existing record
                    let result = sqlx::query(r#"
                        UPDATE "UserNotificationSettings" 
                        SET enabled = $3, ntfytopic = $4, ntfyserverurl = $5, ntfyusername = $6, ntfypassword = $7, ntfyaccesstoken = $8, gotifyurl = $9, gotifytoken = $10, httpurl = $11, httptoken = $12, httpmethod = $13
                        WHERE userid = $1 AND platform = $2
                    "#)
                        .bind(user_id)
                        .bind(platform)
                        .bind(enabled)
                        .bind(ntfy_topic)
                        .bind(ntfy_server_url)
                        .bind(ntfy_username)
                        .bind(ntfy_password)
                        .bind(ntfy_access_token)
                        .bind(gotify_url)
                        .bind(gotify_token)
                        .bind(http_url)
                        .bind(http_token)
                        .bind(http_method)
                        .execute(pool)
                        .await?;
                    result.rows_affected() > 0
                } else {
                    // Insert new record
                    let result = sqlx::query(r#"
                        INSERT INTO "UserNotificationSettings" 
                        (userid, platform, enabled, ntfytopic, ntfyserverurl, ntfyusername, ntfypassword, ntfyaccesstoken, gotifyurl, gotifytoken, httpurl, httptoken, httpmethod)
                        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
                    "#)
                        .bind(user_id)
                        .bind(platform)
                        .bind(enabled)
                        .bind(ntfy_topic)
                        .bind(ntfy_server_url)
                        .bind(ntfy_username)
                        .bind(ntfy_password)
                        .bind(ntfy_access_token)
                        .bind(gotify_url)
                        .bind(gotify_token)
                        .bind(http_url)
                        .bind(http_token)
                        .bind(http_method)
                        .execute(pool)
                        .await?;
                    result.rows_affected() > 0
                }
            }
            DatabasePool::MySQL(pool) => {
                let existing = sqlx::query("SELECT COUNT(*) as count FROM UserNotificationSettings WHERE UserID = ? AND Platform = ?")
                    .bind(user_id)
                    .bind(platform)
                    .fetch_one(pool)
                    .await?;
                
                let count: i64 = existing.try_get("count")?;
                
                if count > 0 {
                    // Update existing record
                    let result = sqlx::query("
                        UPDATE UserNotificationSettings 
                        SET Enabled = ?, NtfyTopic = ?, NtfyServerURL = ?, NtfyUsername = ?, NtfyPassword = ?, NtfyAccessToken = ?, GotifyURL = ?, GotifyToken = ?, HttpUrl = ?, HttpToken = ?, HttpMethod = ?
                        WHERE UserID = ? AND Platform = ?
                    ")
                        .bind(enabled)
                        .bind(ntfy_topic)
                        .bind(ntfy_server_url)
                        .bind(ntfy_username)
                        .bind(ntfy_password)
                        .bind(ntfy_access_token)
                        .bind(gotify_url)
                        .bind(gotify_token)
                        .bind(http_url)
                        .bind(http_token)
                        .bind(http_method)
                        .bind(user_id)
                        .bind(platform)
                        .execute(pool)
                        .await?;
                    result.rows_affected() > 0
                } else {
                    // Insert new record
                    let result = sqlx::query("
                        INSERT INTO UserNotificationSettings 
                        (UserID, Platform, Enabled, NtfyTopic, NtfyServerURL, NtfyUsername, NtfyPassword, NtfyAccessToken, GotifyURL, GotifyToken, HttpUrl, HttpToken, HttpMethod)
                        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                    ")
                        .bind(user_id)
                        .bind(platform)
                        .bind(enabled)
                        .bind(ntfy_topic)
                        .bind(ntfy_server_url)
                        .bind(ntfy_username)
                        .bind(ntfy_password)
                        .bind(ntfy_access_token)
                        .bind(gotify_url)
                        .bind(gotify_token)
                        .bind(http_url)
                        .bind(http_token)
                        .bind(http_method)
                        .execute(pool)
                        .await?;
                    result.rows_affected() > 0
                }
            }
        };
        
        println!("Successfully updated notification settings for user {} platform {}: {}", user_id, platform, success);
        Ok(success)
    }
    
    // Add OIDC provider - matches Python add_oidc_provider function exactly
    pub async fn add_oidc_provider(&self, provider_name: &str, client_id: &str, client_secret: &str, authorization_url: &str, token_url: &str, user_info_url: &str, button_text: &str, scope: &str, button_color: &str, button_text_color: &str, icon_svg: &str, name_claim: &str, email_claim: &str, username_claim: &str, roles_claim: &str, user_role: &str, admin_role: &str, initialized_from_env: bool) -> AppResult<i32> {
        println!("Adding OIDC provider: {}", provider_name);
        
        let provider_id = match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"
                    INSERT INTO "OIDCProviders" (
                        providername, clientid, clientsecret, authorizationurl,
                        tokenurl, userinfourl, buttontext, scope,
                        buttoncolor, buttontextcolor, iconsvg, nameclaim, emailclaim,
                        usernameclaim, rolesclaim, userrole, adminrole, initializedFromEnv
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
                    RETURNING providerid
                "#)
                    .bind(provider_name)
                    .bind(client_id)
                    .bind(client_secret)
                    .bind(authorization_url)
                    .bind(token_url)
                    .bind(user_info_url)
                    .bind(button_text)
                    .bind(scope)
                    .bind(button_color)
                    .bind(button_text_color)
                    .bind(icon_svg)
                    .bind(name_claim)
                    .bind(email_claim)
                    .bind(username_claim)
                    .bind(roles_claim)
                    .bind(user_role)
                    .bind(admin_role)
                    .bind(initialized_from_env)
                    .fetch_one(pool)
                    .await?;
                
                row.try_get("providerid")?
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("
                    INSERT INTO OIDCProviders (
                        ProviderName, ClientID, ClientSecret, AuthorizationURL,
                        TokenURL, UserInfoURL, ButtonText, Scope,
                        ButtonColor, ButtonTextColor, IconSVG, NameClaim, EmailClaim,
                        UsernameClaim, RolesClaim, UserRole, AdminRole, InitializedFromEnv
                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                ")
                    .bind(provider_name)
                    .bind(client_id)
                    .bind(client_secret)
                    .bind(authorization_url)
                    .bind(token_url)
                    .bind(user_info_url)
                    .bind(button_text)
                    .bind(scope)
                    .bind(button_color)
                    .bind(button_text_color)
                    .bind(icon_svg)
                    .bind(name_claim)
                    .bind(email_claim)
                    .bind(username_claim)
                    .bind(roles_claim)
                    .bind(user_role)
                    .bind(admin_role)
                    .bind(initialized_from_env)
                    .execute(pool)
                    .await?;
                
                result.last_insert_id() as i32
            }
        };
        
        println!("Successfully added OIDC provider with ID: {}", provider_id);
        Ok(provider_id)
    }
    
    // List OIDC providers - matches Python list_oidc_providers function exactly
    pub async fn list_oidc_providers(&self) -> AppResult<Vec<serde_json::Value>> {
        println!("Listing all OIDC providers");
        
        let providers = match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(r#"
                    SELECT providerid, providername, clientid, authorizationurl,
                           tokenurl, userinfourl, buttontext, scope, buttoncolor,
                           buttontextcolor, iconsvg, nameclaim, emailclaim, usernameclaim,
                           rolesclaim, userrole, adminrole, enabled, created, modified, initializedfromenv
                    FROM "OIDCProviders" 
                    ORDER BY providername
                "#)
                    .fetch_all(pool)
                    .await?;
                
                let mut providers = Vec::new();
                for row in rows {
                    let provider = serde_json::json!({
                        "provider_id": row.try_get::<i32, _>("providerid")?,
                        "provider_name": row.try_get::<String, _>("providername")?,
                        "client_id": row.try_get::<String, _>("clientid")?,
                        "authorization_url": row.try_get::<String, _>("authorizationurl")?,
                        "token_url": row.try_get::<String, _>("tokenurl")?,
                        "user_info_url": row.try_get::<String, _>("userinfourl")?,
                        "button_text": row.try_get::<String, _>("buttontext")?,
                        "scope": row.try_get::<String, _>("scope")?,
                        "button_color": row.try_get::<String, _>("buttoncolor")?,
                        "button_text_color": row.try_get::<String, _>("buttontextcolor")?,
                        "icon_svg": row.try_get::<Option<String>, _>("iconsvg")?,
                        "name_claim": row.try_get::<Option<String>, _>("nameclaim")?,
                        "email_claim": row.try_get::<Option<String>, _>("emailclaim")?,
                        "username_claim": row.try_get::<Option<String>, _>("usernameclaim")?,
                        "roles_claim": row.try_get::<Option<String>, _>("rolesclaim")?,
                        "user_role": row.try_get::<Option<String>, _>("userrole")?,
                        "admin_role": row.try_get::<Option<String>, _>("adminrole")?,
                        "enabled": row.try_get::<bool, _>("enabled")?,
                        "created": row.try_get::<Option<chrono::NaiveDateTime>, _>("created")?,
                        "modified": row.try_get::<Option<chrono::NaiveDateTime>, _>("modified")?,
                        "initialized_from_env": row.try_get::<bool, _>("initializedfromenv").unwrap_or(false)
                    });
                    providers.push(provider);
                }
                providers
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query("
                    SELECT ProviderID, ProviderName, ClientID, AuthorizationURL,
                           TokenURL, UserInfoURL, ButtonText, Scope, ButtonColor,
                           ButtonTextColor, IconSVG, NameClaim, EmailClaim, UsernameClaim,
                           RolesClaim, UserRole, AdminRole, Enabled, Created, Modified, InitializedFromEnv
                    FROM OIDCProviders 
                    ORDER BY ProviderName
                ")
                    .fetch_all(pool)
                    .await?;
                
                let mut providers = Vec::new();
                for row in rows {
                    let provider = serde_json::json!({
                        "provider_id": row.try_get::<i32, _>("ProviderID")?,
                        "provider_name": row.try_get::<String, _>("ProviderName")?,
                        "client_id": row.try_get::<String, _>("ClientID")?,
                        "authorization_url": row.try_get::<String, _>("AuthorizationURL")?,
                        "token_url": row.try_get::<String, _>("TokenURL")?,
                        "user_info_url": row.try_get::<String, _>("UserInfoURL")?,
                        "button_text": row.try_get::<String, _>("ButtonText")?,
                        "scope": row.try_get::<String, _>("Scope")?,
                        "button_color": row.try_get::<String, _>("ButtonColor")?,
                        "button_text_color": row.try_get::<String, _>("ButtonTextColor")?,
                        "icon_svg": row.try_get::<Option<String>, _>("IconSVG")?,
                        "name_claim": row.try_get::<Option<String>, _>("NameClaim")?,
                        "email_claim": row.try_get::<Option<String>, _>("EmailClaim")?,
                        "username_claim": row.try_get::<Option<String>, _>("UsernameClaim")?,
                        "roles_claim": row.try_get::<Option<String>, _>("RolesClaim")?,
                        "user_role": row.try_get::<Option<String>, _>("UserRole")?,
                        "admin_role": row.try_get::<Option<String>, _>("AdminRole")?,
                        "enabled": row.try_get::<bool, _>("Enabled")?,
                        "created": row.try_get::<Option<chrono::NaiveDateTime>, _>("Created")?,
                        "modified": row.try_get::<Option<chrono::NaiveDateTime>, _>("Modified")?,
                        "initialized_from_env": row.try_get::<bool, _>("InitializedFromEnv").unwrap_or(false)
                    });
                    providers.push(provider);
                }
                providers
            }
        };
        
        println!("Found {} OIDC providers", providers.len());
        Ok(providers)
    }
    
    // Remove OIDC provider - matches Python remove_oidc_provider function exactly
    pub async fn remove_oidc_provider(&self, provider_id: i32) -> AppResult<bool> {
        println!("Removing OIDC provider with ID: {}", provider_id);
        
        let rows_affected = match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"DELETE FROM "OIDCProviders" WHERE ProviderID = $1"#)
                    .bind(provider_id)
                    .execute(pool)
                    .await?;
                
                result.rows_affected()
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("DELETE FROM OIDCProviders WHERE ProviderID = ?")
                    .bind(provider_id)
                    .execute(pool)
                    .await?;
                
                result.rows_affected()
            }
        };
        
        let success = rows_affected > 0;
        if success {
            println!("Successfully removed OIDC provider with ID: {}", provider_id);
        } else {
            println!("No OIDC provider found with ID: {}", provider_id);
        }
        Ok(success)
    }

    // Initialize OIDC provider from environment variables on container startup
    pub async fn init_oidc_from_env(&self, oidc_config: &OIDCConfig) -> AppResult<()> {
        if !oidc_config.is_configured() {
            println!("OIDC environment variables not configured, skipping initialization");
            return Ok(());
        }

        let provider_name = oidc_config.provider_name.as_ref().unwrap();
        let client_id = oidc_config.client_id.as_ref().unwrap();
        
        // Check if provider already exists
        let existing = self.get_oidc_provider_by_client_id(client_id).await?;
        if existing.is_some() {
            println!("OIDC provider with client_id '{}' already exists, skipping initialization", client_id);
            return Ok(());
        }

        println!("Initializing OIDC provider '{}' from environment variables", provider_name);

        // Create the provider with all the configuration
        let provider_id = self.add_oidc_provider(
            provider_name,
            client_id,
            oidc_config.client_secret.as_ref().unwrap(),
            oidc_config.authorization_url.as_ref().unwrap(),
            oidc_config.token_url.as_ref().unwrap(),
            oidc_config.user_info_url.as_ref().unwrap(),
            oidc_config.button_text.as_ref().unwrap(),
            oidc_config.scope.as_deref().unwrap_or("openid email profile"),
            oidc_config.button_color.as_deref().unwrap_or("#000000"),
            oidc_config.button_text_color.as_deref().unwrap_or("#FFFFFF"),
            oidc_config.icon_svg.as_deref().unwrap_or(""),
            oidc_config.name_claim.as_deref().unwrap_or("name"),
            oidc_config.email_claim.as_deref().unwrap_or("email"),
            oidc_config.username_claim.as_deref().unwrap_or("preferred_username"),
            oidc_config.roles_claim.as_deref().unwrap_or("roles"),
            oidc_config.user_role.as_deref().unwrap_or("user"),
            oidc_config.admin_role.as_deref().unwrap_or("admin"),
            true, // initialized_from_env = true
        ).await?;

        println!("Successfully initialized OIDC provider '{}' with ID: {}", provider_name, provider_id);
        Ok(())
    }

    // Check if OIDC provider was initialized from environment variables
    pub async fn is_oidc_provider_env_initialized(&self, provider_id: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"
                    SELECT initializedfromenv FROM "OIDCProviders" WHERE providerid = $1
                "#)
                    .bind(provider_id)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(result.map(|row| row.try_get::<bool, _>("initializedfromenv").unwrap_or(false)).unwrap_or(false))
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query(r#"
                    SELECT InitializedFromEnv FROM OIDCProviders WHERE ProviderID = ?
                "#)
                    .bind(provider_id)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(result.map(|row| row.try_get::<bool, _>("InitializedFromEnv").unwrap_or(false)).unwrap_or(false))
            }
        }
    }

    // Update OIDC provider - updates an existing provider with new values
    // If client_secret is None, the existing secret will be preserved
    pub async fn update_oidc_provider(&self, provider_id: i32, provider_name: &str, client_id: &str, client_secret: Option<&str>, authorization_url: &str, token_url: &str, user_info_url: &str, button_text: &str, scope: &str, button_color: &str, button_text_color: &str, icon_svg: &str, name_claim: &str, email_claim: &str, username_claim: &str, roles_claim: &str, user_role: &str, admin_role: &str) -> AppResult<bool> {
        println!("Updating OIDC provider with ID: {}", provider_id);

        let rows_affected = match self {
            DatabasePool::Postgres(pool) => {
                // Build query dynamically based on whether client_secret is provided
                if let Some(secret) = client_secret {
                    let result = sqlx::query(r#"
                        UPDATE "OIDCProviders" SET
                            providername = $2, clientid = $3, clientsecret = $4,
                            authorizationurl = $5, tokenurl = $6, userinfourl = $7,
                            buttontext = $8, scope = $9, buttoncolor = $10,
                            buttontextcolor = $11, iconsvg = $12, nameclaim = $13,
                            emailclaim = $14, usernameclaim = $15, rolesclaim = $16,
                            userrole = $17, adminrole = $18, modified = CURRENT_TIMESTAMP
                        WHERE providerid = $1
                    "#)
                        .bind(provider_id)
                        .bind(provider_name)
                        .bind(client_id)
                        .bind(secret)
                        .bind(authorization_url)
                        .bind(token_url)
                        .bind(user_info_url)
                        .bind(button_text)
                        .bind(scope)
                        .bind(button_color)
                        .bind(button_text_color)
                        .bind(icon_svg)
                        .bind(name_claim)
                        .bind(email_claim)
                        .bind(username_claim)
                        .bind(roles_claim)
                        .bind(user_role)
                        .bind(admin_role)
                        .execute(pool)
                        .await?;
                    result.rows_affected()
                } else {
                    // Don't update client_secret if not provided
                    let result = sqlx::query(r#"
                        UPDATE "OIDCProviders" SET
                            providername = $2, clientid = $3,
                            authorizationurl = $4, tokenurl = $5, userinfourl = $6,
                            buttontext = $7, scope = $8, buttoncolor = $9,
                            buttontextcolor = $10, iconsvg = $11, nameclaim = $12,
                            emailclaim = $13, usernameclaim = $14, rolesclaim = $15,
                            userrole = $16, adminrole = $17, modified = CURRENT_TIMESTAMP
                        WHERE providerid = $1
                    "#)
                        .bind(provider_id)
                        .bind(provider_name)
                        .bind(client_id)
                        .bind(authorization_url)
                        .bind(token_url)
                        .bind(user_info_url)
                        .bind(button_text)
                        .bind(scope)
                        .bind(button_color)
                        .bind(button_text_color)
                        .bind(icon_svg)
                        .bind(name_claim)
                        .bind(email_claim)
                        .bind(username_claim)
                        .bind(roles_claim)
                        .bind(user_role)
                        .bind(admin_role)
                        .execute(pool)
                        .await?;
                    result.rows_affected()
                }
            }
            DatabasePool::MySQL(pool) => {
                if let Some(secret) = client_secret {
                    let result = sqlx::query("
                        UPDATE OIDCProviders SET
                            ProviderName = ?, ClientID = ?, ClientSecret = ?,
                            AuthorizationURL = ?, TokenURL = ?, UserInfoURL = ?,
                            ButtonText = ?, Scope = ?, ButtonColor = ?,
                            ButtonTextColor = ?, IconSVG = ?, NameClaim = ?,
                            EmailClaim = ?, UsernameClaim = ?, RolesClaim = ?,
                            UserRole = ?, AdminRole = ?, Modified = CURRENT_TIMESTAMP
                        WHERE ProviderID = ?
                    ")
                        .bind(provider_name)
                        .bind(client_id)
                        .bind(secret)
                        .bind(authorization_url)
                        .bind(token_url)
                        .bind(user_info_url)
                        .bind(button_text)
                        .bind(scope)
                        .bind(button_color)
                        .bind(button_text_color)
                        .bind(icon_svg)
                        .bind(name_claim)
                        .bind(email_claim)
                        .bind(username_claim)
                        .bind(roles_claim)
                        .bind(user_role)
                        .bind(admin_role)
                        .bind(provider_id)
                        .execute(pool)
                        .await?;
                    result.rows_affected()
                } else {
                    // Don't update client_secret if not provided
                    let result = sqlx::query("
                        UPDATE OIDCProviders SET
                            ProviderName = ?, ClientID = ?,
                            AuthorizationURL = ?, TokenURL = ?, UserInfoURL = ?,
                            ButtonText = ?, Scope = ?, ButtonColor = ?,
                            ButtonTextColor = ?, IconSVG = ?, NameClaim = ?,
                            EmailClaim = ?, UsernameClaim = ?, RolesClaim = ?,
                            UserRole = ?, AdminRole = ?, Modified = CURRENT_TIMESTAMP
                        WHERE ProviderID = ?
                    ")
                        .bind(provider_name)
                        .bind(client_id)
                        .bind(authorization_url)
                        .bind(token_url)
                        .bind(user_info_url)
                        .bind(button_text)
                        .bind(scope)
                        .bind(button_color)
                        .bind(button_text_color)
                        .bind(icon_svg)
                        .bind(name_claim)
                        .bind(email_claim)
                        .bind(username_claim)
                        .bind(roles_claim)
                        .bind(user_role)
                        .bind(admin_role)
                        .bind(provider_id)
                        .execute(pool)
                        .await?;
                    result.rows_affected()
                }
            }
        };
        
        let success = rows_affected > 0;
        if success {
            println!("Successfully updated OIDC provider with ID: {}", provider_id);
        } else {
            println!("No OIDC provider found with ID: {}", provider_id);
        }
        Ok(success)
    }
    
    // Get user start page - matches Python get_user_startpage function exactly
    pub async fn get_user_startpage(&self, user_id: i32) -> AppResult<String> {
        println!("Getting start page for user {}", user_id);
        
        let startpage = match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT startpage FROM "UserSettings" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    row.try_get::<Option<String>, _>("startpage")?.unwrap_or_else(|| "home".to_string())
                } else {
                    "home".to_string()
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT StartPage FROM UserSettings WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    row.try_get::<Option<String>, _>("StartPage")?.unwrap_or_else(|| "home".to_string())
                } else {
                    "home".to_string()
                }
            }
        };
        
        Ok(startpage)
    }
    
    // Set user start page - matches Python set_user_startpage function exactly
    pub async fn set_user_startpage(&self, user_id: i32, startpage: &str) -> AppResult<bool> {
        println!("Setting start page for user {} to {}", user_id, startpage);
        
        // Check if user settings exist and perform update/insert
        let success = match self {
            DatabasePool::Postgres(pool) => {
                let existing = sqlx::query(r#"SELECT COUNT(*) as count FROM "UserSettings" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                
                let count: i64 = existing.try_get("count")?;
                
                if count > 0 {
                    // Update existing record
                    let result = sqlx::query(r#"UPDATE "UserSettings" SET startpage = $2 WHERE userid = $1"#)
                        .bind(user_id)
                        .bind(startpage)
                        .execute(pool)
                        .await?;
                    result.rows_affected() > 0
                } else {
                    // Insert new record
                    let result = sqlx::query(r#"INSERT INTO "UserSettings" (userid, startpage) VALUES ($1, $2)"#)
                        .bind(user_id)
                        .bind(startpage)
                        .execute(pool)
                        .await?;
                    result.rows_affected() > 0
                }
            }
            DatabasePool::MySQL(pool) => {
                let existing = sqlx::query("SELECT COUNT(*) as count FROM UserSettings WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                
                let count: i64 = existing.try_get("count")?;
                
                if count > 0 {
                    // Update existing record
                    let result = sqlx::query("UPDATE UserSettings SET StartPage = ? WHERE UserID = ?")
                        .bind(startpage)
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                    result.rows_affected() > 0
                } else {
                    // Insert new record
                    let result = sqlx::query("INSERT INTO UserSettings (UserID, StartPage) VALUES (?, ?)")
                        .bind(user_id)
                        .bind(startpage)
                        .execute(pool)
                        .await?;
                    result.rows_affected() > 0
                }
            }
        };
        
        println!("Successfully set start page for user {} to {}: {}", user_id, startpage, success);
        Ok(success)
    }

    // Update startpage wrapper function for compatibility
    pub async fn update_startpage(&self, user_id: i32, startpage: &str) -> AppResult<bool> {
        self.set_user_startpage(user_id, startpage).await
    }

    // Get startpage wrapper function for compatibility 
    pub async fn get_startpage(&self, user_id: i32) -> AppResult<String> {
        self.get_user_startpage(user_id).await
    }

    // Get user auto complete seconds setting
    pub async fn get_user_auto_complete_seconds(&self, user_id: i32) -> AppResult<i32> {
        println!("Getting auto complete seconds for user {}", user_id);
        
        let auto_complete_seconds = match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT autocompleteseconds FROM "UserSettings" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    row.try_get::<Option<i32>, _>("autocompleteseconds")?.unwrap_or(0)
                } else {
                    0
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT AutoCompleteSeconds FROM UserSettings WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    row.try_get::<Option<i32>, _>("AutoCompleteSeconds")?.unwrap_or(0)
                } else {
                    0
                }
            }
        };
        
        Ok(auto_complete_seconds)
    }
    
    // Set user auto complete seconds setting
    pub async fn set_user_auto_complete_seconds(&self, user_id: i32, seconds: i32) -> AppResult<bool> {
        println!("Setting auto complete seconds for user {} to {}", user_id, seconds);
        
        // Check if user settings exist and perform update/insert
        let success = match self {
            DatabasePool::Postgres(pool) => {
                let existing = sqlx::query(r#"SELECT COUNT(*) as count FROM "UserSettings" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                
                let count: i64 = existing.try_get("count")?;
                
                if count > 0 {
                    // Update existing record
                    let result = sqlx::query(r#"UPDATE "UserSettings" SET autocompleteseconds = $2 WHERE userid = $1"#)
                        .bind(user_id)
                        .bind(seconds)
                        .execute(pool)
                        .await?;
                    result.rows_affected() > 0
                } else {
                    // Insert new record with default theme
                    let result = sqlx::query(r#"INSERT INTO "UserSettings" (userid, autocompleteseconds, theme) VALUES ($1, $2, 'Nordic')"#)
                        .bind(user_id)
                        .bind(seconds)
                        .execute(pool)
                        .await?;
                    result.rows_affected() > 0
                }
            }
            DatabasePool::MySQL(pool) => {
                let existing = sqlx::query("SELECT COUNT(*) as count FROM UserSettings WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                
                let count: i64 = existing.try_get("count")?;
                
                if count > 0 {
                    // Update existing record
                    let result = sqlx::query("UPDATE UserSettings SET AutoCompleteSeconds = ? WHERE UserID = ?")
                        .bind(seconds)
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                    result.rows_affected() > 0
                } else {
                    // Insert new record with default theme
                    let result = sqlx::query("INSERT INTO UserSettings (UserID, AutoCompleteSeconds, Theme) VALUES (?, ?, 'Nordic')")
                        .bind(user_id)
                        .bind(seconds)
                        .execute(pool)
                        .await?;
                    result.rows_affected() > 0
                }
            }
        };
        
        println!("Successfully set auto complete seconds for user {} to {}: {}", user_id, seconds, success);
        Ok(success)
    }

    // Get episode duration in seconds
    pub async fn get_episode_duration(&self, episode_id: i32) -> AppResult<i32> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT episodeduration FROM "Episodes" WHERE episodeid = $1"#)
                    .bind(episode_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(row.try_get::<Option<i32>, _>("episodeduration")?.unwrap_or(0))
                } else {
                    Ok(0)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT EpisodeDuration FROM Episodes WHERE EpisodeID = ?")
                    .bind(episode_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(row.try_get::<Option<i32>, _>("EpisodeDuration")?.unwrap_or(0))
                } else {
                    Ok(0)
                }
            }
        }
    }

    // Get YouTube episode duration in seconds
    pub async fn get_youtube_episode_duration(&self, episode_id: i32) -> AppResult<i32> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT duration FROM "YouTubeVideos" WHERE videoid = $1"#)
                    .bind(episode_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(row.try_get::<Option<i32>, _>("duration")?.unwrap_or(0))
                } else {
                    Ok(0)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT Duration FROM YouTubeVideos WHERE VideoID = ?")
                    .bind(episode_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(row.try_get::<Option<i32>, _>("Duration")?.unwrap_or(0))
                } else {
                    Ok(0)
                }
            }
        }
    }

    // Get users who have auto-complete enabled (auto_complete_seconds > 0)
    pub async fn get_users_with_auto_complete_enabled(&self) -> AppResult<Vec<UserAutoComplete>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(r#"
                    SELECT us.userid, us.autocompleteseconds 
                    FROM "UserSettings" us 
                    WHERE us.autocompleteseconds > 0
                "#)
                .fetch_all(pool)
                .await?;

                let mut users = Vec::new();
                for row in rows {
                    users.push(UserAutoComplete {
                        user_id: row.try_get("userid")?,
                        auto_complete_seconds: row.try_get("autocompleteseconds")?,
                    });
                }
                Ok(users)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query("
                    SELECT UserID, AutoCompleteSeconds 
                    FROM UserSettings 
                    WHERE AutoCompleteSeconds > 0
                ")
                .fetch_all(pool)
                .await?;

                let mut users = Vec::new();
                for row in rows {
                    users.push(UserAutoComplete {
                        user_id: row.try_get("UserID")?,
                        auto_complete_seconds: row.try_get("AutoCompleteSeconds")?,
                    });
                }
                Ok(users)
            }
        }
    }

    // Auto-complete episodes for a user based on their setting
    pub async fn auto_complete_user_episodes(&self, user_id: i32, auto_complete_seconds: i32) -> AppResult<i32> {
        if auto_complete_seconds <= 0 {
            return Ok(0);
        }

        let mut completed_count = 0;

        match self {
            DatabasePool::Postgres(pool) => {
                // Handle regular episodes
                let episode_rows = sqlx::query(r#"
                    SELECT e.episodeid, e.episodeduration, COALESCE(h.listenduration, 0) as listenduration
                    FROM "Episodes" e
                    LEFT JOIN "UserEpisodeHistory" h ON e.episodeid = h.episodeid AND h.userid = $1
                    WHERE e.completed = false 
                      AND e.episodeduration > 0 
                      AND h.listenduration > 0
                      AND (e.episodeduration - h.listenduration) <= $2
                "#)
                .bind(user_id)
                .bind(auto_complete_seconds)
                .fetch_all(pool)
                .await?;

                for row in episode_rows {
                    let episode_id: i32 = row.try_get("episodeid")?;
                    let _ = self.mark_episode_completed(episode_id, user_id, false).await;
                    completed_count += 1;
                }

                // Handle YouTube episodes
                let youtube_rows = sqlx::query(r#"
                    SELECT v.videoid, v.duration, COALESCE(h.listenduration, 0) as listenduration
                    FROM "YouTubeVideos" v
                    LEFT JOIN "UserVideoHistory" h ON v.videoid = h.videoid AND h.userid = $1
                    WHERE v.completed = false 
                      AND v.duration > 0 
                      AND h.listenduration > 0
                      AND (v.duration - h.listenduration) <= $2
                "#)
                .bind(user_id)
                .bind(auto_complete_seconds)
                .fetch_all(pool)
                .await?;

                for row in youtube_rows {
                    let video_id: i32 = row.try_get("videoid")?;
                    let _ = self.mark_episode_completed(video_id, user_id, true).await;
                    completed_count += 1;
                }
            }
            DatabasePool::MySQL(pool) => {
                // Handle regular episodes
                let episode_rows = sqlx::query("
                    SELECT e.EpisodeID, e.EpisodeDuration, COALESCE(h.ListenDuration, 0) as ListenDuration
                    FROM Episodes e
                    LEFT JOIN UserEpisodeHistory h ON e.EpisodeID = h.EpisodeID AND h.UserID = ?
                    WHERE e.Completed = 0 
                      AND e.EpisodeDuration > 0 
                      AND h.ListenDuration > 0
                      AND (e.EpisodeDuration - h.ListenDuration) <= ?
                ")
                .bind(user_id)
                .bind(auto_complete_seconds)
                .fetch_all(pool)
                .await?;

                for row in episode_rows {
                    let episode_id: i32 = row.try_get("EpisodeID")?;
                    let _ = self.mark_episode_completed(episode_id, user_id, false).await;
                    completed_count += 1;
                }

                // Handle YouTube episodes
                let youtube_rows = sqlx::query("
                    SELECT v.VideoID, v.Duration, COALESCE(h.ListenDuration, 0) as ListenDuration
                    FROM YouTubeVideos v
                    LEFT JOIN UserVideoHistory h ON v.VideoID = h.VideoID AND h.UserID = ?
                    WHERE v.Completed = 0 
                      AND v.Duration > 0 
                      AND h.ListenDuration > 0
                      AND (v.Duration - h.ListenDuration) <= ?
                ")
                .bind(user_id)
                .bind(auto_complete_seconds)
                .fetch_all(pool)
                .await?;

                for row in youtube_rows {
                    let video_id: i32 = row.try_get("VideoID")?;
                    let _ = self.mark_episode_completed(video_id, user_id, true).await;
                    completed_count += 1;
                }
            }
        }

        Ok(completed_count)
    }
    
    // Subscribe to person - matches Python subscribe_to_person function exactly
    pub async fn subscribe_to_person(&self, user_id: i32, person_id: i32, person_name: &str, person_img: &str, podcast_id: i32) -> AppResult<i32> {
        println!("Subscribing user {} to person {}: {}", user_id, person_id, person_name);

        // Check if person already exists for this user and handle accordingly
        let result = match self {
            DatabasePool::Postgres(pool) => {
                // When peopledbid is 0 or not set, use name for lookup to avoid collisions
                let existing = if person_id == 0 {
                    sqlx::query(r#"SELECT personid FROM "People" WHERE userid = $1 AND LOWER(name) = LOWER($2)"#)
                        .bind(user_id)
                        .bind(person_name)
                        .fetch_optional(pool)
                        .await?
                } else {
                    sqlx::query(r#"SELECT personid FROM "People" WHERE userid = $1 AND peopledbid = $2"#)
                        .bind(user_id)
                        .bind(person_id)
                        .fetch_optional(pool)
                        .await?
                };
                
                if let Some(row) = existing {
                    let person_db_id: i32 = row.try_get("personid")?;
            
                    // Update associated podcasts to include this podcast_id
                    let podcast_row = sqlx::query(r#"SELECT associatedpodcasts FROM "People" WHERE personid = $1"#)
                        .bind(person_db_id)
                        .fetch_one(pool)
                        .await?;
                    let current_podcasts = podcast_row.try_get::<Option<String>, _>("associatedpodcasts")?;
                    
                    let updated_podcasts = if let Some(podcasts) = current_podcasts {
                        if !podcasts.contains(&podcast_id.to_string()) {
                            format!("{},{}", podcasts, podcast_id)
                        } else {
                            podcasts
                        }
                    } else {
                        podcast_id.to_string()
                    };
                    
                    // Update the record
                    sqlx::query(r#"UPDATE "People" SET associatedpodcasts = $2 WHERE personid = $1"#)
                        .bind(person_db_id)
                        .bind(&updated_podcasts)
                        .execute(pool)
                        .await?;
                    
                    println!("Updated existing person subscription with ID: {}", person_db_id);
                    person_db_id
                } else {
                    // Insert new person subscription
                    let row = sqlx::query(r#"
                        INSERT INTO "People" (userid, name, personimg, peopledbid, associatedpodcasts)
                        VALUES ($1, $2, $3, $4, $5)
                        RETURNING personid
                    "#)
                        .bind(user_id)
                        .bind(person_name)
                        .bind(person_img)
                        .bind(person_id)
                        .bind(podcast_id.to_string())
                        .fetch_one(pool)
                        .await?;
                    
                    let person_db_id: i32 = row.try_get("personid")?;
                    println!("Successfully subscribed to person with ID: {}", person_db_id);
                    person_db_id
                }
            }
            DatabasePool::MySQL(pool) => {
                // When peopledbid is 0 or not set, use name for lookup to avoid collisions
                let existing = if person_id == 0 {
                    sqlx::query("SELECT PersonID FROM People WHERE UserID = ? AND LOWER(Name) = LOWER(?)")
                        .bind(user_id)
                        .bind(person_name)
                        .fetch_optional(pool)
                        .await?
                } else {
                    sqlx::query("SELECT PersonID FROM People WHERE UserID = ? AND PeopleDBID = ?")
                        .bind(user_id)
                        .bind(person_id)
                        .fetch_optional(pool)
                        .await?
                };
                
                if let Some(row) = existing {
                    let person_db_id: i32 = row.try_get("PersonID")?;
            
                    // Update associated podcasts to include this podcast_id
                    let podcast_row = sqlx::query("SELECT AssociatedPodcasts FROM People WHERE PersonID = ?")
                        .bind(person_db_id)
                        .fetch_one(pool)
                        .await?;
                    let current_podcasts = podcast_row.try_get::<Option<String>, _>("AssociatedPodcasts")?;
                    
                    let updated_podcasts = if let Some(podcasts) = current_podcasts {
                        if !podcasts.contains(&podcast_id.to_string()) {
                            format!("{},{}", podcasts, podcast_id)
                        } else {
                            podcasts
                        }
                    } else {
                        podcast_id.to_string()
                    };
                    
                    // Update the record
                    sqlx::query("UPDATE People SET AssociatedPodcasts = ? WHERE PersonID = ?")
                        .bind(&updated_podcasts)
                        .bind(person_db_id)
                        .execute(pool)
                        .await?;
                    
                    println!("Updated existing person subscription with ID: {}", person_db_id);
                    person_db_id
                } else {
                    // Insert new person subscription
                    let result = sqlx::query("
                        INSERT INTO People (UserID, Name, PersonImg, PeopleDBID, AssociatedPodcasts)
                        VALUES (?, ?, ?, ?, ?)
                    ")
                        .bind(user_id)
                        .bind(person_name)
                        .bind(person_img)
                        .bind(person_id)
                        .bind(podcast_id.to_string())
                        .execute(pool)
                        .await?;
                    
                    let person_db_id = result.last_insert_id() as i32;
                    println!("Successfully subscribed to person with ID: {}", person_db_id);
                    person_db_id
                }
            }
        };
        
        Ok(result)
    }
    
    // Unsubscribe from person - matches Python unsubscribe_from_person function exactly
    pub async fn unsubscribe_from_person(&self, user_id: i32, person_id: i32, person_name: &str) -> AppResult<bool> {
        println!("Unsubscribing user {} from person {}: {}", user_id, person_id, person_name);

        // Find and delete the person record
        let rows_affected = match self {
            DatabasePool::Postgres(pool) => {
                // When peopledbid is 0 or not set, use name for lookup to avoid collisions
                if person_id == 0 {
                    sqlx::query(r#"DELETE FROM "People" WHERE userid = $1 AND LOWER(name) = LOWER($2)"#)
                        .bind(user_id)
                        .bind(person_name)
                        .execute(pool)
                        .await?
                        .rows_affected()
                } else {
                    sqlx::query(r#"DELETE FROM "People" WHERE userid = $1 AND peopledbid = $2"#)
                        .bind(user_id)
                        .bind(person_id)
                        .execute(pool)
                        .await?
                        .rows_affected()
                }
            }
            DatabasePool::MySQL(pool) => {
                // When peopledbid is 0 or not set, use name for lookup to avoid collisions
                if person_id == 0 {
                    sqlx::query("DELETE FROM People WHERE UserID = ? AND LOWER(Name) = LOWER(?)")
                        .bind(user_id)
                        .bind(person_name)
                        .execute(pool)
                        .await?
                        .rows_affected()
                } else {
                    sqlx::query("DELETE FROM People WHERE UserID = ? AND PeopleDBID = ?")
                        .bind(user_id)
                        .bind(person_id)
                        .execute(pool)
                        .await?
                        .rows_affected()
                }
            }
        };
        
        if rows_affected > 0 {
            // Check if this was the last subscriber to this person
            let count: i64 = match self {
                DatabasePool::Postgres(pool) => {
                    let row = sqlx::query(r#"SELECT COUNT(*) as count FROM "People" WHERE peopledbid = $1"#)
                        .bind(person_id)
                        .fetch_one(pool)
                        .await?;
                    row.try_get("count")?
                }
                DatabasePool::MySQL(pool) => {
                    let row = sqlx::query("SELECT COUNT(*) as count FROM People WHERE PeopleDBID = ?")
                        .bind(person_id)
                        .fetch_one(pool)
                        .await?;
                    row.try_get("count")?
                }
            };
            
            // If no more subscribers, clean up episodes
            if count == 0 {
                match self {
                    DatabasePool::Postgres(pool) => {
                        let _ = sqlx::query(r#"DELETE FROM "PeopleEpisodes" WHERE personid = $1"#)
                            .bind(person_id)
                            .execute(pool)
                            .await;
                    }
                    DatabasePool::MySQL(pool) => {
                        let _ = sqlx::query("DELETE FROM PeopleEpisodes WHERE PersonID = ?")
                            .bind(person_id)
                            .execute(pool)
                            .await;
                    }
                }
            }
            
            println!("Successfully unsubscribed from person {}", person_id);
            Ok(true)
        } else {
            println!("Person subscription not found for user {} and person {}", user_id, person_id);
            Ok(false)
        }
    }
    
    // Get person subscriptions - matches Python get_person_subscriptions function exactly
    pub async fn get_person_subscriptions(&self, user_id: i32) -> AppResult<Vec<serde_json::Value>> {
        println!("Getting person subscriptions for user {}", user_id);
        
        let mut subscriptions = Vec::new();
        
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(r#"
                    SELECT personid, userid, name, personimg, peopledbid, associatedpodcasts
                    FROM "People" 
                    WHERE userid = $1
                    ORDER BY name
                "#)
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;
                
                for row in rows {
                    let person_id = row.try_get::<i32, _>("personid")?;
                    let associated_podcasts_str = row.try_get::<Option<String>, _>("associatedpodcasts")?;
                    
                    // Count associated podcasts by splitting the comma-separated string and filtering out empty/0 values
                    let podcast_count = if let Some(podcasts_str) = &associated_podcasts_str {
                        if podcasts_str.is_empty() {
                            0
                        } else {
                            podcasts_str.split(',')
                                .filter(|s| !s.trim().is_empty() && s.trim() != "0")
                                .count()
                        }
                    } else {
                        0
                    };

                    // Count episodes for this person from PeopleEpisodes table
                    let episode_count: i64 = sqlx::query_scalar(r#"
                        SELECT COUNT(*) 
                        FROM "PeopleEpisodes" pe
                        INNER JOIN "Podcasts" p ON pe.podcastid = p.podcastid
                        WHERE pe.personid = $1 AND p.userid = $2
                    "#)
                    .bind(person_id)
                    .bind(user_id)
                    .fetch_one(pool)
                    .await
                    .unwrap_or(0);

                    let subscription = serde_json::json!({
                        "personid": person_id,
                        "userid": row.try_get::<i32, _>("userid")?,
                        "name": row.try_get::<String, _>("name")?,
                        "image": row.try_get::<String, _>("personimg")?,
                        "peopledbid": row.try_get::<i32, _>("peopledbid")?,
                        "associatedpodcasts": podcast_count,
                        "episode_count": episode_count
                    });
                    subscriptions.push(subscription);
                }
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query("
                    SELECT PersonID, UserID, Name, PersonImg, PeopleDBID, AssociatedPodcasts
                    FROM People 
                    WHERE UserID = ?
                    ORDER BY Name
                ")
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;
                
                for row in rows {
                    let person_id = row.try_get::<i32, _>("PersonID")?;
                    let associated_podcasts_str = row.try_get::<Option<String>, _>("AssociatedPodcasts")?;
                    
                    // Count associated podcasts by splitting the comma-separated string and filtering out empty/0 values
                    let podcast_count = if let Some(podcasts_str) = &associated_podcasts_str {
                        if podcasts_str.is_empty() {
                            0
                        } else {
                            podcasts_str.split(',')
                                .filter(|s| !s.trim().is_empty() && s.trim() != "0")
                                .count()
                        }
                    } else {
                        0
                    };

                    // Count episodes for this person from PeopleEpisodes table
                    let episode_count: i64 = sqlx::query_scalar("
                        SELECT COUNT(*) 
                        FROM PeopleEpisodes pe
                        INNER JOIN Podcasts p ON pe.PodcastID = p.PodcastID
                        WHERE pe.PersonID = ? AND p.UserID = ?
                    ")
                    .bind(person_id)
                    .bind(user_id)
                    .fetch_one(pool)
                    .await
                    .unwrap_or(0);

                    let subscription = serde_json::json!({
                        "personid": person_id,
                        "userid": row.try_get::<i32, _>("UserID")?,
                        "name": row.try_get::<String, _>("Name")?,
                        "image": row.try_get::<String, _>("PersonImg")?,
                        "peopledbid": row.try_get::<i32, _>("PeopleDBID")?,
                        "associatedpodcasts": podcast_count,
                        "episode_count": episode_count
                    });
                    subscriptions.push(subscription);
                }
            }
        }
        
        println!("Found {} person subscriptions for user {}", subscriptions.len(), user_id);
        Ok(subscriptions)
    }
    
    // Get person episodes - matches Python return_person_episodes function exactly
    pub async fn get_person_episodes(&self, user_id: i32, person_id: i32) -> AppResult<Vec<serde_json::Value>> {
        println!("Getting episodes for user {} and person {}", user_id, person_id);
        
        let mut episodes = Vec::new();
        
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(r#"
                    SELECT
                        e.episodeid,  -- Will be NULL if no match in Episodes table
                        pe.episodetitle,
                        pe.episodedescription,
                        pe.episodeurl,
                        CASE
                            WHEN pe.episodeartwork IS NULL THEN
                                (SELECT artworkurl FROM "Podcasts" WHERE podcastid = pe.podcastid)
                            ELSE
                                CASE 
                                    WHEN p.usepodcastcoverscustomized = TRUE AND p.usepodcastcovers = TRUE THEN p.artworkurl
                                    WHEN u.usepodcastcovers = TRUE THEN p.artworkurl
                                    ELSE pe.episodeartwork
                                END
                        END as episodeartwork,
                        pe.episodepubdate,
                        pe.episodeduration,
                        p.podcastname,
                        CASE
                            WHEN (
                                SELECT 1 FROM "Podcasts"
                                WHERE podcastid = pe.podcastid
                                AND userid = $1
                            ) IS NOT NULL THEN
                            CASE
                                WHEN s.episodeid IS NOT NULL THEN TRUE
                                ELSE FALSE
                            END
                            ELSE FALSE
                        END AS saved,
                        CASE
                            WHEN (
                                SELECT 1 FROM "Podcasts"
                                WHERE podcastid = pe.podcastid
                                AND userid = $1
                            ) IS NOT NULL THEN
                            CASE
                                WHEN d.episodeid IS NOT NULL THEN TRUE
                                ELSE FALSE
                            END
                            ELSE FALSE
                        END AS downloaded,
                        CASE
                            WHEN (
                                SELECT 1 FROM "Podcasts"
                                WHERE podcastid = pe.podcastid
                                AND userid = $1
                            ) IS NOT NULL THEN
                            COALESCE(h.listenduration, 0)
                            ELSE 0
                        END AS listenduration,
                        FALSE as is_youtube
                    FROM "PeopleEpisodes" pe
                    INNER JOIN "People" pp ON pe.personid = pp.personid
                    INNER JOIN "Podcasts" p ON pe.podcastid = p.podcastid
                    LEFT JOIN "Users" u ON p.userid = u.userid
                    LEFT JOIN "Episodes" e ON e.episodeurl = pe.episodeurl AND e.podcastid = pe.podcastid
                    LEFT JOIN (
                        SELECT * FROM "SavedEpisodes" WHERE userid = $2
                    ) s ON s.episodeid = e.episodeid
                    LEFT JOIN (
                        SELECT * FROM "DownloadedEpisodes" WHERE userid = $3
                    ) d ON d.episodeid = e.episodeid
                    LEFT JOIN (
                        SELECT * FROM "UserEpisodeHistory" WHERE userid = $4
                    ) h ON h.episodeid = e.episodeid
                    WHERE pe.personid = $5
                    AND pe.episodepubdate >= NOW() - INTERVAL '30 days'
                    ORDER BY pe.episodepubdate DESC
                "#)
                    .bind(user_id)  // $1
                    .bind(user_id)  // $2
                    .bind(user_id)  // $3
                    .bind(user_id)  // $4
                    .bind(person_id) // $5
                    .fetch_all(pool)
                    .await?;
                
                for row in rows {
                    let episodeid = row.try_get::<Option<i32>, _>("episodeid")?;
                    let episodetitle = row.try_get::<String, _>("episodetitle")?;
                    let episodedescription = row.try_get::<String, _>("episodedescription")?;
                    let episodeurl = row.try_get::<String, _>("episodeurl")?;
                    let episodeartwork = row.try_get::<Option<String>, _>("episodeartwork")?;
                    let dt = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                    let episodepubdate = dt.format("%Y-%m-%dT%H:%M:%S").to_string();
                    let episodeduration = row.try_get::<i32, _>("episodeduration")?;
                    let podcastname = row.try_get::<String, _>("podcastname")?;
                    let saved = row.try_get::<bool, _>("saved")?;
                    let downloaded = row.try_get::<bool, _>("downloaded")?;
                    let listenduration = row.try_get::<i32, _>("listenduration")?;
                    let is_youtube = row.try_get::<bool, _>("is_youtube")?;

                    let episode = serde_json::json!({
                        "episodeid": episodeid.unwrap_or(-1),
                        "episodetitle": episodetitle,
                        "episodedescription": episodedescription,
                        "episodeurl": episodeurl,
                        "episodeartwork": episodeartwork,
                        "episodepubdate": episodepubdate,
                        "episodeduration": episodeduration,
                        "podcastname": podcastname,
                        "saved": saved,
                        "downloaded": downloaded,
                        "listenduration": listenduration,
                        "is_youtube": is_youtube
                    });
                    episodes.push(episode);
                }
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query("
                    SELECT
                        e.EpisodeID,  -- Will be NULL if no match in Episodes table
                        pe.EpisodeTitle,
                        pe.EpisodeDescription,
                        pe.EpisodeURL,
                        CASE 
                            WHEN pe.EpisodeArtwork IS NULL THEN p.ArtworkURL
                            ELSE
                                CASE 
                                    WHEN p.UsePodcastCoversCustomized = TRUE AND p.UsePodcastCovers = TRUE THEN p.ArtworkURL
                                    WHEN u.UsePodcastCovers = TRUE THEN p.ArtworkURL
                                    ELSE pe.EpisodeArtwork
                                END
                        END as EpisodeArtwork,
                        pe.EpisodePubDate,
                        pe.EpisodeDuration,
                        p.PodcastName,
                        IF(
                            EXISTS(
                                SELECT 1 FROM Podcasts
                                WHERE PodcastID = pe.PodcastID
                                AND UserID = ?
                            ),
                            IF(s.EpisodeID IS NOT NULL, TRUE, FALSE),
                            FALSE
                        ) AS Saved,
                        IF(
                            EXISTS(
                                SELECT 1 FROM Podcasts
                                WHERE PodcastID = pe.PodcastID
                                AND UserID = ?
                            ),
                            IF(d.EpisodeID IS NOT NULL, TRUE, FALSE),
                            FALSE
                        ) AS Downloaded,
                        IF(
                            EXISTS(
                                SELECT 1 FROM Podcasts
                                WHERE PodcastID = pe.PodcastID
                                AND UserID = ?
                            ),
                            COALESCE(h.ListenDuration, 0),
                            0
                        ) AS ListenDuration,
                        FALSE as is_youtube
                    FROM PeopleEpisodes pe
                    INNER JOIN People pp ON pe.PersonID = pp.PersonID
                    INNER JOIN Podcasts p ON pe.PodcastID = p.PodcastID
                    LEFT JOIN Users u ON p.UserID = u.UserID
                    LEFT JOIN Episodes e ON e.EpisodeURL = pe.EpisodeURL AND e.PodcastID = pe.PodcastID
                    LEFT JOIN (
                        SELECT * FROM SavedEpisodes WHERE UserID = ?
                    ) s ON s.EpisodeID = e.EpisodeID
                    LEFT JOIN (
                        SELECT * FROM DownloadedEpisodes WHERE UserID = ?
                    ) d ON d.EpisodeID = e.EpisodeID
                    LEFT JOIN (
                        SELECT * FROM UserEpisodeHistory WHERE UserID = ?
                    ) h ON h.EpisodeID = e.EpisodeID
                    WHERE pe.PersonID = ?
                    AND pe.EpisodePubDate >= DATE_SUB(NOW(), INTERVAL 30 DAY)
                    ORDER BY pe.EpisodePubDate DESC
                ")
                    .bind(user_id)    // 1st ?
                    .bind(user_id)    // 2nd ?
                    .bind(user_id)    // 3rd ?
                    .bind(user_id)    // 4th ?
                    .bind(user_id)    // 5th ?
                    .bind(user_id)    // 6th ?
                    .bind(person_id)  // 7th ?
                    .fetch_all(pool)
                    .await?;
                
                for row in rows {
                    let episodeid = row.try_get::<Option<i32>, _>("EpisodeID")?;
                    let episodetitle = row.try_get::<String, _>("EpisodeTitle")?;
                    let episodedescription = row.try_get::<String, _>("EpisodeDescription")?;
                    let episodeurl = row.try_get::<String, _>("EpisodeURL")?;
                    let episodeartwork = row.try_get::<Option<String>, _>("EpisodeArtwork")?;
                    let dt = row.try_get::<chrono::NaiveDateTime, _>("EpisodePubDate")?;
                    let episodepubdate = dt.format("%Y-%m-%dT%H:%M:%S").to_string();
                    let episodeduration = row.try_get::<i32, _>("EpisodeDuration")?;
                    let podcastname = row.try_get::<String, _>("PodcastName")?;
                    let saved = row.try_get::<bool, _>("Saved")?;
                    let downloaded = row.try_get::<bool, _>("Downloaded")?;
                    let listenduration = row.try_get::<i32, _>("ListenDuration")?;
                    let is_youtube = row.try_get::<bool, _>("is_youtube")?;

                    let episode = serde_json::json!({
                        "episodeid": episodeid.unwrap_or(-1),
                        "episodetitle": episodetitle,
                        "episodedescription": episodedescription,
                        "episodeurl": episodeurl,
                        "episodeartwork": episodeartwork,
                        "episodepubdate": episodepubdate,
                        "episodeduration": episodeduration,
                        "podcastname": podcastname,
                        "saved": saved,
                        "downloaded": downloaded,
                        "listenduration": listenduration,
                        "is_youtube": is_youtube
                    });
                    episodes.push(episode);
                }
            }
        }
        
        println!("Found {} episodes for user {} and person {}", episodes.len(), user_id, person_id);
        Ok(episodes)
    }
    
    // Check existing YouTube channel subscription - matches Python check_existing_channel_subscription function exactly
    pub async fn check_existing_channel_subscription(&self, channel_id: &str, user_id: i32) -> AppResult<Option<i32>> {
        println!("Checking existing channel subscription for {} and user {}", channel_id, user_id);
        
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT podcastid FROM "Podcasts" WHERE feedurl = $1 AND userid = $2"#)
                    .bind(format!("https://www.youtube.com/channel/{}", channel_id))
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let podcast_id: i32 = row.try_get("podcastid")?;
                    println!("Found existing subscription with ID: {}", podcast_id);
                    Ok(Some(podcast_id))
                } else {
                    println!("No existing subscription found");
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT PodcastID FROM Podcasts WHERE FeedURL = ? AND UserID = ?")
                    .bind(format!("https://www.youtube.com/channel/{}", channel_id))
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let podcast_id: i32 = row.try_get("podcastid")?;
                    println!("Found existing subscription with ID: {}", podcast_id);
                    Ok(Some(podcast_id))
                } else {
                    println!("No existing subscription found");
                    Ok(None)
                }
            }
        }
    }
    
    // Add YouTube channel - matches Python add_youtube_channel function exactly
    pub async fn add_youtube_channel(&self, channel_info: &std::collections::HashMap<String, String>, user_id: i32, feed_cutoff: i32) -> AppResult<i32> {
        println!("Adding YouTube channel to database for user {}", user_id);
        
        let channel_id = channel_info.get("channel_id").ok_or_else(|| AppError::bad_request("Channel ID is required"))?;
        let empty_string = String::new();
        let name = channel_info.get("name").unwrap_or(&empty_string);
        let description = channel_info.get("description").unwrap_or(&empty_string);
        let thumbnail_url = channel_info.get("thumbnail_url").unwrap_or(&empty_string);
        let feed_url = format!("https://www.youtube.com/channel/{}", channel_id);
        
        // Insert new YouTube channel as podcast
        let podcast_id = match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"
                    INSERT INTO "Podcasts" (
                        userid, podcastname, artworkurl, description, episodecount,
                        websiteurl, feedurl, author, categories, explicit, podcastindexid, feedcutoffdays, isyoutubechannel
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
                    RETURNING podcastid
                "#)
                    .bind(user_id)
                    .bind(name)
                    .bind(thumbnail_url)
                    .bind(description)
                    .bind(0) // Initial episode count
                    .bind(&feed_url)
                    .bind(&feed_url)
                    .bind(name) // Use channel name as author
                    .bind("{}") // Empty categories for YouTube
                    .bind(false) // Not explicit by default
                    .bind(0) // No podcast index ID for YouTube
                    .bind(feed_cutoff)
                    .bind(true) // Is YouTube channel
                    .fetch_one(pool)
                    .await?;
                
                row.try_get("podcastid")?
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query(r#"
                    INSERT INTO Podcasts (
                        UserID, PodcastName, ArtworkURL, Description, EpisodeCount,
                        WebsiteURL, FeedURL, Author, Categories, Explicit, PodcastIndexID, FeedCutoffDays, IsYouTubeChannel
                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                "#)
                    .bind(user_id)
                    .bind(name)
                    .bind(thumbnail_url)
                    .bind(description)
                    .bind(0) // Initial episode count
                    .bind(&feed_url)
                    .bind(&feed_url)
                    .bind(name) // Use channel name as author
                    .bind("{}") // Empty categories for YouTube
                    .bind(false) // Not explicit by default
                    .bind(0) // No podcast index ID for YouTube
                    .bind(feed_cutoff)
                    .bind(true) // Is YouTube channel
                    .execute(pool)
                    .await?;
                
                result.last_insert_id() as i32
            }
        };
        
        // Update UserStats PodcastsAdded counter
        match self {
            DatabasePool::Postgres(pool) => {
                let _ = sqlx::query(r#"
                    UPDATE "UserStats" 
                    SET podcastsadded = podcastsadded + 1 
                    WHERE userid = $1
                "#)
                    .bind(user_id)
                    .execute(pool)
                    .await;
            }
            DatabasePool::MySQL(pool) => {
                let _ = sqlx::query("UPDATE UserStats SET PodcastsAdded = PodcastsAdded + 1 WHERE UserID = ?")
                    .bind(user_id)
                    .execute(pool)
                    .await;
            }
        }
        
        println!("Successfully added YouTube channel with ID: {}", podcast_id);
        Ok(podcast_id)
    }

    // Check if YouTube channel already exists - matches Python check_youtube_channel function exactly
    pub async fn check_youtube_channel(&self, user_id: i32, channel_name: &str, channel_url: &str) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"
                    SELECT podcastid FROM "Podcasts"
                    WHERE userid = $1 AND podcastname = $2 AND feedurl = $3 AND isyoutubechannel = TRUE
                "#)
                    .bind(user_id)
                    .bind(channel_name)
                    .bind(channel_url)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(row.is_some())
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query(r#"
                    SELECT PodcastID FROM Podcasts
                    WHERE UserID = ? AND PodcastName = ? AND FeedURL = ? AND IsYouTubeChannel = TRUE
                "#)
                    .bind(user_id)
                    .bind(channel_name)
                    .bind(channel_url)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(row.is_some())
            }
        }
    }
    
    // Remove old YouTube videos - deletes videos and all their references from dependent tables
    pub async fn remove_old_youtube_videos(&self, podcast_id: i32, cutoff_date: chrono::DateTime<chrono::Utc>) -> AppResult<()> {
        println!("Removing old YouTube videos for podcast {} before {}", podcast_id, cutoff_date);

        let cutoff_naive = cutoff_date.naive_utc();

        let rows_affected = match self {
            DatabasePool::Postgres(pool) => {
                // First, delete all references from dependent tables
                let cleanup_queries = vec![
                    r#"DELETE FROM "UserVideoHistory" WHERE videoid IN (SELECT videoid FROM "YouTubeVideos" WHERE podcastid = $1 AND publishedat < $2)"#,
                    r#"DELETE FROM "SavedVideos" WHERE videoid IN (SELECT videoid FROM "YouTubeVideos" WHERE podcastid = $1 AND publishedat < $2)"#,
                    r#"DELETE FROM "DownloadedVideos" WHERE videoid IN (SELECT videoid FROM "YouTubeVideos" WHERE podcastid = $1 AND publishedat < $2)"#,
                    r#"DELETE FROM "PlaylistContents" WHERE videoid IN (SELECT videoid FROM "YouTubeVideos" WHERE podcastid = $1 AND publishedat < $2)"#,
                    r#"DELETE FROM "EpisodeQueue" WHERE episodeid IN (SELECT videoid FROM "YouTubeVideos" WHERE podcastid = $1 AND publishedat < $2)"#,
                ];

                for query in cleanup_queries {
                    sqlx::query(query)
                        .bind(podcast_id)
                        .bind(cutoff_naive)
                        .execute(pool)
                        .await?;
                }

                // Now delete the videos themselves
                sqlx::query(r#"DELETE FROM "YouTubeVideos" WHERE podcastid = $1 AND publishedat < $2"#)
                    .bind(podcast_id)
                    .bind(cutoff_naive)
                    .execute(pool)
                    .await?
                    .rows_affected()
            }
            DatabasePool::MySQL(pool) => {
                // First, delete all references from dependent tables
                let cleanup_queries = vec![
                    "DELETE FROM UserVideoHistory WHERE VideoID IN (SELECT VideoID FROM YouTubeVideos WHERE PodcastID = ? AND PublishedAt < ?)",
                    "DELETE FROM SavedVideos WHERE VideoID IN (SELECT VideoID FROM YouTubeVideos WHERE PodcastID = ? AND PublishedAt < ?)",
                    "DELETE FROM DownloadedVideos WHERE VideoID IN (SELECT VideoID FROM YouTubeVideos WHERE PodcastID = ? AND PublishedAt < ?)",
                    "DELETE FROM PlaylistContents WHERE VideoID IN (SELECT VideoID FROM YouTubeVideos WHERE PodcastID = ? AND PublishedAt < ?)",
                    "DELETE FROM EpisodeQueue WHERE EpisodeID IN (SELECT VideoID FROM YouTubeVideos WHERE PodcastID = ? AND PublishedAt < ?)",
                ];

                for query in cleanup_queries {
                    sqlx::query(query)
                        .bind(podcast_id)
                        .bind(cutoff_naive)
                        .execute(pool)
                        .await?;
                }

                // Now delete the videos themselves
                sqlx::query("DELETE FROM YouTubeVideos WHERE PodcastID = ? AND PublishedAt < ?")
                    .bind(podcast_id)
                    .bind(cutoff_naive)
                    .execute(pool)
                    .await?
                    .rows_affected()
            }
        };

        println!("Removed {} old YouTube videos", rows_affected);
        Ok(())
    }
    
    // Get existing YouTube videos - matches Python get_existing_youtube_videos function exactly
    pub async fn get_existing_youtube_videos(&self, podcast_id: i32) -> AppResult<Vec<String>> {
        println!("Getting existing YouTube videos for podcast {}", podcast_id);
        
        let mut video_urls = Vec::new();
        
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(r#"SELECT videourl FROM "YouTubeVideos" WHERE podcastid = $1"#)
                    .bind(podcast_id)
                    .fetch_all(pool)
                    .await?;
                
                for row in rows {
                    let url: String = row.try_get("videourl")?;
                    video_urls.push(url);
                }
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query("SELECT VideoURL FROM YouTubeVideos WHERE PodcastID = ?")
                    .bind(podcast_id)
                    .fetch_all(pool)
                    .await?;
                
                for row in rows {
                    let url: String = row.try_get("VideoURL")?;
                    video_urls.push(url);
                }
            }
        }
        
        println!("Found {} existing videos", video_urls.len());
        Ok(video_urls)
    }
    
    // Add YouTube videos - matches Python add_youtube_videos function exactly
    pub async fn add_youtube_videos(&self, podcast_id: i32, videos: &[serde_json::Value]) -> AppResult<()> {
        println!("Adding {} YouTube videos for podcast {}", videos.len(), podcast_id);
        
        for video in videos {
            let video_id = video.get("id").and_then(|v| v.as_str()).unwrap_or("");
            let title = video.get("title").and_then(|v| v.as_str()).unwrap_or("");
            let description = video.get("description").and_then(|v| v.as_str()).unwrap_or("");
            let url = video.get("url").and_then(|v| v.as_str()).unwrap_or("");
            let thumbnail = video.get("thumbnail").and_then(|v| v.as_str()).unwrap_or("");
            
            println!("Processing video {} for database insertion", video_id);
            println!("Video data: {:?}", video);
            
            let duration = if let Some(duration_str) = video.get("duration").and_then(|v| v.as_str()) {
                println!("Duration as string: '{}'", duration_str);
                let parsed = crate::handlers::youtube::parse_youtube_duration(duration_str).unwrap_or(0) as i32;
                println!("Parsed duration: {}", parsed);
                parsed
            } else {
                let int_duration = video.get("duration").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
                println!("Duration as integer: {}", int_duration);
                int_duration
            };
            
            // Parse publish date
            let publish_date = if let Some(date_str) = video.get("publish_date").and_then(|v| v.as_str()) {
                chrono::DateTime::parse_from_rfc3339(date_str)
                    .map(|dt| dt.naive_utc())
                    .unwrap_or_else(|_| chrono::Utc::now().naive_utc())
            } else {
                chrono::Utc::now().naive_utc()
            };
            
            match self {
                DatabasePool::Postgres(pool) => {
                    let _ = sqlx::query(r#"
                        INSERT INTO "YouTubeVideos" (
                            podcastid, youtubevideoid, videotitle, videodescription, videourl,
                            thumbnailurl, publishedat, duration, completed, listenposition
                        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
                    "#)
                        .bind(podcast_id)
                        .bind(video_id)
                        .bind(title)
                        .bind(description)
                        .bind(url)
                        .bind(thumbnail)
                        .bind(publish_date)
                        .bind(duration)
                        .bind(false) // Not completed
                        .bind(0) // Listen position 0
                        .execute(pool)
                        .await;
                }
                DatabasePool::MySQL(pool) => {
                    let _ = sqlx::query(r#"
                        INSERT IGNORE INTO YouTubeVideos (
                            PodcastID, YouTubeVideoID, VideoTitle, VideoDescription, VideoURL,
                            ThumbnailURL, PublishedAt, Duration, Completed, ListenPosition
                        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                    "#)
                        .bind(podcast_id)
                        .bind(video_id)
                        .bind(title)
                        .bind(description)
                        .bind(url)
                        .bind(thumbnail)
                        .bind(publish_date)
                        .bind(duration)
                        .bind(false) // Not completed
                        .bind(0) // Listen position 0
                        .execute(pool)
                        .await;
                }
            }
        }
        
        println!("Successfully added {} YouTube videos", videos.len());
        Ok(())
    }
    
    // Get video date using web scraping - matches Python get_video_date function exactly
    pub async fn get_video_date(&self, video_id: &str) -> AppResult<chrono::DateTime<chrono::Utc>> {
        let client = reqwest::Client::new();
        let url = format!("https://www.youtube.com/watch?v={}", video_id);
        
        let response = client.get(&url)
            .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
            .send()
            .await
            .map_err(|e| AppError::external_error(&format!("Failed to fetch video page: {}", e)))?;

        let html = response.text().await
            .map_err(|e| AppError::external_error(&format!("Failed to read response: {}", e)))?;

        // Parse HTML to find upload date (simplified version of Python's BeautifulSoup approach)
        if let Some(start) = html.find("\"uploadDate\":\"") {
            let date_start = start + "\"uploadDate\":\"".len();
            if let Some(end) = html[date_start..].find("\"") {
                let date_str = &html[date_start..date_start + end];
                if let Ok(parsed_date) = chrono::DateTime::parse_from_rfc3339(date_str) {
                    return Ok(parsed_date.with_timezone(&chrono::Utc));
                }
            }
        }

        // Fallback to current time minus some hours if date not found
        Ok(chrono::Utc::now() - chrono::Duration::hours(1))
    }

    // Update episode count for podcast - matches Python update_episode_count function exactly
    pub async fn update_episode_count(&self, podcast_id: i32) -> AppResult<()> {
        println!("Updating episode count for podcast {}", podcast_id);
        
        // Count episodes and YouTube videos
        let (episode_count, youtube_count) = match self {
            DatabasePool::Postgres(pool) => {
                let episode_row = sqlx::query(r#"SELECT COUNT(*) as count FROM "Episodes" WHERE podcastid = $1"#)
                    .bind(podcast_id)
                    .fetch_one(pool)
                    .await?;
                let episode_count: i64 = episode_row.try_get("count")?;
                
                let youtube_row = sqlx::query(r#"SELECT COUNT(*) as count FROM "YouTubeVideos" WHERE podcastid = $1"#)
                    .bind(podcast_id)
                    .fetch_one(pool)
                    .await?;
                let youtube_count: i64 = youtube_row.try_get("count")?;
                
                (episode_count, youtube_count)
            }
            DatabasePool::MySQL(pool) => {
                let episode_row = sqlx::query("SELECT COUNT(*) as count FROM Episodes WHERE PodcastID = ?")
                    .bind(podcast_id)
                    .fetch_one(pool)
                    .await?;
                let episode_count: i64 = episode_row.try_get("count")?;
                
                let youtube_row = sqlx::query("SELECT COUNT(*) as count FROM YouTubeVideos WHERE PodcastID = ?")
                    .bind(podcast_id)
                    .fetch_one(pool)
                    .await?;
                let youtube_count: i64 = youtube_row.try_get("count")?;
                
                (episode_count, youtube_count)
            }
        };
        
        let total_count = episode_count + youtube_count;
        
        // Update podcast episode count
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Podcasts" SET episodecount = $2 WHERE podcastid = $1"#)
                    .bind(podcast_id)
                    .bind(total_count as i32)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Podcasts SET EpisodeCount = ? WHERE PodcastID = ?")
                    .bind(total_count as i32)
                    .bind(podcast_id)
                    .execute(pool)
                    .await?;
            }
        }
        
        println!("Updated episode count to {} ({} episodes + {} videos)", total_count, episode_count, youtube_count);
        Ok(())
    }

    // Get user history - matches Python user_history function exactly with YouTube UNION
    pub async fn user_history(&self, user_id: i32) -> AppResult<Vec<serde_json::Value>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT * FROM (
                        SELECT
                            "Episodes".episodeid as episodeid,
                            "UserEpisodeHistory".listendate as listendate,
                            "UserEpisodeHistory".listenduration as listenduration,
                            "Episodes".episodetitle as episodetitle,
                            "Episodes".episodedescription as episodedescription,
                            CASE 
                                WHEN "Podcasts".usepodcastcoverscustomized = TRUE AND "Podcasts".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                WHEN "Users".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                ELSE "Episodes".episodeartwork
                            END as episodeartwork,
                            "Episodes".episodeurl as episodeurl,
                            "Episodes".episodeduration as episodeduration,
                            "Podcasts".podcastname as podcastname,
                            "Episodes".episodepubdate as episodepubdate,
                            "Episodes".completed as completed,
                            CASE WHEN "SavedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN "EpisodeQueue".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN "DownloadedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            FALSE as is_youtube
                        FROM "UserEpisodeHistory"
                        JOIN "Episodes" ON "UserEpisodeHistory".episodeid = "Episodes".episodeid
                        JOIN "Podcasts" ON "Episodes".podcastid = "Podcasts".podcastid
                        LEFT JOIN "Users" ON "Podcasts".userid = "Users".userid
                        LEFT JOIN "SavedEpisodes" ON "Episodes".episodeid = "SavedEpisodes".episodeid AND "SavedEpisodes".userid = $1
                        LEFT JOIN "EpisodeQueue" ON "Episodes".episodeid = "EpisodeQueue".episodeid AND "EpisodeQueue".userid = $1
                        LEFT JOIN "DownloadedEpisodes" ON "Episodes".episodeid = "DownloadedEpisodes".episodeid AND "DownloadedEpisodes".userid = $1
                        WHERE "UserEpisodeHistory".userid = $1

                        UNION ALL

                        SELECT
                            "YouTubeVideos".videoid as episodeid,
                            NULL as listendate,
                            "YouTubeVideos".listenposition as listenduration,
                            "YouTubeVideos".videotitle as episodetitle,
                            "YouTubeVideos".videodescription as episodedescription,
                            CASE 
                                WHEN "Podcasts".usepodcastcoverscustomized = TRUE AND "Podcasts".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                WHEN "Users".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                ELSE "YouTubeVideos".thumbnailurl
                            END as episodeartwork,
                            "YouTubeVideos".videourl as episodeurl,
                            "YouTubeVideos".duration as episodeduration,
                            "Podcasts".podcastname as podcastname,
                            "YouTubeVideos".publishedat as episodepubdate,
                            "YouTubeVideos".completed as completed,
                            CASE WHEN "SavedVideos".videoid IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN "EpisodeQueue".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN "DownloadedVideos".videoid IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            TRUE as is_youtube
                        FROM "YouTubeVideos"
                        JOIN "Podcasts" ON "YouTubeVideos".podcastid = "Podcasts".podcastid
                        LEFT JOIN "Users" ON "Podcasts".userid = "Users".userid
                        LEFT JOIN "SavedVideos" ON "YouTubeVideos".videoid = "SavedVideos".videoid AND "SavedVideos".userid = $1
                        LEFT JOIN "EpisodeQueue" ON "YouTubeVideos".videoid = "EpisodeQueue".episodeid AND "EpisodeQueue".userid = $1
                        LEFT JOIN "DownloadedVideos" ON "YouTubeVideos".videoid = "DownloadedVideos".videoid AND "DownloadedVideos".userid = $1
                        WHERE "YouTubeVideos".listenposition > 0
                          AND "Podcasts".userid = $1
                    ) combined_results
                    ORDER BY listendate DESC NULLS LAST"#
                )
                .bind(user_id)
                .fetch_all(pool)
                .await?;

                let mut episodes = Vec::new();
                for row in rows {
                    let listendate = row.try_get::<Option<chrono::NaiveDateTime>, _>("listendate")?
                        .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S").to_string());
                    let episodepubdate = row.try_get::<Option<chrono::NaiveDateTime>, _>("episodepubdate")?
                        .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S").to_string());
                    
                    episodes.push(serde_json::json!({
                        "episodeid": row.get::<Option<i32>, _>("episodeid"),
                        "listendate": listendate,
                        "listenduration": row.get::<Option<i32>, _>("listenduration"),
                        "episodetitle": row.get::<Option<String>, _>("episodetitle"),
                        "episodedescription": row.get::<Option<String>, _>("episodedescription"),
                        "episodeartwork": row.get::<Option<String>, _>("episodeartwork"),
                        "episodeurl": row.get::<Option<String>, _>("episodeurl"),
                        "episodeduration": row.get::<Option<i32>, _>("episodeduration"),
                        "podcastname": row.get::<Option<String>, _>("podcastname"),
                        "episodepubdate": episodepubdate,
                        "completed": row.get::<Option<bool>, _>("completed"),
                        "saved": row.get::<Option<bool>, _>("saved"),
                        "queued": row.get::<Option<bool>, _>("queued"),
                        "downloaded": row.get::<Option<bool>, _>("downloaded"),
                        "is_youtube": row.get::<Option<bool>, _>("is_youtube")
                    }));
                }
                Ok(episodes)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(
                    "SELECT * FROM (
                        SELECT
                            e.EpisodeID as episodeid,
                            ueh.ListenDate as listendate,
                            ueh.ListenDuration as listenduration,
                            e.EpisodeTitle as episodetitle,
                            e.EpisodeDescription as episodedescription,
                            CASE 
                                WHEN p.UsePodcastCoversCustomized = 1 AND p.UsePodcastCovers = 1 THEN p.ArtworkURL
                                WHEN u.UsePodcastCovers = 1 THEN p.ArtworkURL
                                ELSE e.EpisodeArtwork
                            END as episodeartwork,
                            e.EpisodeURL as episodeurl,
                            e.EpisodeDuration as episodeduration,
                            p.PodcastName as podcastname,
                            e.EpisodePubDate as episodepubdate,
                            e.Completed as completed,
                            CASE WHEN se.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN eq.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN de.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            0 as is_youtube
                        FROM UserEpisodeHistory ueh
                        JOIN Episodes e ON ueh.EpisodeID = e.EpisodeID
                        JOIN Podcasts p ON e.PodcastID = p.PodcastID
                        LEFT JOIN Users u ON p.UserID = u.UserID
                        LEFT JOIN SavedEpisodes se ON e.EpisodeID = se.EpisodeID AND se.UserID = ?
                        LEFT JOIN EpisodeQueue eq ON e.EpisodeID = eq.EpisodeID AND eq.UserID = ?
                        LEFT JOIN DownloadedEpisodes de ON e.EpisodeID = de.EpisodeID AND de.UserID = ?
                        WHERE ueh.UserID = ?

                        UNION ALL

                        SELECT
                            yv.VideoID as episodeid,
                            NULL as listendate,
                            yv.ListenPosition as listenduration,
                            yv.VideoTitle as episodetitle,
                            yv.VideoDescription as episodedescription,
                            CASE 
                                WHEN p.UsePodcastCoversCustomized = 1 AND p.UsePodcastCovers = 1 THEN p.ArtworkURL
                                WHEN u.UsePodcastCovers = 1 THEN p.ArtworkURL
                                ELSE yv.ThumbnailURL
                            END as episodeartwork,
                            yv.VideoURL as episodeurl,
                            yv.Duration as episodeduration,
                            p.PodcastName as podcastname,
                            yv.PublishedAt as episodepubdate,
                            yv.Completed as completed,
                            CASE WHEN sv.VideoID IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN eq.EpisodeID IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN dv.VideoID IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            1 as is_youtube
                        FROM YouTubeVideos yv
                        JOIN Podcasts p ON yv.PodcastID = p.PodcastID
                        LEFT JOIN Users u ON p.UserID = u.UserID
                        LEFT JOIN SavedVideos sv ON yv.VideoID = sv.VideoID AND sv.UserID = ?
                        LEFT JOIN EpisodeQueue eq ON yv.VideoID = eq.EpisodeID AND eq.UserID = ?
                        LEFT JOIN DownloadedVideos dv ON yv.VideoID = dv.VideoID AND dv.UserID = ?
                        WHERE yv.ListenPosition > 0
                          AND p.UserID = ?
                    ) combined_results
                    ORDER BY listendate DESC"
                )
                .bind(user_id)  // SavedEpisodes join
                .bind(user_id)  // EpisodeQueue join  
                .bind(user_id)  // DownloadedEpisodes join
                .bind(user_id)  // WHERE clause
                .bind(user_id)  // SavedVideos join
                .bind(user_id)  // EpisodeQueue join (YouTube)
                .bind(user_id)  // DownloadedVideos join
                .bind(user_id)  // WHERE clause (YouTube)
                .fetch_all(pool)
                .await?;

                let mut episodes = Vec::new();
                for row in rows {
                    let listendate = row.try_get::<Option<chrono::NaiveDateTime>, _>("listendate")?
                        .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S").to_string());
                    let episodepubdate = row.try_get::<Option<chrono::NaiveDateTime>, _>("episodepubdate")?
                        .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S").to_string());
                    
                    episodes.push(serde_json::json!({
                        "episodeid": row.get::<Option<i32>, _>("episodeid"),
                        "listendate": listendate,
                        "listenduration": row.get::<Option<i32>, _>("listenduration"),
                        "episodetitle": row.get::<Option<String>, _>("episodetitle"),
                        "episodedescription": row.get::<Option<String>, _>("episodedescription"),
                        "episodeartwork": row.get::<Option<String>, _>("episodeartwork"),
                        "episodeurl": row.get::<Option<String>, _>("episodeurl"),
                        "episodeduration": row.get::<Option<i32>, _>("episodeduration"),
                        "podcastname": row.get::<Option<String>, _>("podcastname"),
                        "episodepubdate": episodepubdate,
                        "completed": row.get::<Option<bool>, _>("completed"),
                        "saved": row.get::<Option<bool>, _>("saved"),
                        "queued": row.get::<Option<bool>, _>("queued"),
                        "downloaded": row.get::<Option<bool>, _>("downloaded"),
                        "is_youtube": row.get::<Option<bool>, _>("is_youtube")
                    }));
                }
                Ok(episodes)
            }
        }
    }

    // Increment listen time - matches Python increment_listen_time function exactly
    pub async fn increment_listen_time(&self, user_id: i32) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "UserStats" SET TimeListened = TimeListened + 1 WHERE UserID = $1"#)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                Ok(())
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE UserStats SET TimeListened = TimeListened + 1 WHERE UserID = ?")
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                Ok(())
            }
        }
    }

    // Get playback speed - matches Python get_playback_speed function exactly
    pub async fn get_playback_speed(&self, user_id: i32, _is_youtube: bool, podcast_id: Option<i32>) -> AppResult<f64> {
        match self {
            DatabasePool::Postgres(pool) => {
                let query = if let Some(_pod_id) = podcast_id {
                    r#"SELECT PlaybackSpeed FROM "Podcasts" WHERE PodcastID = $1"#
                } else {
                    r#"SELECT PlaybackSpeed FROM "Users" WHERE UserID = $1"#
                };
                
                let param = podcast_id.unwrap_or(user_id);
                let row = sqlx::query(query)
                    .bind(param)
                    .fetch_one(pool)
                    .await?;

                Ok(row.try_get::<f64, _>("PlaybackSpeed").unwrap_or(1.0))
            }
            DatabasePool::MySQL(pool) => {
                let query = if let Some(_pod_id) = podcast_id {
                    "SELECT PlaybackSpeed FROM Podcasts WHERE PodcastID = ?"
                } else {
                    "SELECT PlaybackSpeed FROM Users WHERE UserID = ?"
                };
                
                let param = podcast_id.unwrap_or(user_id);
                let row = sqlx::query(query)
                    .bind(param)
                    .fetch_one(pool)
                    .await?;

                if let Ok(speed) = row.try_get::<bigdecimal::BigDecimal, _>("PlaybackSpeed") {
                    Ok(speed.to_f64().unwrap_or(1.0))
                } else {
                    Ok(1.0)
                }
            }
        }
    }

    // Add news feed if not already added - matches Python add_news_feed_if_not_added function exactly

    // Cleanup old episodes - matches Python cleanup_old_episodes function exactly
    pub async fn cleanup_old_episodes(&self) -> AppResult<()> {
        self.cleanup_old_people_episodes(30).await?;
        self.cleanup_expired_shared_episodes().await?;
        Ok(())
    }

    // Cleanup old people episodes - matches Python cleanup_old_people_episodes function exactly
    pub async fn cleanup_old_people_episodes(&self, days: i32) -> AppResult<()> {
        let cutoff_date = chrono::Utc::now() - chrono::Duration::days(days as i64);
        
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"DELETE FROM "PeopleEpisodes" WHERE addeddate < $1"#)
                    .bind(cutoff_date)
                    .execute(pool)
                    .await?;
                
                tracing::info!("Cleaned up {} old PeopleEpisodes records older than {} days", result.rows_affected(), days);
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("DELETE FROM PeopleEpisodes WHERE AddedDate < ?")
                    .bind(cutoff_date)
                    .execute(pool)
                    .await?;
                
                tracing::info!("Cleaned up {} old PeopleEpisodes records older than {} days", result.rows_affected(), days);
            }
        }
        Ok(())
    }

    // Cleanup expired shared episodes - matches Python cleanup_expired_shared_episodes function exactly
    pub async fn cleanup_expired_shared_episodes(&self) -> AppResult<()> {
        let now = chrono::Utc::now();
        
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"DELETE FROM "SharedEpisodes" WHERE expirationdate < $1"#)
                    .bind(now)
                    .execute(pool)
                    .await?;
                
                tracing::info!("Cleaned up {} expired SharedEpisodes records", result.rows_affected());
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("DELETE FROM SharedEpisodes WHERE ExpirationDate < ?")
                    .bind(now)
                    .execute(pool)
                    .await?;
                
                tracing::info!("Cleaned up {} expired SharedEpisodes records", result.rows_affected());
            }
        }
        Ok(())
    }

    // Update all playlists - matches Python update_all_playlists function exactly
    pub async fn update_all_playlists(&self) -> AppResult<()> {
        tracing::info!("=================== PLAYLIST UPDATE STARTING ===================");
        tracing::info!("Starting to fetch all playlists");
        
        match self {
            DatabasePool::Postgres(pool) => {
                let playlists = sqlx::query(r#"
                    SELECT playlistid, name, userid, podcastids, includeunplayed, 
                           includepartiallyplayed, includeplayed, playprogressmin, playprogressmax, 
                           timefilterhours, minduration, maxduration, sortorder, 
                           groupbypodcast, maxepisodes
                    FROM "Playlists"
                "#)
                    .fetch_all(pool)
                    .await?;
                
                tracing::info!("Found {} playlists to update", playlists.len());
                
                for playlist in playlists {
                    let playlist_id: i32 = playlist.try_get("playlistid")?;
                    let playlist_name: String = playlist.try_get("name")?;
                    let user_id: i32 = playlist.try_get("userid")?;
                    
                    tracing::info!("Updating playlist: {} (ID: {}, User: {})", playlist_name, playlist_id, user_id);
                    
                    match self.update_playlist_contents(playlist_id).await {
                        Ok(episode_count) => {
                            tracing::info!("Successfully updated playlist '{}': {} episodes", playlist_name, episode_count);
                        }
                        Err(e) => {
                            tracing::error!("Failed to update playlist '{}' (ID: {}): {}", playlist_name, playlist_id, e);
                            // Continue with other playlists
                        }
                    }
                }
            }
            DatabasePool::MySQL(pool) => {
                let playlists = sqlx::query("
                    SELECT PlaylistID, Name, UserID, PodcastIds, IncludeUnplayed, 
                           IncludePartiallyPlayed, IncludePlayed, PlayProgressMin, PlayProgressMax, 
                           TimeFilterHours, MinDuration, MaxDuration, SortOrder, 
                           GroupByPodcast, MaxEpisodes
                    FROM Playlists
                ")
                    .fetch_all(pool)
                    .await?;
                
                tracing::info!("Found {} playlists to update", playlists.len());
                
                for playlist in playlists {
                    let playlist_id: i32 = playlist.try_get("PlaylistID")?;
                    let playlist_name: String = playlist.try_get("Name")?;
                    let user_id: i32 = playlist.try_get("UserID")?;
                    
                    tracing::info!("Updating playlist: {} (ID: {}, User: {})", playlist_name, playlist_id, user_id);
                    
                    match self.update_playlist_contents(playlist_id).await {
                        Ok(episode_count) => {
                            tracing::info!("Successfully updated playlist '{}': {} episodes", playlist_name, episode_count);
                        }
                        Err(e) => {
                            tracing::error!("Failed to update playlist '{}' (ID: {}): {}", playlist_name, playlist_id, e);
                            // Continue with other playlists
                        }
                    }
                }
            }
        }
        
        tracing::info!("=================== PLAYLIST UPDATE COMPLETED ===================");
        Ok(())
    }

    // Update playlist contents - matches Python update_playlist_contents function exactly
    pub async fn update_playlist_contents(&self, playlist_id: i32) -> AppResult<i32> {
        tracing::info!("======= UPDATE PLAYLIST ID: {} =======", playlist_id);
        
        match self {
            DatabasePool::Postgres(pool) => {
                // Get playlist configuration first  
                let playlist = sqlx::query(r#"
                    SELECT playlistid, name, userid, podcastids, includeunplayed, 
                           includepartiallyplayed, includeplayed, playprogressmin, playprogressmax, 
                           timefilterhours, minduration, maxduration, sortorder, 
                           groupbypodcast, maxepisodes, issystemplaylist
                    FROM "Playlists" WHERE playlistid = $1
                "#)
                    .bind(playlist_id)
                    .fetch_one(pool)
                    .await?;
                
                // Clear existing contents
                sqlx::query(r#"DELETE FROM "PlaylistContents" WHERE playlistid = $1"#)
                    .bind(playlist_id)
                    .execute(pool)
                    .await?;
                
                // Handle special playlists
                let playlist_name: String = playlist.try_get("name")?;
                let is_system_playlist: bool = playlist.try_get("issystemplaylist").unwrap_or(false);
                
                let episode_count = if playlist_name == "Fresh Releases" && is_system_playlist {
                    // Special handling for Fresh Releases
                    self.update_fresh_releases_playlist_postgres(pool, playlist_id).await?
                } else if playlist_name == "Currently Listening" && is_system_playlist {
                    // Special handling for Currently Listening
                    self.update_currently_listening_playlist_postgres(pool, playlist_id).await?
                } else if playlist_name == "Almost Done" && is_system_playlist {
                    // Special handling for Almost Done  
                    self.update_almost_done_playlist_postgres(pool, playlist_id).await?
                } else if playlist_name == "Quick Listens" && is_system_playlist {
                    // Special handling for Quick Listens
                    self.update_quick_listens_playlist_postgres(pool, playlist_id).await?
                } else {
                    // Standard playlist query building
                    self.build_and_execute_playlist_query_postgres(pool, &playlist).await?
                };
                
                // Update timestamp
                sqlx::query(r#"UPDATE "Playlists" SET lastupdated = CURRENT_TIMESTAMP WHERE playlistid = $1"#)
                    .bind(playlist_id)
                    .execute(pool)
                    .await?;
                
                Ok(episode_count)
            }
            DatabasePool::MySQL(pool) => {
                // Get playlist configuration first
                let playlist = sqlx::query("
                    SELECT PlaylistID, Name, UserID, PodcastIDs, IncludeUnplayed, 
                           IncludePartiallyPlayed, IncludePlayed, PlayProgressMin, PlayProgressMax, 
                           TimeFilterHours, MinDuration, MaxDuration, SortOrder, 
                           GroupByPodcast, MaxEpisodes, IsSystemPlaylist
                    FROM Playlists WHERE PlaylistID = ?
                ")
                    .bind(playlist_id)
                    .fetch_one(pool)
                    .await?;
                
                // Clear existing contents  
                sqlx::query("DELETE FROM PlaylistContents WHERE PlaylistID = ?")
                    .bind(playlist_id)
                    .execute(pool)
                    .await?;
                
                // Handle special playlists
                let playlist_name: String = playlist.try_get("Name")?;
                let is_system_playlist: bool = playlist.try_get("IsSystemPlaylist").unwrap_or(false);
                
                let episode_count = if playlist_name == "Fresh Releases" && is_system_playlist {
                    // Special handling for Fresh Releases
                    self.update_fresh_releases_playlist_mysql(pool, playlist_id).await?
                } else if playlist_name == "Currently Listening" && is_system_playlist {
                    // Special handling for Currently Listening
                    self.update_currently_listening_playlist_mysql(pool, playlist_id).await?
                } else if playlist_name == "Almost Done" && is_system_playlist {
                    // Special handling for Almost Done
                    self.update_almost_done_playlist_mysql(pool, playlist_id).await?
                } else if playlist_name == "Quick Listens" && is_system_playlist {
                    // Special handling for Quick Listens
                    self.update_quick_listens_playlist_mysql(pool, playlist_id).await?
                } else {
                    // Standard playlist query building
                    self.build_and_execute_playlist_query_mysql(pool, &playlist).await?
                };
                
                // Update timestamp
                sqlx::query("UPDATE Playlists SET LastUpdated = CURRENT_TIMESTAMP WHERE PlaylistID = ?")
                    .bind(playlist_id)
                    .execute(pool)
                    .await?;
                
                Ok(episode_count)
            }
        }
    }

    // Get all people/hosts for refresh_hosts endpoint - matches Python refresh_all_hosts function
    pub async fn get_all_people_for_refresh(&self) -> AppResult<Vec<(i32, String, i32)>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let people = sqlx::query(r#"
                    SELECT DISTINCT p.personid, p.name, p.userid
                    FROM "People" p
                "#)
                    .fetch_all(pool)
                    .await?;
                
                let mut result = Vec::new();
                for person in people {
                    let person_id: i32 = person.try_get("personid")?;
                    let name: String = person.try_get("name")?;
                    let user_id: i32 = person.try_get("userid")?;
                    result.push((person_id, name, user_id));
                }
                Ok(result)
            }
            DatabasePool::MySQL(pool) => {
                let people = sqlx::query("
                    SELECT DISTINCT p.PersonID, p.Name, p.UserID
                    FROM People p
                ")
                    .fetch_all(pool)
                    .await?;
                
                let mut result = Vec::new();
                for person in people {
                    let person_id: i32 = person.try_get("PersonID")?;
                    let name: String = person.try_get("Name")?;
                    let user_id: i32 = person.try_get("UserID")?;
                    result.push((person_id, name, user_id));
                }
                Ok(result)
            }
        }
    }

    // Process person subscription - matches Python process_person_subscription function exactly
    pub async fn process_person_subscription(&self, user_id: i32, person_id: i32, person_name: String) -> AppResult<()> {
        use std::collections::HashSet;
        
        tracing::info!("Starting refresh for host: {} (ID: {})", person_name, person_id);
        
        let mut processed_shows: HashSet<(String, String, i32)> = HashSet::new();
        let people_url = std::env::var("PEOPLE_API_URL").unwrap_or_else(|_| "https://podpeople.pinepods.online".to_string());
        let api_url = std::env::var("SEARCH_API_URL").unwrap_or_else(|_| "https://api.pinepods.online/api/search".to_string());
        
        // 1. Get podcasts from podpeople
        let client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(30))
            .build()
            .map_err(|e| AppError::internal(&format!("Failed to create HTTP client: {}", e)))?;
        
        match client
            .get(&format!("{}/api/hostsearch", people_url))
            .query(&[("name", &person_name)])
            .send()
            .await
        {
            Ok(response) => {
                if let Ok(podpeople_data) = response.json::<serde_json::Value>().await {
                    if podpeople_data.get("success").and_then(|v| v.as_bool()).unwrap_or(false) {
                        if let Some(podcasts) = podpeople_data.get("podcasts").and_then(|v| v.as_array()) {
                            for podcast in podcasts {
                                if let (Some(title), Some(feed_url), Some(id)) = (
                                    podcast.get("title").and_then(|v| v.as_str()),
                                    podcast.get("feed_url").and_then(|v| v.as_str()),
                                    podcast.get("id").and_then(|v| v.as_i64()),
                                ) {
                                    processed_shows.insert((title.to_string(), feed_url.to_string(), id as i32));
                                }
                            }
                        }
                    }
                }
            }
            Err(e) => {
                tracing::error!("Error getting data from podpeople: {}", e);
            }
        }
        
        // 2. Get podcasts from podcast index
        tracing::info!("API URL configured as: {}", api_url);
        match client
            .get(&api_url)
            .query(&[
                ("query", person_name.as_str()),
                ("index", "person"),
                ("search_type", "person")
            ])
            .send()
            .await
        {
            Ok(response) => {
                if let Ok(index_data) = response.json::<serde_json::Value>().await {
                    if let Some(items) = index_data.get("items").and_then(|v| v.as_array()) {
                        for episode in items {
                            if let (Some(title), Some(feed_url), Some(feed_id)) = (
                                episode.get("feedTitle").and_then(|v| v.as_str()),
                                episode.get("feedUrl").and_then(|v| v.as_str()),
                                episode.get("feedId").and_then(|v| v.as_i64()),
                            ) {
                                processed_shows.insert((title.to_string(), feed_url.to_string(), feed_id as i32));
                            }
                        }
                    }
                }
            }
            Err(e) => {
                tracing::error!("Error getting data from podcast index: {}", e);
            }
        }
        
        if processed_shows.is_empty() {
            tracing::info!("No shows found for person: {}", person_name);
            return Ok(());
        }
        
        // 3. Process each unique show
        for (title, feed_url, feed_id) in processed_shows {
            match self.process_person_show(user_id, person_id, &title, &feed_url, feed_id).await {
                Ok(_) => {
                    tracing::info!("Successfully processed show: {}", title);
                }
                Err(e) => {
                    tracing::error!("Error processing show {}: {}", title, e);
                    continue;
                }
            }
        }
        
        Ok(())
    }

    // Helper function to process individual show for person - matches Python logic
    async fn process_person_show(&self, user_id: i32, person_id: i32, title: &str, feed_url: &str, _feed_id: i32) -> AppResult<()> {
        // First check if podcast exists for user
        let user_podcast_id = self.get_podcast_id_by_feed_url(user_id, feed_url).await?;
        
        let podcast_id = if user_podcast_id.is_none() {
            // Check if system podcast exists (UserID = 1)
            let system_podcast_id = self.get_podcast_id_by_feed_url(1, feed_url).await?;
            
            if system_podcast_id.is_none() {
                // Add as new system podcast
                tracing::info!("Creating system podcast for feed: {}", feed_url);
                let podcast_values = self.get_podcast_values_for_person(feed_url).await?;
                let add_result = self.add_person_podcast_from_values(&podcast_values, 1).await?;
                tracing::info!("Add podcast result: {}", add_result);
                
                // Get the podcast ID after adding
                tracing::info!("Looking for podcast with UserID=1 and FeedURL='{}'", feed_url);
                match self.get_podcast_id_by_feed_url(1, feed_url).await? {
                    Some(id) => {
                        tracing::info!("Successfully created system podcast with ID: {}", id);
                        id
                    }
                    None => {
                        // Let's debug by listing all podcasts for UserID=1
                        tracing::error!("Failed to get podcast ID after adding system podcast for feed: {}", feed_url);
                        
                        // Debug: List all system podcasts to see what's there
                        match self {
                            DatabasePool::Postgres(pool) => {
                                let rows = sqlx::query(r#"SELECT podcastid, podcastname, feedurl FROM "Podcasts" WHERE userid = $1"#)
                                    .bind(1)
                                    .fetch_all(pool)
                                    .await?;
                                
                                tracing::error!("System podcasts (UserID=1):");
                                for row in rows {
                                    let id: i32 = row.try_get("podcastid")?;
                                    let name: String = row.try_get("podcastname")?;
                                    let url: String = row.try_get("feedurl")?;
                                    tracing::error!("  ID: {}, Name: '{}', URL: '{}'", id, name, url);
                                }
                            }
                            DatabasePool::MySQL(pool) => {
                                let rows = sqlx::query("SELECT PodcastID, PodcastName, FeedURL FROM Podcasts WHERE UserID = ?")
                                    .bind(1)
                                    .fetch_all(pool)
                                    .await?;
                                
                                tracing::error!("System podcasts (UserID=1):");
                                for row in rows {
                                    let id: i32 = row.try_get("PodcastID")?;
                                    let name: String = row.try_get("PodcastName")?;
                                    let url: String = row.try_get("FeedURL")?;
                                    tracing::error!("  ID: {}, Name: '{}', URL: '{}'", id, name, url);
                                }
                            }
                        }
                        
                        return Err(AppError::internal("Failed to create system podcast"));
                    }
                }
            } else {
                system_podcast_id.unwrap()
            }
        } else {
            user_podcast_id.unwrap()
        };
        
        tracing::info!("Using podcast: ID={}, Title={}", podcast_id, title);
        
        // Add episodes to PeopleEpisodes
        self.add_people_episodes(person_id, podcast_id, feed_url).await?;
        
        Ok(())
    }

    // Add people episodes - matches Python add_people_episodes function exactly
    pub async fn add_people_episodes(&self, person_id: i32, podcast_id: i32, feed_url: &str) -> AppResult<()> {
        // Validate that we have a valid podcast ID
        if podcast_id <= 0 {
            return Err(AppError::internal(&format!("Invalid podcast ID {} for person episodes", podcast_id)));
        }
        
        // Use the same robust feed fetching and parsing as add_episodes
        let content = self.try_fetch_feed(feed_url, None, None).await?;
        let episodes = self.parse_rss_feed(&content, podcast_id, "").await?;
        
        println!("Parsed {} episodes from feed for person {} with podcast ID {}", episodes.len(), person_id, podcast_id);
        
        let mut added_count = 0;
        
        for episode in episodes {
            // Check if episode already exists
            let episode_exists = match self {
                DatabasePool::Postgres(pool) => {
                    let result = sqlx::query(r#"
                        SELECT episodeid FROM "PeopleEpisodes"
                        WHERE personid = $1 AND podcastid = $2 AND episodeurl = $3
                    "#)
                    .bind(person_id)
                    .bind(podcast_id)
                    .bind(&episode.url)
                    .fetch_optional(pool)
                    .await?;
                    
                    result.is_some()
                }
                DatabasePool::MySQL(pool) => {
                    let result = sqlx::query("
                        SELECT EpisodeID FROM PeopleEpisodes
                        WHERE PersonID = ? AND PodcastID = ? AND EpisodeURL = ?
                    ")
                    .bind(person_id)
                    .bind(podcast_id)
                    .bind(&episode.url)
                    .fetch_optional(pool)
                    .await?;
                    
                    result.is_some()
                }
            };
            
            if episode_exists {
                continue;
            }
            
            // Insert new episode
            match self {
                DatabasePool::Postgres(pool) => {
                    // PostgreSQL expects timestamp type, not string
                    let naive_datetime = episode.pub_date.naive_utc();
                    sqlx::query(r#"
                        INSERT INTO "PeopleEpisodes"
                        (personid, podcastid, episodetitle, episodedescription,
                        episodeurl, episodeartwork, episodepubdate, episodeduration)
                        VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
                    "#)
                    .bind(person_id)
                    .bind(podcast_id)
                    .bind(&episode.title)
                    .bind(&episode.description)
                    .bind(&episode.url)
                    .bind(&episode.artwork_url)
                    .bind(naive_datetime)
                    .bind(episode.duration as i32)
                    .execute(pool)
                    .await?;
                }
                DatabasePool::MySQL(pool) => {
                    // MySQL accepts string format
                    let pub_date_str = episode.pub_date.format("%Y-%m-%d %H:%M:%S").to_string();
                    sqlx::query("
                        INSERT INTO PeopleEpisodes
                        (PersonID, PodcastID, EpisodeTitle, EpisodeDescription,
                        EpisodeURL, EpisodeArtwork, EpisodePubDate, EpisodeDuration)
                        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                    ")
                    .bind(person_id)
                    .bind(podcast_id)
                    .bind(&episode.title)
                    .bind(&episode.description)
                    .bind(&episode.url)
                    .bind(&episode.artwork_url)
                    .bind(&pub_date_str)
                    .bind(episode.duration as i32)
                    .execute(pool)
                    .await?;
                }
            }
            
            added_count += 1;
        }
        
        println!("Successfully added {} new episodes for person {} from podcast {}", added_count, person_id, podcast_id);
        Ok(())
    }

    // Helper function to parse duration from string
    fn parse_duration(&self, duration_str: &str) -> Option<i64> {
        if duration_str.contains(':') {
            let parts: Vec<&str> = duration_str.split(':').collect();
            match parts.len() {
                2 => {
                    let minutes: i64 = parts[0].parse().ok()?;
                    let seconds: i64 = parts[1].parse().ok()?;
                    Some(minutes * 60 + seconds)
                }
                3 => {
                    let hours: i64 = parts[0].parse().ok()?;
                    let minutes: i64 = parts[1].parse().ok()?;
                    let seconds: i64 = parts[2].parse().ok()?;
                    Some(hours * 3600 + minutes * 60 + seconds)
                }
                _ => None,
            }
        } else if let Ok(duration) = duration_str.parse::<i64>() {
            Some(duration)
        } else {
            None
        }
    }

    // Add person podcast from values map - matches Python add_person_podcast function exactly  
    pub async fn add_person_podcast_from_values(&self, podcast_values: &std::collections::HashMap<String, String>, user_id: i32) -> AppResult<bool> {
        // Use the same key mapping as add_podcast_from_values
        let pod_title = podcast_values.get("podcastname").cloned().unwrap_or_default();
        let pod_feed_url = podcast_values.get("feedurl").cloned().unwrap_or_default();
        let pod_artwork = podcast_values.get("artworkurl").cloned().unwrap_or_default();
        let pod_description = podcast_values.get("description").cloned().unwrap_or_default();
        // First check if podcast already exists for user with a valid feed URL
        match self {
            DatabasePool::Postgres(pool) => {
                let existing = sqlx::query(r#"SELECT podcastid FROM "Podcasts" WHERE feedurl = $1 AND userid = $2 AND feedurl != ''"#)
                    .bind(&pod_feed_url)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if existing.is_some() {
                    return Ok(true); // Already exists
                }
                
                // Insert new podcast
                sqlx::query(r#"
                    INSERT INTO "Podcasts" (podcastname, feedurl, artworkurl, description, 
                                         userid, autodownload, isyoutubechannel, podcastindexid)
                    VALUES ($1, $2, $3, $4, $5, FALSE, FALSE, $6)
                "#)
                .bind(&pod_title)
                .bind(&pod_feed_url)
                .bind(&pod_artwork)
                .bind(&pod_description)
                .bind(user_id)
                .bind(0) // podcast_index_id placeholder
                .execute(pool)
                .await?;
            }
            DatabasePool::MySQL(pool) => {
                let existing = sqlx::query("SELECT PodcastID FROM Podcasts WHERE FeedURL = ? AND UserID = ? AND FeedURL != ''")
                    .bind(&pod_feed_url)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if existing.is_some() {
                    return Ok(true); // Already exists
                }
                
                // Insert new podcast
                sqlx::query("
                    INSERT INTO Podcasts (PodcastName, FeedURL, ArtworkURL, Description, 
                                        UserID, AutoDownload, IsYouTubeChannel, PodcastIndexID)
                    VALUES (?, ?, ?, ?, ?, 0, 0, ?)
                ")
                .bind(&pod_title)
                .bind(&pod_feed_url)
                .bind(&pod_artwork)
                .bind(&pod_description)
                .bind(user_id)
                .bind(0) // podcast_index_id placeholder
                .execute(pool)
                .await?;
            }
        }
        
        Ok(true)
    }

    // Get podcast ID by feed URL
    pub async fn get_podcast_id_by_feed_url(&self, user_id: i32, feed_url: &str) -> AppResult<Option<i32>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"SELECT podcastid FROM "Podcasts" WHERE feedurl = $1 AND userid = $2"#)
                    .bind(feed_url)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(result.map(|row| row.try_get("podcastid")).transpose()?)
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("SELECT PodcastID FROM Podcasts WHERE FeedURL = ? AND UserID = ?")
                    .bind(feed_url)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(result.map(|row| row.try_get("PodcastID")).transpose()?)
            }
        }
    }

    // Get podcast values for person podcasts - uses existing get_podcast_values function
    pub async fn get_podcast_values_for_person(&self, feed_url: &str) -> AppResult<std::collections::HashMap<String, String>> {
        self.get_podcast_values(feed_url, 1, None, None).await
    }

    // Helper function to normalize timezone names for database compatibility
    fn normalize_timezone(tz_str: &str) -> String {
        // Try to parse the timezone string with chrono-tz
        if let Ok(tz) = tz_str.parse::<Tz>() {
            // Return the canonical name for this timezone
            tz.name().to_string()
        } else {
            // If parsing fails, return UTC as fallback
            tracing::warn!("Unable to parse timezone '{}', falling back to UTC", tz_str);
            "UTC".to_string()
        }
    }

    // COMPLETE PLAYLIST SYSTEM IMPLEMENTATION - matches Python functionality exactly

    // Update Fresh Releases playlist with timezone-aware logic - matches Python update_fresh_releases_playlist
    async fn update_fresh_releases_playlist_postgres(&self, pool: &Pool<Postgres>, playlist_id: i32) -> AppResult<i32> {
        tracing::info!("Updating Fresh Releases playlist with timezone logic");
        
        // Get all users with their timezones
        let users = sqlx::query(r#"SELECT userid, timezone FROM "Users""#)
            .fetch_all(pool)
            .await?;
        
        let mut added_episodes: std::collections::HashSet<i32> = std::collections::HashSet::new();
        let mut position = 1;
        
        for user in users {
            let user_id: i32 = user.try_get("userid")?;
            let timezone: Option<String> = user.try_get("timezone").ok();
            let raw_tz = timezone.as_deref()
                .filter(|s| !s.is_empty()) // Filter out empty strings
                .unwrap_or("UTC");
            
            let normalized_tz = Self::normalize_timezone(raw_tz);
            
            tracing::info!("Processing Fresh Releases for user {} with timezone {} (normalized: {})", user_id, raw_tz, normalized_tz);
            
            // Get episodes from last 24 hours in user's timezone
            let episodes = sqlx::query(r#"
                SELECT e.episodeid
                FROM "Episodes" e
                JOIN "Podcasts" p ON e.podcastid = p.podcastid
                WHERE e.episodepubdate AT TIME ZONE 'UTC' AT TIME ZONE $1 >
                      (CURRENT_TIMESTAMP AT TIME ZONE 'UTC' AT TIME ZONE $1 - INTERVAL '24 hours')
                ORDER BY e.episodepubdate DESC
            "#)
                .bind(&normalized_tz)
                .fetch_all(pool)
                .await?;
            
            // Add unique episodes to playlist
            for episode in episodes {
                let episode_id: i32 = episode.try_get("episodeid")?;
                if !added_episodes.contains(&episode_id) {
                    sqlx::query(r#"
                        INSERT INTO "PlaylistContents" (playlistid, episodeid, position)
                        VALUES ($1, $2, $3)
                    "#)
                        .bind(playlist_id)
                        .bind(episode_id)
                        .bind(position)
                        .execute(pool)
                        .await?;
                    
                    added_episodes.insert(episode_id);
                    position += 1;
                }
            }
        }
        
        tracing::info!("Fresh Releases playlist updated with {} episodes", added_episodes.len());
        Ok(added_episodes.len() as i32)
    }

    // Update Currently Listening playlist - episodes that users have started but not finished
    async fn update_currently_listening_playlist_postgres(&self, pool: &Pool<Postgres>, playlist_id: i32) -> AppResult<i32> {
        tracing::info!("Updating Currently Listening playlist");
        
        // Get all users and their currently listening episodes
        let users = sqlx::query(r#"SELECT userid FROM "Users""#)
            .fetch_all(pool)
            .await?;
        
        let mut added_episodes: std::collections::HashSet<i32> = std::collections::HashSet::new();
        let mut position = 1;
        
        for user in users {
            let user_id: i32 = user.try_get("userid")?;
            
            tracing::info!("Processing Currently Listening for user {}", user_id);
            
            // Get episodes user has started but not finished
            let episodes = sqlx::query(r#"
                SELECT e.episodeid
                FROM "Episodes" e
                JOIN "UserEpisodeHistory" h ON e.episodeid = h.episodeid
                WHERE h.userid = $1
                AND h.listenduration > 0
                AND h.listenduration < e.episodeduration
                ORDER BY h.listendate DESC
            "#)
                .bind(user_id)
                .fetch_all(pool)
                .await?;
            
            // Add unique episodes to playlist
            for episode in episodes {
                let episode_id: i32 = episode.try_get("episodeid")?;
                if !added_episodes.contains(&episode_id) {
                    sqlx::query(r#"INSERT INTO "PlaylistContents" (playlistid, episodeid, position) VALUES ($1, $2, $3)"#)
                        .bind(playlist_id)
                        .bind(episode_id)
                        .bind(position)
                        .execute(pool)
                        .await?;
                    
                    added_episodes.insert(episode_id);
                    position += 1;
                }
            }
        }
        
        tracing::info!("Currently Listening playlist updated with {} episodes", added_episodes.len());
        Ok(added_episodes.len() as i32)
    }

    // Update Almost Done playlist - episodes that users are 75%+ through
    async fn update_almost_done_playlist_postgres(&self, pool: &Pool<Postgres>, playlist_id: i32) -> AppResult<i32> {
        tracing::info!("Updating Almost Done playlist");
        
        // Get all users and their almost done episodes
        let users = sqlx::query(r#"SELECT userid FROM "Users""#)
            .fetch_all(pool)
            .await?;
        
        let mut added_episodes: std::collections::HashSet<i32> = std::collections::HashSet::new();
        let mut position = 1;
        
        for user in users {
            let user_id: i32 = user.try_get("userid")?;
            
            tracing::info!("Processing Almost Done for user {}", user_id);
            
            // Get episodes user is 75%+ through but not completed
            let episodes = sqlx::query(r#"
                SELECT e.episodeid
                FROM "Episodes" e
                JOIN "UserEpisodeHistory" h ON e.episodeid = h.episodeid
                WHERE h.userid = $1
                AND h.listenduration > 0
                AND h.listenduration < e.episodeduration
                AND (h.listenduration::float / NULLIF(e.episodeduration, 0)) >= 0.75
                ORDER BY h.listendate DESC
            "#)
                .bind(user_id)
                .fetch_all(pool)
                .await?;
            
            // Add unique episodes to playlist
            for episode in episodes {
                let episode_id: i32 = episode.try_get("episodeid")?;
                if !added_episodes.contains(&episode_id) {
                    sqlx::query(r#"INSERT INTO "PlaylistContents" (playlistid, episodeid, position) VALUES ($1, $2, $3)"#)
                        .bind(playlist_id)
                        .bind(episode_id)
                        .bind(position)
                        .execute(pool)
                        .await?;
                    
                    added_episodes.insert(episode_id);
                    position += 1;
                }
            }
        }
        
        tracing::info!("Almost Done playlist updated with {} episodes", added_episodes.len());
        Ok(added_episodes.len() as i32)
    }

    // Update Quick Listens playlist - episodes under 15 minutes from ALL users' podcasts
    async fn update_quick_listens_playlist_postgres(&self, pool: &Pool<Postgres>, playlist_id: i32) -> AppResult<i32> {
        tracing::info!("Updating Quick Listens playlist");
        
        // Get shortest 1000 episodes under 15 minutes (900 seconds) and over 1 second from ALL podcasts
        let episodes = sqlx::query(r#"
            SELECT e.episodeid
            FROM "Episodes" e
            JOIN "Podcasts" p ON e.podcastid = p.podcastid
            WHERE e.episodeduration >= 1
            AND e.episodeduration <= 900
            AND e.completed = FALSE
            ORDER BY e.episodeduration ASC
            LIMIT 1000
        "#)
            .fetch_all(pool)
            .await?;
        
        let mut position = 1;
        
        // Add episodes to playlist
        for episode in episodes {
            let episode_id: i32 = episode.try_get("episodeid")?;
            sqlx::query(r#"INSERT INTO "PlaylistContents" (playlistid, episodeid, position) VALUES ($1, $2, $3)"#)
                .bind(playlist_id)
                .bind(episode_id)
                .bind(position)
                .execute(pool)
                .await?;
            
            position += 1;
        }
        
        let episode_count = position - 1;
        tracing::info!("Quick Listens playlist updated with {} episodes", episode_count);
        Ok(episode_count)
    }

    async fn update_quick_listens_playlist_mysql(&self, pool: &Pool<MySql>, playlist_id: i32) -> AppResult<i32> {
        tracing::info!("Updating Quick Listens playlist");
        
        // Get shortest 1000 episodes under 15 minutes (900 seconds) and over 1 second from ALL podcasts
        let episodes = sqlx::query("
            SELECT e.EpisodeID
            FROM Episodes e
            JOIN Podcasts p ON e.PodcastID = p.PodcastID
            WHERE e.EpisodeDuration >= 1
            AND e.EpisodeDuration <= 900
            AND e.Completed = FALSE
            ORDER BY e.EpisodeDuration ASC
            LIMIT 1000
        ")
            .fetch_all(pool)
            .await?;
        
        let mut position = 1;
        
        // Add episodes to playlist
        for episode in episodes {
            let episode_id: i32 = episode.try_get("EpisodeID")?;
            sqlx::query("INSERT INTO PlaylistContents (PlaylistID, EpisodeID, Position) VALUES (?, ?, ?)")
                .bind(playlist_id)
                .bind(episode_id)
                .bind(position)
                .execute(pool)
                .await?;
            
            position += 1;
        }
        
        let episode_count = position - 1;
        tracing::info!("Quick Listens playlist updated with {} episodes", episode_count);
        Ok(episode_count)
    }

    async fn update_fresh_releases_playlist_mysql(&self, pool: &Pool<MySql>, playlist_id: i32) -> AppResult<i32> {
        tracing::info!("Updating Fresh Releases playlist with timezone logic");
        
        // Get all users with their timezones
        let users = sqlx::query("SELECT UserID, TimeZone FROM Users")
            .fetch_all(pool)
            .await?;
        
        let mut added_episodes: std::collections::HashSet<i32> = std::collections::HashSet::new();
        let mut position = 1;
        
        for user in users {
            let user_id: i32 = user.try_get("UserID")?;
            let timezone: Option<String> = user.try_get("TimeZone").ok();
            let raw_tz = timezone.as_deref()
                .filter(|s| !s.is_empty()) // Filter out empty strings
                .unwrap_or("UTC");
            
            let normalized_tz = Self::normalize_timezone(raw_tz);
            
            tracing::info!("Processing Fresh Releases for user {} with timezone {} (normalized: {})", user_id, raw_tz, normalized_tz);
            
            // Get episodes from last 24 hours in user's timezone
            let episodes = sqlx::query("
                SELECT e.EpisodeID
                FROM Episodes e
                JOIN Podcasts p ON e.PodcastID = p.PodcastID
                WHERE CONVERT_TZ(e.EpisodePubDate, 'UTC', ?) >
                      DATE_SUB(CONVERT_TZ(NOW(), 'UTC', ?), INTERVAL 24 HOUR)
                ORDER BY e.EpisodePubDate DESC
            ")
                .bind(&normalized_tz)
                .bind(&normalized_tz)
                .fetch_all(pool)
                .await?;
            
            // Add unique episodes to playlist
            for episode in episodes {
                let episode_id: i32 = episode.try_get("EpisodeID")?;
                if !added_episodes.contains(&episode_id) {
                    sqlx::query("
                        INSERT INTO PlaylistContents (PlaylistID, EpisodeID, Position)
                        VALUES (?, ?, ?)
                    ")
                        .bind(playlist_id)
                        .bind(episode_id)
                        .bind(position)
                        .execute(pool)
                        .await?;
                    
                    added_episodes.insert(episode_id);
                    position += 1;
                }
            }
        }
        
        tracing::info!("Fresh Releases playlist updated with {} episodes", added_episodes.len());
        Ok(added_episodes.len() as i32)
    }

    async fn update_currently_listening_playlist_mysql(&self, pool: &Pool<MySql>, playlist_id: i32) -> AppResult<i32> {
        tracing::info!("Updating Currently Listening playlist");
        
        // Clear existing playlist contents
        sqlx::query("DELETE FROM PlaylistContents WHERE PlaylistID = ?")
            .bind(playlist_id)
            .execute(pool)
            .await?;
        
        // Get all users
        let users = sqlx::query("SELECT UserID FROM Users")
            .fetch_all(pool)
            .await?;
        
        let mut added_episodes: std::collections::HashSet<i32> = std::collections::HashSet::new();
        let mut position = 1;
        
        for user in users {
            let user_id: i32 = user.try_get("UserID")?;
            
            tracing::info!("Processing Currently Listening for user {}", user_id);
            
            // Get episodes that are currently being listened to (started but not completed)
            let episodes = sqlx::query("
                SELECT e.EpisodeID
                FROM Episodes e
                JOIN Podcasts p ON e.PodcastID = p.PodcastID
                JOIN UserEpisodeHistory h ON e.EpisodeID = h.EpisodeID AND h.UserID = ?
                WHERE h.ListenDuration > 0 AND h.ListenDuration < e.EpisodeDuration
                ORDER BY h.ListenDate DESC
            ")
                .bind(user_id)
                .fetch_all(pool)
                .await?;
            
            // Add unique episodes to playlist
            for episode in episodes {
                let episode_id: i32 = episode.try_get("EpisodeID")?;
                if !added_episodes.contains(&episode_id) {
                    sqlx::query("
                        INSERT INTO PlaylistContents (PlaylistID, EpisodeID, Position)
                        VALUES (?, ?, ?)
                    ")
                        .bind(playlist_id)
                        .bind(episode_id)
                        .bind(position)
                        .execute(pool)
                        .await?;
                    
                    added_episodes.insert(episode_id);
                    position += 1;
                }
            }
        }
        
        tracing::info!("Currently Listening playlist updated with {} episodes", added_episodes.len());
        Ok(added_episodes.len() as i32)
    }

    async fn update_almost_done_playlist_mysql(&self, pool: &Pool<MySql>, playlist_id: i32) -> AppResult<i32> {
        tracing::info!("Updating Almost Done playlist");
        
        // Clear existing playlist contents
        sqlx::query("DELETE FROM PlaylistContents WHERE PlaylistID = ?")
            .bind(playlist_id)
            .execute(pool)
            .await?;
        
        // Get all users
        let users = sqlx::query("SELECT UserID FROM Users")
            .fetch_all(pool)
            .await?;
        
        let mut added_episodes: std::collections::HashSet<i32> = std::collections::HashSet::new();
        let mut position = 1;
        
        for user in users {
            let user_id: i32 = user.try_get("UserID")?;
            
            tracing::info!("Processing Almost Done for user {}", user_id);
            
            // Get episodes that are almost done (75%+ listened and not completed)
            let episodes = sqlx::query("
                SELECT e.EpisodeID
                FROM Episodes e
                JOIN Podcasts p ON e.PodcastID = p.PodcastID
                JOIN UserEpisodeHistory h ON e.EpisodeID = h.EpisodeID AND h.UserID = ?
                WHERE h.ListenDuration > 0 
                  AND h.ListenDuration < e.EpisodeDuration 
                  AND (h.ListenDuration / NULLIF(e.EpisodeDuration, 0)) >= 0.75
                ORDER BY h.ListenDate DESC
            ")
                .bind(user_id)
                .fetch_all(pool)
                .await?;
            
            // Add unique episodes to playlist
            for episode in episodes {
                let episode_id: i32 = episode.try_get("EpisodeID")?;
                if !added_episodes.contains(&episode_id) {
                    sqlx::query("
                        INSERT INTO PlaylistContents (PlaylistID, EpisodeID, Position)
                        VALUES (?, ?, ?)
                    ")
                        .bind(playlist_id)
                        .bind(episode_id)
                        .bind(position)
                        .execute(pool)
                        .await?;
                    
                    added_episodes.insert(episode_id);
                    position += 1;
                }
            }
        }
        
        tracing::info!("Almost Done playlist updated with {} episodes", added_episodes.len());
        Ok(added_episodes.len() as i32)
    }

    // Build and execute playlist query for PostgreSQL - matches Python build_playlist_query exactly
    async fn build_and_execute_playlist_query_postgres(&self, pool: &Pool<Postgres>, playlist: &sqlx::postgres::PgRow) -> AppResult<i32> {
        let playlist_id: i32 = playlist.try_get("playlistid")?;
        let user_id: i32 = playlist.try_get("userid")?;
        let playlist_name: String = playlist.try_get("name")?;
        let is_system_playlist: bool = playlist.try_get("issystemplaylist").unwrap_or(false);
        
        // Parse playlist configuration
        let config = PlaylistConfig::from_postgres_row(playlist)?;
        
        // Determine if this playlist needs user history filtering
        let needs_user_history = playlist_name == "Currently Listening" || 
                                  playlist_name == "Almost Done" || 
                                  !is_system_playlist;
        
        // Check for special optimized queries for partially played
        if config.include_partially_played && !config.include_unplayed && !config.include_played {
            return self.execute_partially_played_query_postgres(pool, playlist_id, user_id, &config).await;
        }
        
        // Build the appropriate base query - FIXED PARAMETER INDEXING
        let (base_query, params) = if is_system_playlist {
            if needs_user_history {
                // System playlist with user history filtering
                (r#"
                    SELECT e.episodeid, p.podcastid, u.timezone
                    FROM "Episodes" e
                    JOIN "Podcasts" p ON e.podcastid = p.podcastid
                    LEFT JOIN "UserEpisodeHistory" h ON e.episodeid = h.episodeid AND h.userid = $2
                    JOIN "Users" u ON u.userid = $3
                    WHERE 1=1
                "#.to_string(), vec![user_id, user_id])
            } else {
                // System playlist without user history filtering (Fresh Releases, etc.)  
                // But still need user context for history when needed
                (r#"
                    SELECT e.episodeid, p.podcastid, u.timezone
                    FROM "Episodes" e
                    JOIN "Podcasts" p ON e.podcastid = p.podcastid
                    LEFT JOIN "UserEpisodeHistory" h ON e.episodeid = h.episodeid AND h.userid = $2
                    JOIN "Users" u ON u.userid = $3
                    WHERE 1=1
                "#.to_string(), vec![user_id, user_id])
            }
        } else {
            // User-specific playlist
            (r#"
                SELECT e.episodeid, p.podcastid, u.timezone
                FROM "Episodes" e
                JOIN "Podcasts" p ON e.podcastid = p.podcastid
                LEFT JOIN "UserEpisodeHistory" h ON e.episodeid = h.episodeid AND h.userid = $2
                JOIN "Users" u ON u.userid = $3
                WHERE p.userid = $4
            "#.to_string(), vec![user_id, user_id, user_id])
        };
        
        // Build the complete query with all filters
        let (complete_query, all_params) = self.build_complete_postgres_query(
            base_query, params, &config, playlist_id
        )?;
        
        // Execute the query and insert episodes
        self.execute_playlist_query_postgres(pool, &complete_query, &all_params, playlist_id).await
    }

    // Build and execute playlist query for MySQL - matches Python build_playlist_query exactly  
    async fn build_and_execute_playlist_query_mysql(&self, pool: &Pool<MySql>, playlist: &sqlx::mysql::MySqlRow) -> AppResult<i32> {
        let playlist_id: i32 = playlist.try_get("PlaylistID")?;
        let user_id: i32 = playlist.try_get("UserID")?;
        let playlist_name: String = playlist.try_get("Name")?;
        let is_system_playlist: bool = playlist.try_get("IsSystemPlaylist").unwrap_or(false);
        
        // Parse playlist configuration
        let config = PlaylistConfig::from_mysql_row(playlist)?;
        
        // Determine if this playlist needs user history filtering
        let needs_user_history = playlist_name == "Currently Listening" || 
                                  playlist_name == "Almost Done" || 
                                  !is_system_playlist;
        
        // Check for special optimized queries for partially played
        if config.include_partially_played && !config.include_unplayed && !config.include_played {
            return self.execute_partially_played_query_mysql(pool, playlist_id, user_id, &config).await;
        }
        
        // Build the appropriate base query
        let (base_query, params) = if is_system_playlist {
            if needs_user_history {
                // System playlist with user history filtering
                ("
                    SELECT e.EpisodeID
                    FROM Episodes e
                    JOIN Podcasts p ON e.PodcastID = p.PodcastID
                    LEFT JOIN UserEpisodeHistory h ON e.EpisodeID = h.EpisodeID AND h.UserID = ?
                    JOIN Users u ON u.UserID = ?
                    WHERE 1=1
                ".to_string(), vec![user_id, user_id])
            } else {
                // System playlist without user history filtering (Fresh Releases, etc.)
                // But still need user context for history when needed
                ("
                    SELECT e.EpisodeID
                    FROM Episodes e
                    JOIN Podcasts p ON e.PodcastID = p.PodcastID
                    LEFT JOIN UserEpisodeHistory h ON e.EpisodeID = h.EpisodeID AND h.UserID = ?
                    JOIN Users u ON u.UserID = ?
                    WHERE 1=1
                ".to_string(), vec![user_id, user_id])
            }
        } else {
            // User-specific playlist
            ("
                SELECT e.EpisodeID
                FROM Episodes e
                JOIN Podcasts p ON e.PodcastID = p.PodcastID
                LEFT JOIN UserEpisodeHistory h ON e.EpisodeID = h.EpisodeID AND h.UserID = ?
                JOIN Users u ON u.UserID = ?
                WHERE p.UserID = ?
            ".to_string(), vec![user_id, user_id, user_id])
        };
        
        // Build the complete query with all filters
        let (complete_query, all_params) = self.build_complete_mysql_query(
            base_query, params, &config, playlist_id
        )?;
        
        // Execute the query and insert episodes
        self.execute_playlist_query_mysql(pool, &complete_query, &all_params, playlist_id).await
    }

    // Execute optimized partially played query for PostgreSQL - FIXED VERSION
    async fn execute_partially_played_query_postgres(&self, pool: &Pool<Postgres>, playlist_id: i32, user_id: i32, config: &PlaylistConfig) -> AppResult<i32> {
        // Use direct INSERT without subquery for this optimized case - no alias scoping issues
        let sort_order = config.get_postgres_sort_order().replace("ORDER BY ", "");
        
        // Build base query
        let mut query = format!(r#"
            INSERT INTO "PlaylistContents" (playlistid, episodeid, position)
            SELECT $1, e.episodeid, ROW_NUMBER() OVER (ORDER BY {}) as position
            FROM "Episodes" e
            JOIN "Podcasts" p ON e.podcastid = p.podcastid
            JOIN "UserEpisodeHistory" h ON e.episodeid = h.episodeid
            WHERE h.listenduration > 0
            AND h.listenduration < e.episodeduration
            AND e.completed = FALSE
            AND e.episodeduration > 0
            AND h.userid = $2
        "#, sort_order);
        
        let params = vec![playlist_id, user_id];
        
        // Add progress filters using hardcoded values instead of parameters to avoid type issues
        if let Some(min_progress) = config.play_progress_min {
            query.push_str(&format!(" AND (h.listenduration::float / NULLIF(e.episodeduration, 0)) >= {}", min_progress / 100.0));
        }
        
        if let Some(max_progress) = config.play_progress_max {
            query.push_str(&format!(" AND (h.listenduration::float / NULLIF(e.episodeduration, 0)) <= {}", max_progress / 100.0));
        }
        
        // Add limit
        if let Some(max_episodes) = config.max_episodes {
            query.push_str(&format!(" LIMIT {}", max_episodes));
        }
        
        tracing::info!("Executing partially played query with {} parameters", params.len());
        tracing::debug!("Query: {}", query);
        
        // Execute with proper parameter binding
        let mut sqlx_query = sqlx::query(&query);
        for param in &params {
            sqlx_query = sqlx_query.bind(*param);
        }
        
        let result = sqlx_query.execute(pool).await?;
        Ok(result.rows_affected() as i32)
    }

    // Execute optimized partially played query for MySQL
    async fn execute_partially_played_query_mysql(&self, pool: &Pool<MySql>, playlist_id: i32, user_id: i32, config: &PlaylistConfig) -> AppResult<i32> {
        // Use direct INSERT without subquery for this optimized case - no alias scoping issues
        let sort_order = config.get_mysql_sort_order().replace("ORDER BY ", "");
        
        let query = format!("
            INSERT INTO PlaylistContents (PlaylistID, EpisodeID, Position)
            SELECT ?, e.EpisodeID, ROW_NUMBER() OVER (ORDER BY {}) as position
            FROM Episodes e
            JOIN Podcasts p ON e.PodcastID = p.PodcastID
            JOIN UserEpisodeHistory h ON e.EpisodeID = h.EpisodeID
            WHERE h.ListenDuration > 0
            AND h.ListenDuration < e.EpisodeDuration
            AND e.Completed = FALSE
            AND e.EpisodeDuration > 0
            AND h.UserID = ?
        ", sort_order);
        
        // For simplicity, execute basic version - full implementation would add all filters
        let result = sqlx::query(&query)
            .bind(playlist_id)
            .bind(user_id)
            .execute(pool)
            .await?;
        
        Ok(result.rows_affected() as i32)
    }

    // Build complete PostgreSQL query with all filters - EXACT PYTHON MATCH
    fn build_complete_postgres_query(&self, base_query: String, params: Vec<i32>, config: &PlaylistConfig, playlist_id: i32) -> AppResult<(String, Vec<i32>)> {
        // Build proper SELECT query first - need to include columns for ordering in subquery
        let mut select_columns = vec![
            "e.episodeid".to_string(),
            "p.podcastid".to_string(),
            "e.episodepubdate".to_string(),
            "e.episodeduration".to_string(),
            "COALESCE(h.listenduration, 0) as listenduration".to_string()
        ];
        
        let mut select_query = base_query.replace(
            "SELECT e.episodeid, p.podcastid, u.timezone",
            &format!("SELECT {}", select_columns.join(", "))
        );
        
        let mut all_params = params;
        let mut param_index = all_params.len() + 2; // +2 because playlist_id will be inserted as $1
        
        // Add podcast filter (PostgreSQL IN clause support)
        if let Some(ref podcast_ids) = config.podcast_ids {
            println!("Playlist {}: Applying podcast filter with IDs: {:?}", playlist_id, podcast_ids);
            if !podcast_ids.is_empty() {
                if podcast_ids.len() == 1 {
                    println!("PostgreSQL single podcast filter: p.podcastid = {}", podcast_ids[0]);
                    select_query.push_str(&format!(" AND p.podcastid = ${}", param_index));
                    all_params.push(podcast_ids[0]);
                    param_index += 1;
                } else {
                    let placeholders: String = (0..podcast_ids.len())
                        .map(|i| format!("${}", param_index + i))
                        .collect::<Vec<_>>()
                        .join(",");
                    println!("PostgreSQL multiple podcast filter: p.podcastid IN ({})", placeholders);
                    select_query.push_str(&format!(" AND p.podcastid IN ({})", placeholders));
                    all_params.extend(podcast_ids);
                    param_index += podcast_ids.len();
                }
            } else {
                // If podcast_ids is Some but empty, user selected specific podcasts but list is empty
                // This should return no results (exclude all podcasts)
                println!("PostgreSQL podcast filter is empty - excluding all podcasts");
                select_query.push_str(" AND FALSE");
            }
        } else {
            println!("PostgreSQL no podcast filter applied - config.podcast_ids is None");
        }
        
        // Add duration filters
        if let Some(min_duration) = config.min_duration {
            select_query.push_str(&format!(" AND e.episodeduration >= ${}", param_index));
            all_params.push(min_duration);
            param_index += 1;
        }
        
        if let Some(max_duration) = config.max_duration {
            select_query.push_str(&format!(" AND e.episodeduration <= ${}", param_index));
            all_params.push(max_duration);
            param_index += 1;
        }
        
        // Add time filter with timezone awareness
        if let Some(time_filter_hours) = config.time_filter_hours {
            select_query.push_str(&format!(
                " AND e.episodepubdate AT TIME ZONE 'UTC' AT TIME ZONE COALESCE(u.timezone, 'UTC') > \
                 (CURRENT_TIMESTAMP AT TIME ZONE 'UTC' AT TIME ZONE COALESCE(u.timezone, 'UTC') - INTERVAL '{}' HOUR)",
                time_filter_hours
            ));
        }
        
        // Add play state filters - EXACT PYTHON LOGIC
        println!("Playlist {}: Applying play state filters - unplayed: {}, partially_played: {}, played: {}", 
                playlist_id, config.include_unplayed, config.include_partially_played, config.include_played);
        let mut play_state_conditions = Vec::new();
        
        if config.include_unplayed {
            play_state_conditions.push("h.listenduration IS NULL".to_string());
            println!("Playlist {}: Added unplayed episode filter", playlist_id);
        }
        
        if config.include_partially_played {
            let mut partial_condition = "(h.listenduration > 0 AND h.listenduration < e.episodeduration AND e.completed = FALSE)".to_string();
            
            if let Some(min_progress) = config.play_progress_min {
                partial_condition.push_str(&format!(" AND (h.listenduration::float / NULLIF(e.episodeduration, 0)) >= {}", min_progress / 100.0));
            }
            
            if let Some(max_progress) = config.play_progress_max {
                partial_condition.push_str(&format!(" AND (h.listenduration::float / NULLIF(e.episodeduration, 0)) <= {}", max_progress / 100.0));
            }
            
            play_state_conditions.push(partial_condition);
        }
        
        if config.include_played {
            play_state_conditions.push("h.listenduration >= e.episodeduration".to_string());
            tracing::debug!("Playlist {}: Added played/completed episode filter", playlist_id);
        }
        
        if !play_state_conditions.is_empty() {
            select_query.push_str(&format!(" AND ({})", play_state_conditions.join(" OR ")));
            tracing::debug!("Playlist {}: Applied play state filter: ({})", playlist_id, play_state_conditions.join(" OR "));
        } else {
            // If no play states are selected, exclude all episodes (return no results)
            select_query.push_str(" AND FALSE");
            tracing::debug!("Playlist {}: No play states selected - excluding all episodes", playlist_id);
        }
        
        // Note: No ORDER BY in inner query - final sorting is handled by ROW_NUMBER() OVER clause
        
        // Add limit
        if let Some(max_episodes) = config.max_episodes {
            select_query.push_str(&format!(" LIMIT {}", max_episodes));
        }
        
        // Now wrap the SELECT query in INSERT with ROW_NUMBER() - FIXED ALIAS SCOPING
        println!("Playlist {}: group_by_podcast setting: {}", playlist_id, config.group_by_podcast);
        let sort_for_insert = if config.group_by_podcast {
            let sort_with_grouping = format!("ORDER BY episodes.podcastid, {}", config.get_postgres_outer_sort_order().replace("ORDER BY ", ""));
            println!("Playlist {}: Using grouped sort: {}", playlist_id, sort_with_grouping);
            sort_with_grouping
        } else {
            let sort_without_grouping = config.get_postgres_outer_sort_order();
            println!("Playlist {}: Using non-grouped sort: {}", playlist_id, sort_without_grouping);
            println!("Playlist {}: Debug - raw sort_order: '{}'", playlist_id, config.sort_order);
            sort_without_grouping
        };
        
        let insert_query = format!(r#"
            INSERT INTO "PlaylistContents" (playlistid, episodeid, position)
            SELECT $1, episodes.episodeid, ROW_NUMBER() OVER ({}) as position
            FROM ({}) episodes
        "#, sort_for_insert, select_query);
        
        println!("Playlist {}: Final insert query: {}", playlist_id, insert_query);
        
        // Final params: playlist_id first, then all query params
        let mut final_params = vec![playlist_id];
        final_params.extend(all_params);
        
        Ok((insert_query, final_params))
    }

    // Build complete MySQL query with all filters - EXACT PYTHON MATCH
    fn build_complete_mysql_query(&self, base_query: String, params: Vec<i32>, config: &PlaylistConfig, playlist_id: i32) -> AppResult<(String, Vec<i32>)> {
        // Build proper SELECT query first - need to include columns for ordering in subquery
        let mut select_columns = vec![
            "e.EpisodeID".to_string(),
            "p.PodcastID".to_string(),
            "e.EpisodePubDate".to_string(),
            "e.EpisodeDuration".to_string(),
            "COALESCE(h.ListenDuration, 0) as ListenDuration".to_string()
        ];
        
        let mut select_query = base_query.replace(
            "SELECT e.EpisodeID",
            &format!("SELECT {}", select_columns.join(", "))
        );
        
        let mut all_params = params;
        
        // Add podcast filter (MySQL JSON/IN support)
        if let Some(ref podcast_ids) = config.podcast_ids {
            if !podcast_ids.is_empty() {
                println!("MySQL applying podcast filter with IDs: {:?}", podcast_ids);
                if podcast_ids.len() == 1 {
                    select_query.push_str(" AND p.PodcastID = ?");
                    all_params.push(podcast_ids[0]);
                    println!("MySQL single podcast filter: p.PodcastID = {}", podcast_ids[0]);
                } else {
                    let placeholders: String = podcast_ids.iter().map(|_| "?").collect::<Vec<_>>().join(",");
                    select_query.push_str(&format!(" AND p.PodcastID IN ({})", placeholders));
                    all_params.extend(podcast_ids);
                    println!("MySQL multiple podcast filter: p.PodcastID IN ({})", placeholders);
                }
            } else {
                // If podcast_ids is Some but empty, user selected specific podcasts but list is empty
                // This should return no results (exclude all podcasts)
                println!("MySQL podcast filter is empty - excluding all podcasts");
                select_query.push_str(" AND FALSE");
            }
        } else {
            println!("MySQL no podcast_ids specified, no filter applied");
        }
        
        // Add duration filters
        if let Some(min_duration) = config.min_duration {
            select_query.push_str(" AND e.EpisodeDuration >= ?");
            all_params.push(min_duration);
        }
        
        if let Some(max_duration) = config.max_duration {
            select_query.push_str(" AND e.EpisodeDuration <= ?");
            all_params.push(max_duration);
        }
        
        // Add time filter with timezone awareness
        if let Some(time_filter_hours) = config.time_filter_hours {
            select_query.push_str(&format!(
                " AND CONVERT_TZ(e.EpisodePubDate, 'UTC', COALESCE(u.TimeZone, 'UTC')) > \
                 DATE_SUB(CONVERT_TZ(NOW(), 'UTC', COALESCE(u.TimeZone, 'UTC')), INTERVAL {} HOUR)",
                time_filter_hours
            ));
        }
        
        // Add play state filters - EXACT PYTHON LOGIC
        let mut play_state_conditions = Vec::new();
        
        if config.include_unplayed {
            play_state_conditions.push("h.ListenDuration IS NULL".to_string());
        }
        
        if config.include_partially_played {
            let mut partial_condition = "(h.ListenDuration > 0 AND h.ListenDuration < e.EpisodeDuration AND e.Completed = FALSE)".to_string();
            
            if let Some(min_progress) = config.play_progress_min {
                partial_condition.push_str(&format!(" AND (h.ListenDuration / NULLIF(e.EpisodeDuration, 0)) >= {}", min_progress / 100.0));
            }
            
            if let Some(max_progress) = config.play_progress_max {
                partial_condition.push_str(&format!(" AND (h.ListenDuration / NULLIF(e.EpisodeDuration, 0)) <= {}", max_progress / 100.0));
            }
            
            play_state_conditions.push(partial_condition);
        }
        
        if config.include_played {
            play_state_conditions.push("h.ListenDuration >= e.EpisodeDuration".to_string());
        }
        
        if !play_state_conditions.is_empty() {
            select_query.push_str(&format!(" AND ({})", play_state_conditions.join(" OR ")));
        } else {
            // If no play states are selected, exclude all episodes (return no results)
            select_query.push_str(" AND FALSE");
        }
        
        // Note: No ORDER BY in inner query - final sorting is handled by ROW_NUMBER() OVER clause
        
        // Add limit
        if let Some(max_episodes) = config.max_episodes {
            select_query.push_str(&format!(" LIMIT {}", max_episodes));
        }
        
        // Now wrap the SELECT query in INSERT with ROW_NUMBER() - FIXED ALIAS SCOPING
        println!("Playlist {}: group_by_podcast setting: {}", playlist_id, config.group_by_podcast);
        let sort_for_insert = if config.group_by_podcast {
            let sort_with_grouping = format!("ORDER BY episodes.PodcastID, {}", config.get_mysql_outer_sort_order().replace("ORDER BY ", ""));
            println!("Playlist {}: Using grouped sort: {}", playlist_id, sort_with_grouping);
            sort_with_grouping
        } else {
            let sort_without_grouping = config.get_mysql_outer_sort_order();
            println!("Playlist {}: Using non-grouped sort: {}", playlist_id, sort_without_grouping);
            sort_without_grouping
        };
        
        let insert_query = format!(r#"
            INSERT INTO PlaylistContents (PlaylistID, EpisodeID, Position)
            SELECT ?, episodes.EpisodeID, ROW_NUMBER() OVER ({}) as position
            FROM ({}) episodes
        "#, sort_for_insert, select_query);
        
        // Final params: playlist_id first, then all query params
        let mut final_params = vec![playlist_id];
        final_params.extend(all_params);
        
        Ok((insert_query, final_params))
    }

    // Add play state filters for PostgreSQL
    fn add_play_state_filters_postgres(&self, query: &mut String, params: &mut Vec<i32>, param_index: &mut usize, config: &PlaylistConfig) -> AppResult<()> {
        let mut play_state_conditions = Vec::new();
        
        if config.include_unplayed {
            play_state_conditions.push("h.listenduration IS NULL".to_string());
        }
        
        if config.include_partially_played {
            let mut partial_condition = "(h.listenduration > 0 AND h.listenduration < e.episodeduration AND e.completed = FALSE)".to_string();
            
            if let Some(min_progress) = config.play_progress_min {
                partial_condition.push_str(&format!(" AND (h.listenduration::float / NULLIF(e.episodeduration, 0)) >= {}", min_progress / 100.0));
            }
            
            if let Some(max_progress) = config.play_progress_max {
                partial_condition.push_str(&format!(" AND (h.listenduration::float / NULLIF(e.episodeduration, 0)) <= {}", max_progress / 100.0));
            }
            
            play_state_conditions.push(partial_condition);
        }
        
        if config.include_played {
            play_state_conditions.push("h.listenduration >= e.episodeduration".to_string());
        }
        
        if !play_state_conditions.is_empty() {
            query.push_str(&format!(" AND ({})", play_state_conditions.join(" OR ")));
        }
        
        Ok(())
    }

    // Add play state filters for MySQL
    fn add_play_state_filters_mysql(&self, query: &mut String, config: &PlaylistConfig) -> AppResult<()> {
        let mut play_state_conditions = Vec::new();
        
        if config.include_unplayed {
            play_state_conditions.push("h.ListenDuration IS NULL".to_string());
        }
        
        if config.include_partially_played {
            let mut partial_condition = "(h.ListenDuration > 0 AND h.ListenDuration < e.EpisodeDuration AND e.Completed = FALSE)".to_string();
            
            if let Some(min_progress) = config.play_progress_min {
                partial_condition.push_str(&format!(" AND (h.ListenDuration / NULLIF(e.EpisodeDuration, 0)) >= {}", min_progress / 100.0));
            }
            
            if let Some(max_progress) = config.play_progress_max {
                partial_condition.push_str(&format!(" AND (h.ListenDuration / NULLIF(e.EpisodeDuration, 0)) <= {}", max_progress / 100.0));
            }
            
            play_state_conditions.push(partial_condition);
        }
        
        if config.include_played {
            play_state_conditions.push("h.ListenDuration >= e.EpisodeDuration".to_string());
        }
        
        if !play_state_conditions.is_empty() {
            query.push_str(&format!(" AND ({})", play_state_conditions.join(" OR ")));
        }
        
        Ok(())
    }

    // Add common filters for PostgreSQL (simplified helper)
    fn add_common_filters_postgres(&self, _query: &mut String, _params: &mut Vec<Box<dyn sqlx::Encode<'_, Postgres> + Send + 'static>>, _param_index: &mut usize, _config: &PlaylistConfig, _user_id: i32) -> AppResult<()> {
        // Implementation for duration, time, podcast filters
        // Simplified for now due to complexity of dynamic parameters
        Ok(())
    }

    // Helper function to parse categories JSON string into HashMap - matches Python version
    fn parse_categories_json(&self, categories_str: &str) -> Option<std::collections::HashMap<String, String>> {
        if categories_str.is_empty() {
            return Some(std::collections::HashMap::new());
        }
        
        if categories_str.starts_with('{') {
            // Try to parse as JSON first
            if let Ok(parsed) = serde_json::from_str::<std::collections::HashMap<String, String>>(categories_str) {
                return Some(parsed);
            }
        } else {
            // Fall back to comma-separated parsing like Python version
            let mut result = std::collections::HashMap::new();
            for (i, cat) in categories_str.split(',').enumerate() {
                result.insert(i.to_string(), cat.trim().to_string());
            }
            return Some(result);
        }
        
        // Return empty map if parsing fails
        Some(std::collections::HashMap::new())
    }

    // Execute the final playlist query for PostgreSQL - FIXED VERSION
    async fn execute_playlist_query_postgres(&self, pool: &Pool<Postgres>, query: &str, params: &[i32], _playlist_id: i32) -> AppResult<i32> {
        println!("PostgreSQL executing playlist query with {} parameters", params.len());
        println!("PostgreSQL Query: {}", query);
        println!("PostgreSQL Params: {:?}", params);
        
        // Build query with proper parameter binding
        let mut sqlx_query = sqlx::query(query);
        for param in params {
            sqlx_query = sqlx_query.bind(*param);
        }
        
        let result = sqlx_query.execute(pool).await?;
        println!("PostgreSQL playlist query affected {} rows", result.rows_affected());
        Ok(result.rows_affected() as i32)
    }

    // Execute the final playlist query for MySQL
    async fn execute_playlist_query_mysql(&self, pool: &Pool<MySql>, query: &str, params: &[i32], _playlist_id: i32) -> AppResult<i32> {
        tracing::info!("Executing MySQL playlist query with {} parameters", params.len());
        tracing::debug!("Query: {}", query);
        tracing::debug!("Params: {:?}", params);
        
        // Build query with proper parameter binding
        let mut sqlx_query = sqlx::query(query);
        for param in params {
            sqlx_query = sqlx_query.bind(*param);
        }
        
        let result = sqlx_query.execute(pool)
            .await?;
        
        Ok(result.rows_affected() as i32)
    }

    // Get podcast details - matches Python get_podcast_details function exactly
    pub async fn get_podcast_details(&self, user_id: i32, podcast_id: i32) -> AppResult<serde_json::Value> {
        match self {
            DatabasePool::Postgres(pool) => {
                // First try to get podcast for specific user
                let mut podcast_row = sqlx::query(r#"
                    SELECT podcastid, podcastname, feedurl, description, author, artworkurl, 
                           explicit, episodecount, categories, websiteurl, podcastindexid, isyoutubechannel,
                           userid, autodownload, startskip, endskip, username, password, notificationsenabled, feedcutoffdays,
                           playbackspeed, playbackspeedcustomized
                    FROM "Podcasts" 
                    WHERE podcastid = $1 AND userid = $2
                "#)
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                // Fallback to admin/public podcasts (userid = 1) if not found for user
                if podcast_row.is_none() {
                    podcast_row = sqlx::query(r#"
                        SELECT podcastid, podcastname, feedurl, description, author, artworkurl, 
                               explicit, episodecount, categories, websiteurl, podcastindexid, isyoutubechannel,
                               userid, autodownload, startskip, endskip, username, password, notificationsenabled, feedcutoffdays,
                               playbackspeed, playbackspeedcustomized
                        FROM "Podcasts" 
                        WHERE podcastid = $1 AND userid = 1
                    "#)
                        .bind(podcast_id)
                        .fetch_optional(pool)
                        .await?;
                }

                let row = podcast_row.ok_or_else(|| AppError::not_found("Podcast not found"))?;

                // Get episode count (special handling for YouTube channels)
                let is_youtube: bool = row.try_get("isyoutubechannel").unwrap_or(false);
                let episode_count = if is_youtube {
                    // Get count from YouTubeVideos table for YouTube channels
                    let count_row = sqlx::query(r#"SELECT COUNT(*) as count FROM "YouTubeVideos" WHERE podcastid = $1"#)
                        .bind(podcast_id)
                        .fetch_one(pool)
                        .await?;
                    count_row.try_get::<i64, _>("count")? as i32
                } else {
                    row.try_get("episodecount").unwrap_or(0)
                };

                // Get categories and parse to HashMap - matches Python version exactly
                let categories_str = row.try_get::<String, _>("categories").unwrap_or_else(|_| String::new());
                let categories = self.parse_categories_json(&categories_str);

                Ok(serde_json::json!({
                    "podcastid": row.try_get::<i32, _>("podcastid")?,
                    "podcastindexid": row.try_get::<i32, _>("podcastindexid").unwrap_or(0),
                    "podcastname": row.try_get::<String, _>("podcastname").unwrap_or_else(|_| "Unknown Podcast".to_string()),
                    "artworkurl": row.try_get::<Option<String>, _>("artworkurl").unwrap_or_default().unwrap_or_else(|| String::new()),
                    "author": row.try_get::<String, _>("author").unwrap_or_else(|_| "Unknown Author".to_string()),
                    "categories": categories,
                    "description": row.try_get::<String, _>("description").unwrap_or_else(|_| String::new()),
                    "episodecount": episode_count,
                    "feedurl": row.try_get::<String, _>("feedurl").unwrap_or_else(|_| String::new()),
                    "websiteurl": row.try_get::<String, _>("websiteurl").unwrap_or_else(|_| String::new()),
                    "explicit": row.try_get::<bool, _>("explicit").unwrap_or(false),
                    "userid": row.try_get::<i32, _>("userid")?,
                    "autodownload": row.try_get::<bool, _>("autodownload").unwrap_or(false),
                    "startskip": row.try_get::<i32, _>("startskip").unwrap_or(0),
                    "endskip": row.try_get::<i32, _>("endskip").unwrap_or(0),
                    "username": row.try_get::<Option<String>, _>("username")?,
                    "password": row.try_get::<Option<String>, _>("password")?,
                    "isyoutubechannel": is_youtube,
                    "notificationsenabled": row.try_get::<bool, _>("notificationsenabled").unwrap_or(false),
                    "feedcutoffdays": row.try_get::<i32, _>("feedcutoffdays").unwrap_or(0),
                    "playbackspeedcustomized": row.try_get::<bool, _>("playbackspeedcustomized").unwrap_or(false),
                    "playbackspeed": row.try_get::<f64, _>("playbackspeed").unwrap_or(1.0)
                }))
            }
            DatabasePool::MySQL(pool) => {
                // First try to get podcast for specific user
                let mut podcast_row = sqlx::query(r#"
                    SELECT PodcastID, PodcastName, FeedURL, Description, Author, ArtworkURL, 
                           Explicit, EpisodeCount, Categories, WebsiteURL, PodcastIndexID, IsYouTubeChannel,
                           UserID, AutoDownload, StartSkip, EndSkip, Username, Password, NotificationsEnabled, FeedCutoffDays,
                           PlaybackSpeed, PlaybackSpeedCustomized
                    FROM Podcasts 
                    WHERE PodcastID = ? AND UserID = ?
                "#)
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                // Fallback to admin/public podcasts (UserID = 1) if not found for user
                if podcast_row.is_none() {
                    podcast_row = sqlx::query(r#"
                        SELECT PodcastID, PodcastName, FeedURL, Description, Author, ArtworkURL, 
                               Explicit, EpisodeCount, Categories, WebsiteURL, PodcastIndexID, IsYouTubeChannel,
                               UserID, AutoDownload, StartSkip, EndSkip, Username, Password, NotificationsEnabled, FeedCutoffDays,
                               PlaybackSpeed, PlaybackSpeedCustomized
                        FROM Podcasts 
                        WHERE PodcastID = ? AND UserID = 1
                    "#)
                        .bind(podcast_id)
                        .fetch_optional(pool)
                        .await?;
                }

                let row = podcast_row.ok_or_else(|| AppError::not_found("Podcast not found"))?;

                // Get episode count (special handling for YouTube channels)
                let is_youtube: bool = row.try_get("IsYouTubeChannel").unwrap_or(false);
                let episode_count = if is_youtube {
                    // Get count from YouTubeVideos table for YouTube channels
                    let count_row = sqlx::query("SELECT COUNT(*) as count FROM YouTubeVideos WHERE PodcastID = ?")
                        .bind(podcast_id)
                        .fetch_one(pool)
                        .await?;
                    count_row.try_get::<i64, _>("count")? as i32
                } else {
                    row.try_get("EpisodeCount").unwrap_or(0)
                };

                // Get categories and parse to HashMap - matches Python version exactly
                let categories_str = row.try_get::<String, _>("Categories").unwrap_or_else(|_| String::new());
                let categories = self.parse_categories_json(&categories_str);

                Ok(serde_json::json!({
                    "podcastid": row.try_get::<i32, _>("PodcastID")?,
                    "podcastindexid": row.try_get::<i32, _>("PodcastIndexID").unwrap_or(0),
                    "podcastname": row.try_get::<String, _>("PodcastName").unwrap_or_else(|_| "Unknown Podcast".to_string()),
                    "artworkurl": row.try_get::<Option<String>, _>("ArtworkURL").unwrap_or_default().unwrap_or_else(|| String::new()),
                    "author": row.try_get::<String, _>("Author").unwrap_or_else(|_| "Unknown Author".to_string()),
                    "categories": categories,
                    "description": row.try_get::<String, _>("Description").unwrap_or_else(|_| String::new()),
                    "episodecount": episode_count,
                    "feedurl": row.try_get::<String, _>("FeedURL").unwrap_or_else(|_| String::new()),
                    "websiteurl": row.try_get::<String, _>("WebsiteURL").unwrap_or_else(|_| String::new()),
                    "explicit": row.try_get::<i8, _>("Explicit").unwrap_or(0) != 0,
                    "userid": row.try_get::<i32, _>("UserID")?,
                    "autodownload": row.try_get::<i8, _>("AutoDownload").unwrap_or(0) != 0,
                    "startskip": row.try_get::<i32, _>("StartSkip").unwrap_or(0),
                    "endskip": row.try_get::<i32, _>("EndSkip").unwrap_or(0),
                    "username": row.try_get::<Option<String>, _>("Username")?,
                    "password": row.try_get::<Option<String>, _>("Password")?,
                    "isyoutubechannel": is_youtube,
                    "notificationsenabled": row.try_get::<i8, _>("NotificationsEnabled").unwrap_or(0) != 0,
                    "feedcutoffdays": row.try_get::<i32, _>("FeedCutoffDays").unwrap_or(0),
                    "playbackspeedcustomized": row.try_get::<i8, _>("PlaybackSpeedCustomized").unwrap_or(0) != 0,
                    "playbackspeed": if let Ok(speed) = row.try_get::<bigdecimal::BigDecimal, _>("PlaybackSpeed") {
                        speed.to_f64().unwrap_or(1.0)
                    } else {
                        1.0
                    }
                }))
            }
        }
    }

    // OIDC Provider Management - matches Python OIDC functions

    // Get OIDC provider by client ID - for callback processing
    pub async fn get_oidc_provider_by_client_id(&self, client_id: &str) -> AppResult<Option<serde_json::Value>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"
                    SELECT providerid, providername, clientid, clientsecret, authorizationurl, 
                           tokenurl, userinfourl, scope, buttoncolor, buttontext, buttontextcolor, 
                           iconsvg, enabled, nameclaim, emailclaim, usernameclaim, rolesclaim, 
                           userrole, adminrole
                    FROM "OIDCProviders" 
                    WHERE clientid = $1 AND enabled = true
                "#)
                    .bind(client_id)
                    .fetch_optional(pool)
                    .await?;

                if let Some(row) = row {
                    Ok(Some(serde_json::json!({
                        "provider_id": row.try_get::<i32, _>("providerid")?,
                        "provider_name": row.try_get::<String, _>("providername")?,
                        "client_id": row.try_get::<String, _>("clientid")?,
                        "client_secret": row.try_get::<String, _>("clientsecret")?,
                        "authorization_url": row.try_get::<String, _>("authorizationurl")?,
                        "token_url": row.try_get::<String, _>("tokenurl")?,
                        "userinfo_url": row.try_get::<String, _>("userinfourl")?,
                        "scope": row.try_get::<String, _>("scope")?,
                        "button_color": row.try_get::<String, _>("buttoncolor")?,
                        "button_text": row.try_get::<String, _>("buttontext")?,
                        "button_text_color": row.try_get::<String, _>("buttontextcolor")?,
                        "icon_svg": row.try_get::<Option<String>, _>("iconsvg")?,
                        "enabled": row.try_get::<bool, _>("enabled")?,
                        "name_claim": row.try_get::<Option<String>, _>("nameclaim")?,
                        "email_claim": row.try_get::<Option<String>, _>("emailclaim")?,
                        "username_claim": row.try_get::<Option<String>, _>("usernameclaim")?,
                        "roles_claim": row.try_get::<Option<String>, _>("rolesclaim")?,
                        "user_role": row.try_get::<Option<String>, _>("userrole")?,
                        "admin_role": row.try_get::<Option<String>, _>("adminrole")?
                    })))
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query(r#"
                    SELECT ProviderID, ProviderName, ClientID, ClientSecret, AuthorizationURL, 
                           TokenURL, UserInfoURL, Scope, ButtonColor, ButtonText, ButtonTextColor, 
                           IconSVG, Enabled, NameClaim, EmailClaim, UsernameClaim, RolesClaim, 
                           UserRole, AdminRole
                    FROM OIDCProviders 
                    WHERE ClientID = ? AND Enabled = true
                "#)
                    .bind(client_id)
                    .fetch_optional(pool)
                    .await?;

                if let Some(row) = row {
                    Ok(Some(serde_json::json!({
                        "provider_id": row.try_get::<i32, _>("ProviderID")?,
                        "provider_name": row.try_get::<String, _>("ProviderName")?,
                        "client_id": row.try_get::<String, _>("ClientID")?,
                        "client_secret": row.try_get::<String, _>("ClientSecret")?,
                        "authorization_url": row.try_get::<String, _>("AuthorizationURL")?,
                        "token_url": row.try_get::<String, _>("TokenURL")?,
                        "userinfo_url": row.try_get::<String, _>("UserInfoURL")?,
                        "scope": row.try_get::<String, _>("Scope")?,
                        "button_color": row.try_get::<String, _>("ButtonColor")?,
                        "button_text": row.try_get::<String, _>("ButtonText")?,
                        "button_text_color": row.try_get::<String, _>("ButtonTextColor")?,
                        "icon_svg": row.try_get::<Option<String>, _>("IconSVG")?,
                        "enabled": row.try_get::<i8, _>("Enabled")? != 0,
                        "name_claim": row.try_get::<Option<String>, _>("NameClaim")?,
                        "email_claim": row.try_get::<Option<String>, _>("EmailClaim")?,
                        "username_claim": row.try_get::<Option<String>, _>("UsernameClaim")?,
                        "roles_claim": row.try_get::<Option<String>, _>("RolesClaim")?,
                        "user_role": row.try_get::<Option<String>, _>("UserRole")?,
                        "admin_role": row.try_get::<Option<String>, _>("AdminRole")?
                    })))
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Get OIDC provider - matches Python get_oidc_provider function EXACTLY
    pub async fn get_oidc_provider(&self, client_id: &str) -> AppResult<Option<(i32, String, String, String, String, Option<String>, Option<String>, Option<String>, Option<String>, Option<String>, Option<String>)>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"
                    SELECT providerid, clientid, clientsecret, tokenurl, userinfourl, nameclaim, emailclaim, usernameclaim, rolesclaim, userrole, adminrole
                    FROM "OIDCProviders"
                    WHERE clientid = $1 AND enabled = true
                "#)
                    .bind(client_id)
                    .fetch_optional(pool)
                    .await?;

                if let Some(row) = result {
                    Ok(Some((
                        row.try_get("providerid")?,
                        row.try_get("clientid")?,
                        row.try_get("clientsecret")?,
                        row.try_get("tokenurl")?,
                        row.try_get("userinfourl")?,
                        row.try_get("nameclaim")?,
                        row.try_get("emailclaim")?,
                        row.try_get("usernameclaim")?,
                        row.try_get("rolesclaim")?,
                        row.try_get("userrole")?,
                        row.try_get("adminrole")?,
                    )))
                } else {
                    Ok(None)
                }
            },
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query(r#"
                    SELECT ProviderID, ClientID, ClientSecret, TokenURL, UserInfoURL, NameClaim, EmailClaim, UsernameClaim, RolesClaim, UserRole, AdminRole
                    FROM OIDCProviders
                    WHERE ClientID = ? AND Enabled = true
                "#)
                    .bind(client_id)
                    .fetch_optional(pool)
                    .await?;

                if let Some(row) = result {
                    Ok(Some((
                        row.try_get("ProviderID")?,
                        row.try_get("ClientID")?,
                        row.try_get("ClientSecret")?,
                        row.try_get("TokenURL")?,
                        row.try_get("UserInfoURL")?,
                        row.try_get("NameClaim")?,
                        row.try_get("EmailClaim")?,
                        row.try_get("UsernameClaim")?,
                        row.try_get("RolesClaim")?,
                        row.try_get("UserRole")?,
                        row.try_get("AdminRole")?,
                    )))
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Get user by email - matches Python get_user_by_email function EXACTLY  
    pub async fn get_user_by_email(&self, email: &str) -> AppResult<Option<(i32, String, Option<String>, Option<String>, bool)>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"
                    SELECT userid, email, username, fullname, isadmin
                    FROM "Users"
                    WHERE email = $1
                "#)
                    .bind(email)
                    .fetch_optional(pool)
                    .await?;

                if let Some(row) = result {
                    Ok(Some((
                        row.try_get("userid")?,
                        row.try_get("email")?,
                        row.try_get("username")?,
                        row.try_get("fullname")?,
                        row.try_get("isadmin")?,
                    )))
                } else {
                    Ok(None)
                }
            },
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query(r#"
                    SELECT UserID, Email, Username, Fullname, IsAdmin
                    FROM Users
                    WHERE Email = ?
                "#)
                    .bind(email)
                    .fetch_optional(pool)
                    .await?;

                if let Some(row) = result {
                    let is_admin: i32 = row.try_get("IsAdmin")?;
                    Ok(Some((
                        row.try_get("UserID")?,
                        row.try_get("Email")?,
                        row.try_get("Username")?,
                        row.try_get("Fullname")?,
                        is_admin != 0,
                    )))
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Check if username exists - matches Python check_usernames function EXACTLY
    pub async fn check_usernames(&self, username: &str) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"SELECT COUNT(*) as count FROM "Users" WHERE username = $1"#)
                    .bind(username)
                    .fetch_one(pool)
                    .await?;
                let count: i64 = result.try_get("count")?;
                Ok(count > 0)
            },
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query(r#"SELECT COUNT(*) as count FROM Users WHERE Username = ?"#)
                    .bind(username)
                    .fetch_one(pool)
                    .await?;
                let count: i64 = result.try_get("count")?;
                Ok(count > 0)
            }
        }
    }

    // Get user API key - matches Python get_user_api_key function EXACTLY
    pub async fn get_user_api_key(&self, user_id: i32) -> AppResult<Option<String>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"
                    SELECT apikey FROM "APIKeys" 
                    WHERE userid = $1 
                    ORDER BY created DESC 
                    LIMIT 1
                "#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                if let Some(row) = result {
                    Ok(Some(row.try_get("apikey")?))
                } else {
                    Ok(None)
                }
            },
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query(r#"
                    SELECT APIKey FROM APIKeys 
                    WHERE UserID = ? 
                    ORDER BY Created DESC 
                    LIMIT 1
                "#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                if let Some(row) = result {
                    Ok(Some(row.try_get("APIKey")?))
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Create OIDC user - matches Python create_oidc_user function EXACTLY
    pub async fn create_oidc_user(&self, email: &str, fullname: &str, username: &str) -> AppResult<i32> {
        use base64::{Engine as _, engine::general_purpose::STANDARD};
        use rand::Rng;
        
        // Create salt exactly like Python version
        let salt_bytes: [u8; 16] = rand::rng().random();
        let salt = STANDARD.encode(salt_bytes);
        let hashed_password = format!("$argon2id$v=19$m=65536,t=3,p=4${}${}_OIDC_ACCOUNT_NO_PASSWORD", 
                                    salt, "X".repeat(43));

        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"
                    INSERT INTO "Users" (fullname, username, email, hashed_pw, isadmin)
                    VALUES ($1, $2, $3, $4, false)
                    RETURNING userid
                "#)
                    .bind(fullname)
                    .bind(username)
                    .bind(email)
                    .bind(&hashed_password)
                    .fetch_one(pool)
                    .await?;

                let user_id: i32 = result.try_get("userid")?;

                // Add default user settings
                sqlx::query(r#"INSERT INTO "UserSettings" (userid, theme) VALUES ($1, $2)"#)
                    .bind(user_id)
                    .bind("Nordic")
                    .execute(pool)
                    .await?;

                // Add default user stats
                sqlx::query(r#"INSERT INTO "UserStats" (userid) VALUES ($1)"#)
                    .bind(user_id)
                    .execute(pool)
                    .await?;

                Ok(user_id)
            },
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query(r#"
                    INSERT INTO Users (Fullname, Username, Email, Hashed_PW, IsAdmin)
                    VALUES (?, ?, ?, ?, 0)
                "#)
                    .bind(fullname)
                    .bind(username)
                    .bind(email)
                    .bind(&hashed_password)
                    .execute(pool)
                    .await?;

                let user_id = result.last_insert_id() as i32;

                // Add default user settings
                sqlx::query(r#"INSERT INTO UserSettings (UserID, Theme) VALUES (?, ?)"#)
                    .bind(user_id)
                    .bind("Nordic")
                    .execute(pool)
                    .await?;

                // Add default user stats
                sqlx::query(r#"INSERT INTO UserStats (UserID) VALUES (?)"#)
                    .bind(user_id)
                    .execute(pool)
                    .await?;

                Ok(user_id)
            }
        }
    }



    // Check if username exists - helper for OIDC user creation
    pub async fn username_exists(&self, username: &str) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"SELECT 1 FROM "Users" WHERE username = $1 LIMIT 1"#)
                    .bind(username)
                    .fetch_optional(pool)
                    .await?;
                Ok(result.is_some())
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("SELECT 1 FROM Users WHERE Username = ? LIMIT 1")
                    .bind(username)
                    .fetch_optional(pool)
                    .await?;
                Ok(result.is_some())
            }
        }
    }

    // Create or get API key for user - for OIDC login completion
    pub async fn create_or_get_api_key(&self, user_id: i32) -> AppResult<String> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Check for existing API key
                let existing_key = sqlx::query(r#"SELECT apikey FROM "APIKeys" WHERE userid = $1 LIMIT 1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                if let Some(row) = existing_key {
                    return Ok(row.try_get("apikey")?);
                }

                // Generate new API key
                let api_key = format!("pk_{}", uuid::Uuid::new_v4().simple());
                
                sqlx::query(r#"INSERT INTO "APIKeys" (userid, apikey) VALUES ($1, $2)"#)
                    .bind(user_id)
                    .bind(&api_key)
                    .execute(pool)
                    .await?;

                Ok(api_key)
            }
            DatabasePool::MySQL(pool) => {
                let existing_key = sqlx::query("SELECT APIKey FROM APIKeys WHERE UserID = ? LIMIT 1")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                if let Some(row) = existing_key {
                    return Ok(row.try_get("APIKey")?);
                }

                let api_key = format!("pk_{}", uuid::Uuid::new_v4().simple());
                
                sqlx::query("INSERT INTO APIKeys (UserID, APIKey) VALUES (?, ?)")
                    .bind(user_id)
                    .bind(&api_key)
                    .execute(pool)
                    .await?;

                Ok(api_key)
            }
        }
    }

    // Get playlist episodes - matches Python get_playlist_episodes function exactly
    pub async fn get_playlist_episodes(&self, user_id: i32, playlist_id: i32) -> AppResult<serde_json::Value> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Get playlist info with episode count - matches Python exactly
                let playlist_row = sqlx::query(r#"
                    SELECT
                        p.name,
                        p.description,
                        (SELECT COUNT(*)
                            FROM "PlaylistContents" pc
                            JOIN "Episodes" e ON pc.episodeid = e.episodeid
                            JOIN "Podcasts" pod ON e.podcastid = pod.podcastid
                            LEFT JOIN "UserEpisodeHistory" h ON e.episodeid = h.episodeid AND h.userid = $1
                            WHERE pc.playlistid = p.playlistid
                            AND (p.issystemplaylist = FALSE OR
                                (p.issystemplaylist = TRUE AND
                                (h.episodeid IS NOT NULL OR pod.userid = $2)))
                        ) as episode_count,
                        p.iconname,
                        p.issystemplaylist
                    FROM "Playlists" p
                    WHERE p.playlistid = $3 AND (p.userid = $4 OR p.issystemplaylist = TRUE)
                "#)
                    .bind(user_id)
                    .bind(user_id)
                    .bind(playlist_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                if playlist_row.is_none() {
                    return Err(AppError::not_found("Playlist not found"));
                }

                let row = playlist_row.unwrap();
                let playlist_name: String = row.try_get("name")?;
                let playlist_description: String = row.try_get("description").unwrap_or_default();
                let episode_count: i64 = row.try_get("episodecount")?;
                let icon_name: String = row.try_get("iconname").unwrap_or_default();
                let is_system_playlist: bool = row.try_get("issystemplaylist")?;

                let episodes_rows = if is_system_playlist {
                    // For system playlists, update playlist first to get current episodes, then filter to user's podcasts
                    self.update_playlist_contents(playlist_id).await?;
                    
                    // Same query as user playlists but with additional filter for user's podcasts
                    sqlx::query(r#"
                        SELECT DISTINCT
                            "Episodes".episodeid,
                            "Episodes".episodetitle,
                            "Episodes".episodepubdate,
                            "Episodes".episodedescription,
                            CASE 
                                WHEN "Podcasts".usepodcastcoverscustomized = TRUE AND "Podcasts".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                WHEN "Users".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                ELSE "Episodes".episodeartwork
                            END as episodeartwork,
                            "Episodes".episodeurl,
                            "Episodes".episodeduration,
                            "Episodes".completed,
                            "Podcasts".podcastname,
                            "Podcasts".podcastid,
                            "Podcasts".isyoutubechannel as is_youtube,
                            "UserEpisodeHistory".listenduration,
                            CASE WHEN "SavedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN "EpisodeQueue".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN "DownloadedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            "PlaylistContents".dateadded
                        FROM "PlaylistContents"
                        JOIN "Episodes" ON "PlaylistContents".episodeid = "Episodes".episodeid
                        JOIN "Podcasts" ON "Episodes".podcastid = "Podcasts".podcastid
                        LEFT JOIN "Users" ON "Podcasts".userid = "Users".userid
                        LEFT JOIN "UserEpisodeHistory" ON "Episodes".episodeid = "UserEpisodeHistory".episodeid
                            AND "UserEpisodeHistory".userid = $1
                        LEFT JOIN "SavedEpisodes" ON "Episodes".episodeid = "SavedEpisodes".episodeid
                            AND "SavedEpisodes".userid = $1
                        LEFT JOIN "EpisodeQueue" ON "Episodes".episodeid = "EpisodeQueue".episodeid
                            AND "EpisodeQueue".userid = $1
                            AND "EpisodeQueue".is_youtube = FALSE
                        LEFT JOIN "DownloadedEpisodes" ON "Episodes".episodeid = "DownloadedEpisodes".episodeid
                            AND "DownloadedEpisodes".userid = $1
                        WHERE "PlaylistContents".playlistid = $2 
                            AND "Podcasts".userid = $1
                        ORDER BY "PlaylistContents".dateadded DESC
                    "#)
                        .bind(user_id)
                        .bind(playlist_id)
                        .fetch_all(pool)
                        .await?
                } else {
                    // For user playlists, use existing PlaylistContents logic
                    sqlx::query(r#"
                        SELECT DISTINCT
                            "Episodes".episodeid,
                            "Episodes".episodetitle,
                            "Episodes".episodepubdate,
                            "Episodes".episodedescription,
                            CASE 
                                WHEN "Podcasts".usepodcastcoverscustomized = TRUE AND "Podcasts".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                WHEN "Users".usepodcastcovers = TRUE THEN "Podcasts".artworkurl
                                ELSE "Episodes".episodeartwork
                            END as episodeartwork,
                            "Episodes".episodeurl,
                            "Episodes".episodeduration,
                            "Episodes".completed,
                            "Podcasts".podcastname,
                            "Podcasts".podcastid,
                            "Podcasts".isyoutubechannel as is_youtube,
                            "UserEpisodeHistory".listenduration,
                            CASE WHEN "SavedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS saved,
                            CASE WHEN "EpisodeQueue".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS queued,
                            CASE WHEN "DownloadedEpisodes".episodeid IS NOT NULL THEN TRUE ELSE FALSE END AS downloaded,
                            "PlaylistContents".dateadded
                        FROM "PlaylistContents"
                        JOIN "Episodes" ON "PlaylistContents".episodeid = "Episodes".episodeid
                        JOIN "Podcasts" ON "Episodes".podcastid = "Podcasts".podcastid
                        LEFT JOIN "Users" ON "Podcasts".userid = "Users".userid
                        LEFT JOIN "UserEpisodeHistory" ON "Episodes".episodeid = "UserEpisodeHistory".episodeid
                            AND "UserEpisodeHistory".userid = $1
                        LEFT JOIN "SavedEpisodes" ON "Episodes".episodeid = "SavedEpisodes".episodeid
                            AND "SavedEpisodes".userid = $1
                        LEFT JOIN "EpisodeQueue" ON "Episodes".episodeid = "EpisodeQueue".episodeid
                            AND "EpisodeQueue".userid = $1
                            AND "EpisodeQueue".is_youtube = FALSE
                        LEFT JOIN "DownloadedEpisodes" ON "Episodes".episodeid = "DownloadedEpisodes".episodeid
                            AND "DownloadedEpisodes".userid = $1
                        WHERE "PlaylistContents".playlistid = $2
                        ORDER BY "PlaylistContents".dateadded DESC
                    "#)
                        .bind(user_id)
                        .bind(playlist_id)
                        .fetch_all(pool)
                        .await?
                };

                let mut episodes = Vec::new();
                for row in episodes_rows {
                    let episodeid: i32 = row.try_get("episodeid")?;
                    let episodetitle: String = row.try_get("episodetitle")?;
                    let naive = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                    let episodepubdate = naive.format("%Y-%m-%dT%H:%M:%S").to_string();
                    let episodedescription: String = row.try_get("episodedescription")?;
                    let episodeartwork: String = row.try_get("episodeartwork")?;
                    let episodeurl: String = row.try_get("episodeurl")?;
                    let episodeduration: i32 = row.try_get("episodeduration")?;
                    let completed: bool = row.try_get("completed")?;
                    let podcastname: String = row.try_get("podcastname")?;
                    let podcastid: i32 = row.try_get("podcastid")?;
                    let is_youtube: bool = row.try_get("is_youtube")?;
                    let listenduration: Option<i32> = row.try_get("listenduration")?;
                    let saved: bool = row.try_get("saved")?;
                    let queued: bool = row.try_get("queued")?;
                    let downloaded: bool = row.try_get("downloaded")?;
                    let dateadded_naive = row.try_get::<chrono::NaiveDateTime, _>("dateadded")?;
                    let dateadded = dateadded_naive.format("%Y-%m-%dT%H:%M:%S").to_string();

                    episodes.push(serde_json::json!({
                        "episodeid": episodeid,
                        "episodetitle": episodetitle,
                        "episodepubdate": episodepubdate,
                        "episodedescription": episodedescription,
                        "episodeartwork": episodeartwork,
                        "episodeurl": episodeurl,
                        "episodeduration": episodeduration,
                        "completed": completed,
                        "podcastname": podcastname,
                        "podcastid": podcastid,
                        "is_youtube": is_youtube,
                        "listenduration": listenduration,
                        "saved": saved,
                        "queued": queued,
                        "downloaded": downloaded,
                        "dateadded": dateadded
                    }));
                }

                // Build playlist_info structure matching Python exactly
                let playlist_info = serde_json::json!({
                    "name": playlist_name,
                    "description": playlist_description,
                    "episode_count": episode_count,
                    "icon_name": icon_name
                });

                Ok(serde_json::json!({
                    "playlist_info": playlist_info,
                    "episodes": episodes
                }))
            }
            DatabasePool::MySQL(pool) => {
                // Get playlist info with episode count - matches Python exactly
                let playlist_row = sqlx::query(
                    "SELECT
                        p.Name,
                        p.Description,
                        (SELECT COUNT(*)
                            FROM PlaylistContents pc
                            JOIN Episodes e ON pc.EpisodeID = e.EpisodeID
                            JOIN Podcasts pod ON e.PodcastID = pod.PodcastID
                            LEFT JOIN UserEpisodeHistory h ON e.EpisodeID = h.EpisodeID AND h.UserID = ?
                            WHERE pc.PlaylistID = p.PlaylistID
                            AND (p.IsSystemPlaylist = 0 OR
                                (p.IsSystemPlaylist = 1 AND
                                (h.EpisodeID IS NOT NULL OR pod.UserID = ?)))
                        ) as episode_count,
                        p.IconName,
                        p.IsSystemPlaylist
                    FROM Playlists p
                    WHERE p.PlaylistID = ? AND (p.UserID = ? OR p.IsSystemPlaylist = 1)"
                )
                    .bind(user_id)
                    .bind(user_id)
                    .bind(playlist_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                if playlist_row.is_none() {
                    return Err(AppError::not_found("Playlist not found"));
                }

                let row = playlist_row.unwrap();
                let playlist_name: String = row.try_get("Name")?;
                let playlist_description: String = row.try_get("Description").unwrap_or_default();
                let episode_count: i64 = row.try_get("episode_count")?;
                let icon_name: String = row.try_get("IconName").unwrap_or_default();
                let is_system_playlist: bool = row.try_get::<i8, _>("IsSystemPlaylist")? != 0;

                let episodes_rows = if is_system_playlist {
                    // For system playlists, update playlist first to get current episodes, then filter to user's podcasts
                    self.update_playlist_contents(playlist_id).await?;
                    
                    // Same query as user playlists but with additional filter for user's podcasts
                    sqlx::query(
                        "SELECT DISTINCT
                            e.EpisodeID as episodeid,
                            e.EpisodeTitle as episodetitle,
                            e.EpisodePubDate as episodepubdate,
                            e.EpisodeDescription as episodedescription,
                            CASE 
                                WHEN p.UsePodcastCoversCustomized = TRUE AND p.UsePodcastCovers = TRUE THEN p.ArtworkURL
                                WHEN u.UsePodcastCovers = TRUE THEN p.ArtworkURL
                                ELSE e.EpisodeArtwork
                            END as episodeartwork,
                            e.EpisodeURL as episodeurl,
                            e.EpisodeDuration as episodeduration,
                            e.Completed as completed,
                            p.PodcastName as podcastname,
                            p.PodcastID as podcastid,
                            p.IsYouTubeChannel as is_youtube,
                            ueh.ListenDuration as listenduration,
                            CASE WHEN se.EpisodeID IS NOT NULL THEN 1 ELSE 0 END AS saved,
                            CASE WHEN eq.EpisodeID IS NOT NULL THEN 1 ELSE 0 END AS queued,
                            CASE WHEN de.EpisodeID IS NOT NULL THEN 1 ELSE 0 END AS downloaded,
                            pc.DateAdded as addeddate
                         FROM PlaylistContents pc
                         JOIN Episodes e ON pc.EpisodeID = e.EpisodeID
                         JOIN Podcasts p ON e.PodcastID = p.PodcastID
                         LEFT JOIN Users u ON p.UserID = u.UserID
                         LEFT JOIN UserEpisodeHistory ueh ON e.EpisodeID = ueh.EpisodeID AND ueh.UserID = ?
                         LEFT JOIN SavedEpisodes se ON e.EpisodeID = se.EpisodeID AND se.UserID = ?
                         LEFT JOIN EpisodeQueue eq ON e.EpisodeID = eq.EpisodeID AND eq.UserID = ? AND eq.is_youtube = 0
                         LEFT JOIN DownloadedEpisodes de ON e.EpisodeID = de.EpisodeID AND de.UserID = ?
                         WHERE pc.PlaylistID = ? AND p.UserID = ?
                         ORDER BY pc.DateAdded DESC"
                    )
                        .bind(user_id)
                        .bind(user_id)
                        .bind(user_id)
                        .bind(user_id)
                        .bind(playlist_id)
                        .bind(user_id)
                        .fetch_all(pool)
                        .await?
                } else {
                    // For user playlists, use existing PlaylistContents logic
                    sqlx::query(
                        "SELECT DISTINCT
                            e.EpisodeID as episodeid,
                            e.EpisodeTitle as episodetitle,
                            e.EpisodePubDate as episodepubdate,
                            e.EpisodeDescription as episodedescription,
                            CASE 
                                WHEN p.UsePodcastCoversCustomized = TRUE AND p.UsePodcastCovers = TRUE THEN p.ArtworkURL
                                WHEN u.UsePodcastCovers = TRUE THEN p.ArtworkURL
                                ELSE e.EpisodeArtwork
                            END as episodeartwork,
                            e.EpisodeURL as episodeurl,
                            e.EpisodeDuration as episodeduration,
                            e.Completed as completed,
                            p.PodcastName as podcastname,
                            p.PodcastID as podcastid,
                            p.IsYouTubeChannel as is_youtube,
                            ueh.ListenDuration as listenduration,
                            CASE WHEN se.EpisodeID IS NOT NULL THEN 1 ELSE 0 END AS saved,
                            CASE WHEN eq.EpisodeID IS NOT NULL THEN 1 ELSE 0 END AS queued,
                            CASE WHEN de.EpisodeID IS NOT NULL THEN 1 ELSE 0 END AS downloaded,
                            pc.DateAdded as addeddate
                         FROM PlaylistContents pc
                         JOIN Episodes e ON pc.EpisodeID = e.EpisodeID
                         JOIN Podcasts p ON e.PodcastID = p.PodcastID
                         LEFT JOIN Users u ON p.UserID = u.UserID
                         LEFT JOIN UserEpisodeHistory ueh ON e.EpisodeID = ueh.EpisodeID AND ueh.UserID = ?
                         LEFT JOIN SavedEpisodes se ON e.EpisodeID = se.EpisodeID AND se.UserID = ?
                         LEFT JOIN EpisodeQueue eq ON e.EpisodeID = eq.EpisodeID AND eq.UserID = ? AND eq.is_youtube = 0
                         LEFT JOIN DownloadedEpisodes de ON e.EpisodeID = de.EpisodeID AND de.UserID = ?
                         WHERE pc.PlaylistID = ?
                         ORDER BY pc.DateAdded DESC"
                    )
                        .bind(user_id)
                        .bind(user_id)
                        .bind(user_id)
                        .bind(user_id)
                        .bind(playlist_id)
                        .fetch_all(pool)
                        .await?
                };

                let mut episodes = Vec::new();
                for row in episodes_rows {
                    let episodeid: i32 = row.try_get("episodeid")?;
                    let episodetitle: String = row.try_get("episodetitle")?;
                    let naive = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?;
                    let episodepubdate = naive.format("%Y-%m-%dT%H:%M:%S").to_string();
                    let episodedescription: String = row.try_get("episodedescription")?;
                    let episodeartwork: String = row.try_get("episodeartwork")?;
                    let episodeurl: String = row.try_get("episodeurl")?;
                    let episodeduration: i32 = row.try_get("episodeduration")?;
                    let completed: bool = row.try_get::<i8, _>("completed")? != 0;
                    let podcastname: String = row.try_get("podcastname")?;
                    let podcastid: i32 = row.try_get("podcastid")?;
                    let is_youtube: bool = row.try_get::<i8, _>("is_youtube")? != 0;
                    let listenduration: Option<i32> = row.try_get("listenduration")?;
                    let saved: bool = row.try_get::<i8, _>("saved")? != 0;
                    let queued: bool = row.try_get::<i8, _>("queued")? != 0;
                    let downloaded: bool = row.try_get::<i8, _>("downloaded")? != 0;
                    let addeddate_dt = row.try_get::<sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>, _>("addeddate")?;
                    let addeddate = addeddate_dt.format("%Y-%m-%dT%H:%M:%S").to_string();

                    episodes.push(serde_json::json!({
                        "episodeid": episodeid,
                        "episodetitle": episodetitle,
                        "episodepubdate": episodepubdate,
                        "episodedescription": episodedescription,
                        "episodeartwork": episodeartwork,
                        "episodeurl": episodeurl,
                        "episodeduration": episodeduration,
                        "completed": completed,
                        "podcastname": podcastname,
                        "podcastid": podcastid,
                        "is_youtube": is_youtube,
                        "listenduration": listenduration,
                        "saved": saved,
                        "queued": queued,
                        "downloaded": downloaded,
                        "dateadded": addeddate
                    }));
                }

                // Build playlist_info structure matching Python exactly
                let playlist_info = serde_json::json!({
                    "name": playlist_name,
                    "description": playlist_description,
                    "episode_count": episode_count,
                    "icon_name": icon_name
                });

                Ok(serde_json::json!({
                    "playlist_info": playlist_info,
                    "episodes": episodes
                }))
            }
        }
    }

    // Set user playback speed - matches Python set_playback_speed_user function
    pub async fn set_playback_speed_user(&self, user_id: i32, playback_speed: f64) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Users" SET playbackspeed = $1 WHERE userid = $2"#)
                    .bind(playback_speed)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Users SET PlaybackSpeed = ? WHERE UserID = ?")
                    .bind(playback_speed)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Set user podcast cover preference - global setting
    pub async fn set_global_podcast_cover_preference(&self, user_id: i32, use_podcast_covers: bool) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Users" SET usepodcastcovers = $1 WHERE userid = $2"#)
                    .bind(use_podcast_covers)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Users SET UsePodcastCovers = ? WHERE UserID = ?")
                    .bind(use_podcast_covers as i32)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Set podcast cover preference - per-podcast setting
    pub async fn set_podcast_cover_preference(&self, user_id: i32, podcast_id: i32, use_podcast_covers: bool) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Podcasts" SET usepodcastcovers = $1, usepodcastcoverscustomized = TRUE WHERE podcastid = $2 AND userid = $3"#)
                    .bind(use_podcast_covers)
                    .bind(podcast_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Podcasts SET UsePodcastCovers = ?, UsePodcastCoversCustomized = 1 WHERE PodcastID = ? AND UserID = ?")
                    .bind(use_podcast_covers as i32)
                    .bind(podcast_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Clear podcast cover preference - reset to use global setting
    pub async fn clear_podcast_cover_preference(&self, user_id: i32, podcast_id: i32) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Podcasts" SET usepodcastcovers = FALSE, usepodcastcoverscustomized = FALSE WHERE podcastid = $1 AND userid = $2"#)
                    .bind(podcast_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Podcasts SET UsePodcastCovers = 0, UsePodcastCoversCustomized = 0 WHERE PodcastID = ? AND UserID = ?")
                    .bind(podcast_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Get global podcast cover preference - for settings page
    pub async fn get_global_podcast_cover_preference(&self, user_id: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query_scalar::<_, Option<bool>>(r#"SELECT usepodcastcovers FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                Ok(result.unwrap_or(false))
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query_scalar::<_, Option<i32>>("SELECT UsePodcastCovers FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                Ok(result.unwrap_or(0) != 0)
            }
        }
    }

    // Get per-podcast cover preference - for episode layout page
    pub async fn get_podcast_cover_preference(&self, user_id: i32, podcast_id: i32) -> AppResult<Option<bool>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query_scalar::<_, Option<bool>>(r#"SELECT usepodcastcovers FROM "Podcasts" WHERE podcastid = $1 AND userid = $2 AND usepodcastcoverscustomized = TRUE"#)
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                Ok(result.flatten())
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query_scalar::<_, Option<i32>>("SELECT UsePodcastCovers FROM Podcasts WHERE PodcastID = ? AND UserID = ? AND UsePodcastCoversCustomized = 1")
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                Ok(result.flatten().map(|val| val != 0))
            }
        }
    }

    // Get all admin user IDs - matches Python add_news_feed_if_not_added logic
    pub async fn get_all_admin_user_ids(&self) -> AppResult<Vec<i32>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(r#"SELECT userid FROM "Users" WHERE isadmin = TRUE"#)
                    .fetch_all(pool)
                    .await?;
                
                let user_ids: Vec<i32> = rows.into_iter()
                    .map(|row| row.try_get("userid"))
                    .collect::<Result<Vec<_>, _>>()?;
                
                Ok(user_ids)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query("SELECT UserID FROM Users WHERE IsAdmin = 1")
                    .fetch_all(pool)
                    .await?;
                
                let user_ids: Vec<i32> = rows.into_iter()
                    .map(|row| row.try_get("UserID"))
                    .collect::<Result<Vec<_>, _>>()?;
                
                Ok(user_ids)
            }
        }
    }

    // Check if user already has a specific podcast feed - matches Python logic
    pub async fn user_has_podcast_feed(&self, user_id: i32, feed_url: &str) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT podcastid FROM "Podcasts" WHERE userid = $1 AND feedurl = $2"#)
                    .bind(user_id)
                    .bind(feed_url)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(row.is_some())
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT PodcastID FROM Podcasts WHERE UserID = ? AND FeedURL = ?")
                    .bind(user_id)
                    .bind(feed_url)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(row.is_some())
            }
        }
    }

    // Add PinePods news feed to admin users - matches Python add_news_feed_if_not_added function
    pub async fn add_news_feed_if_not_added(&self) -> AppResult<()> {
        let admin_user_ids = self.get_all_admin_user_ids().await?;
        let feed_url = "https://news.pinepods.online/feed.xml";

        for user_id in admin_user_ids {
            // Check if this user already has the news feed
            if !self.user_has_podcast_feed(user_id, feed_url).await? {
                // Add the PinePods news feed using existing functions - matches Python add_custom_podcast
                match self.get_podcast_values(feed_url, user_id, None, None).await {
                    Ok(podcast_values) => {
                        let feed_cutoff = 30; // Default cutoff like Python
                        if let Err(e) = self.add_podcast_from_values(&podcast_values, user_id, feed_cutoff, None, None).await {
                            eprintln!("Failed to add PinePods news feed for user {}: {}", user_id, e);
                            // Continue with other users even if one fails
                        }
                    },
                    Err(e) => {
                        eprintln!("Failed to get podcast values for PinePods news feed for user {}: {}", user_id, e);
                        // Continue with other users even if one fails
                    }
                }
            }
        }

        Ok(())
    }

    // Get YouTube video location - matches Python get_youtube_video_location function exactly
    pub async fn get_youtube_video_location(
        &self,
        episode_id: i32,
        user_id: i32,
    ) -> AppResult<Option<String>> {
        println!("Looking up YouTube video location for episode_id: {}, user_id: {}", episode_id, user_id);
        
        let youtube_id = match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"
                    SELECT "YouTubeVideos".youtubevideoid
                    FROM "YouTubeVideos"
                    INNER JOIN "Podcasts" ON "YouTubeVideos".podcastid = "Podcasts".podcastid
                    WHERE "YouTubeVideos".videoid = $1 AND "Podcasts".userid = $2
                "#)
                .bind(episode_id)
                .bind(user_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    row.try_get::<String, _>("youtubevideoid")?
                } else {
                    return Ok(None);
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query(r#"
                    SELECT YouTubeVideos.YouTubeVideoID
                    FROM YouTubeVideos
                    INNER JOIN Podcasts ON YouTubeVideos.PodcastID = Podcasts.PodcastID
                    WHERE YouTubeVideos.VideoID = ? AND Podcasts.UserID = ?
                "#)
                .bind(episode_id)
                .bind(user_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    row.try_get::<String, _>("YouTubeVideoID")?
                } else {
                    return Ok(None);
                }
            }
        };

        println!("Found YouTube ID: {}", youtube_id);

        let file_path = format!("/opt/pinepods/downloads/youtube/{}.mp3", youtube_id);
        let file_path_double = format!("/opt/pinepods/downloads/youtube/{}.mp3.mp3", youtube_id);

        println!("Checking paths: {} and {}", file_path, file_path_double);

        if tokio::fs::metadata(&file_path).await.is_ok() {
            println!("Found file at {}", file_path);
            Ok(Some(file_path))
        } else if tokio::fs::metadata(&file_path_double).await.is_ok() {
            println!("Found file at {}", file_path_double);
            Ok(Some(file_path_double))
        } else {
            println!("No file found for YouTube ID: {}", youtube_id);
            Ok(None)
        }
    }

    // Get download location - matches Python get_download_location function exactly
    pub async fn get_download_location(
        &self,
        episode_id: i32,
        user_id: i32,
    ) -> AppResult<Option<String>> {
        println!("Looking up download location for episode_id: {}, user_id: {}", episode_id, user_id);
        
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT downloadedlocation FROM "DownloadedEpisodes" WHERE episodeid = $1 AND userid = $2"#)
                    .bind(episode_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let location: String = row.try_get("downloadedlocation")?;
                    println!("DownloadedLocation found: {}", location);
                    Ok(Some(location))
                } else {
                    println!("No DownloadedLocation found for the given EpisodeID and UserID");
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT DownloadedLocation FROM DownloadedEpisodes WHERE EpisodeID = ? AND UserID = ?")
                    .bind(episode_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let location: String = row.try_get("DownloadedLocation")?;
                    println!("DownloadedLocation found: {}", location);
                    Ok(Some(location))
                } else {
                    println!("No DownloadedLocation found for the given EpisodeID and UserID");
                    Ok(None)
                }
            }
        }
    }

    // Update YouTube video duration after download - updates duration from MP3 file
    pub async fn update_youtube_video_duration(&self, video_id: &str, duration_seconds: i32) -> AppResult<()> {
        println!("Updating duration for YouTube video {} to {} seconds", video_id, duration_seconds);
        
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "YouTubeVideos" SET duration = $1 WHERE youtubevideoid = $2"#)
                    .bind(duration_seconds)
                    .bind(video_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE YouTubeVideos SET Duration = ? WHERE YouTubeVideoID = ?")
                    .bind(duration_seconds)
                    .bind(video_id)
                    .execute(pool)
                    .await?;
            }
        }
        
        println!("Successfully updated duration for YouTube video {}", video_id);
        Ok(())
    }
}

#[derive(Debug)]
pub struct PodcastValues {
    pub pod_title: String,
    pub pod_description: String, 
    pub pod_artwork: String,
    pub pod_feed_url: String,
    pub user_id: i32,
}

// Playlist configuration struct - matches Python playlist data structure exactly
#[derive(Debug, Clone)]
pub struct PlaylistConfig {
    pub playlist_id: i32,
    pub name: String,
    pub user_id: i32,
    pub podcast_ids: Option<Vec<i32>>,
    pub include_unplayed: bool,
    pub include_partially_played: bool,
    pub include_played: bool,
    pub play_progress_min: Option<f64>,
    pub play_progress_max: Option<f64>,
    pub time_filter_hours: Option<i32>,
    pub min_duration: Option<i32>,
    pub max_duration: Option<i32>,
    pub sort_order: String,
    pub group_by_podcast: bool,
    pub max_episodes: Option<i32>,
    pub is_system_playlist: bool,
}

impl PlaylistConfig {
    // Create from PostgreSQL row - matches Python playlist dictionary extraction
    pub fn from_postgres_row(row: &sqlx::postgres::PgRow) -> AppResult<Self> {
        use sqlx::Row;
        
        // Parse podcast IDs from PostgreSQL int4 array - can be NULL or {29,57} format
        let podcast_ids = match row.try_get::<Option<Vec<i32>>, _>("podcastids") {
            Ok(Some(ids)) => {
                println!("PostgreSQL got podcastids array: {:?}", ids);
                if ids.is_empty() {
                    None
                } else {
                    Some(ids)
                }
            }
            Ok(None) => {
                println!("PostgreSQL podcastids is NULL");
                None
            }
            Err(_) => {
                println!("PostgreSQL failed to get podcastids as array");
                None
            }
        };
        
        Ok(PlaylistConfig {
            playlist_id: row.try_get("playlistid")?,
            name: row.try_get("name")?,
            user_id: row.try_get("userid")?,
            podcast_ids,
            include_unplayed: row.try_get("includeunplayed").unwrap_or(true),
            include_partially_played: row.try_get("includepartiallyplayed").unwrap_or(true),
            include_played: row.try_get("includeplayed").unwrap_or(false),
            play_progress_min: row.try_get("playprogressmin").ok(),
            play_progress_max: row.try_get("playprogressmax").ok(),
            time_filter_hours: row.try_get("timefilterhours").ok(),
            min_duration: row.try_get("minduration").ok(),
            max_duration: row.try_get("maxduration").ok(),
            sort_order: row.try_get("sortorder").unwrap_or_else(|_| "date_desc".to_string()),
            group_by_podcast: row.try_get("groupbypodcast").unwrap_or(false),
            max_episodes: row.try_get("maxepisodes").ok(),
            is_system_playlist: row.try_get("issystemplaylist").unwrap_or(false),
        })
    }
    
    // Create from MySQL row - matches Python playlist dictionary extraction
    pub fn from_mysql_row(row: &sqlx::mysql::MySqlRow) -> AppResult<Self> {
        use sqlx::Row;
        
        // Parse podcast IDs from MySQL JSON (stored as BLOB)
        let podcast_ids = match row.try_get::<Option<Vec<u8>>, _>("PodcastIDs") {
            Ok(Some(ids_bytes)) => {
                let ids_str = String::from_utf8_lossy(&ids_bytes);
                println!("Got PodcastIDs from BLOB: '{}'", ids_str);
                if ids_str.is_empty() || ids_str == "null" || ids_str == "[]" {
                    None
                } else {
                    let parsed: Vec<i32> = serde_json::from_str(&ids_str).unwrap_or_default();
                    if parsed.is_empty() {
                        None
                    } else {
                        Some(parsed)
                    }
                }
            }
            Ok(None) => {
                println!("PodcastIDs is NULL");
                None
            }
            Err(_) => {
                // Fallback to try as String for older records
                match row.try_get::<Option<String>, _>("PodcastIDs") {
                    Ok(Some(ids_str)) => {
                        println!("Got PodcastIDs as String: '{}'", ids_str);
                        if ids_str.is_empty() || ids_str == "null" || ids_str == "[]" {
                            None
                        } else {
                            let parsed: Vec<i32> = serde_json::from_str(&ids_str).unwrap_or_default();
                            if parsed.is_empty() { None } else { Some(parsed) }
                        }
                    }
                    _ => None
                }
            }
        };
        
        Ok(PlaylistConfig {
            playlist_id: row.try_get("PlaylistID")?,
            name: row.try_get("Name")?,
            user_id: row.try_get("UserID")?,
            podcast_ids,
            include_unplayed: row.try_get("IncludeUnplayed").unwrap_or(true),
            include_partially_played: row.try_get("IncludePartiallyPlayed").unwrap_or(true),
            include_played: row.try_get("IncludePlayed").unwrap_or(false),
            play_progress_min: row.try_get("PlayProgressMin").ok(),
            play_progress_max: row.try_get("PlayProgressMax").ok(),
            time_filter_hours: row.try_get("TimeFilterHours").ok(),
            min_duration: row.try_get("MinDuration").ok(),
            max_duration: row.try_get("MaxDuration").ok(),
            sort_order: row.try_get("SortOrder").unwrap_or_else(|_| "date_desc".to_string()),
            group_by_podcast: row.try_get("GroupByPodcast").unwrap_or(false),
            max_episodes: row.try_get("MaxEpisodes").ok(),
            is_system_playlist: row.try_get("IsSystemPlaylist").unwrap_or(false),
        })
    }
    
    // Get PostgreSQL sort order - matches Python sort_mapping exactly (for inner query)
    pub fn get_postgres_sort_order(&self) -> String {
        match self.sort_order.as_str() {
            "date_asc" => "ORDER BY e.episodepubdate ASC".to_string(),
            "date_desc" => "ORDER BY e.episodepubdate DESC".to_string(),
            "duration_asc" => "ORDER BY e.episodeduration ASC".to_string(),
            "duration_desc" => "ORDER BY e.episodeduration DESC".to_string(),
            "listen_progress" => "ORDER BY (COALESCE(h.listenduration, 0)::float / NULLIF(e.episodeduration, 0)) DESC".to_string(),
            "completion" => "ORDER BY COALESCE(h.listenduration::float / NULLIF(e.episodeduration, 0), 0) DESC".to_string(),
            _ => "ORDER BY e.episodepubdate DESC".to_string(),
        }
    }
    
    // Get PostgreSQL sort order for outer query (ROW_NUMBER OVER) - fixed alias scoping
    pub fn get_postgres_outer_sort_order(&self) -> String {
        match self.sort_order.as_str() {
            "date_asc" => "ORDER BY episodes.episodepubdate ASC".to_string(),
            "date_desc" => "ORDER BY episodes.episodepubdate DESC".to_string(),
            "duration_asc" => "ORDER BY episodes.episodeduration ASC".to_string(),
            "duration_desc" => "ORDER BY episodes.episodeduration DESC".to_string(),
            "listen_progress" => "ORDER BY (episodes.listenduration::float / NULLIF(episodes.episodeduration, 0)) DESC".to_string(),
            "completion" => "ORDER BY (episodes.listenduration::float / NULLIF(episodes.episodeduration, 0)) DESC".to_string(),
            _ => "ORDER BY episodes.episodepubdate DESC".to_string(),
        }
    }
    
    // Get MySQL sort order - matches Python sort_mapping exactly (for inner query)
    pub fn get_mysql_sort_order(&self) -> String {
        match self.sort_order.as_str() {
            "date_asc" => "ORDER BY e.EpisodePubDate ASC".to_string(),
            "date_desc" => "ORDER BY e.EpisodePubDate DESC".to_string(),
            "duration_asc" => "ORDER BY e.EpisodeDuration ASC".to_string(),
            "duration_desc" => "ORDER BY e.EpisodeDuration DESC".to_string(),
            "listen_progress" => "ORDER BY (COALESCE(h.ListenDuration, 0) / NULLIF(e.EpisodeDuration, 0)) DESC".to_string(),
            "completion" => "ORDER BY COALESCE(h.ListenDuration / NULLIF(e.EpisodeDuration, 0), 0) DESC".to_string(),
            _ => "ORDER BY e.EpisodePubDate DESC".to_string(),
        }
    }
    
    // Get MySQL sort order for outer query (ROW_NUMBER OVER) - fixed alias scoping
    pub fn get_mysql_outer_sort_order(&self) -> String {
        match self.sort_order.as_str() {
            "date_asc" => "ORDER BY episodes.EpisodePubDate ASC".to_string(),
            "date_desc" => "ORDER BY episodes.EpisodePubDate DESC".to_string(),
            "duration_asc" => "ORDER BY episodes.EpisodeDuration ASC".to_string(),
            "duration_desc" => "ORDER BY episodes.EpisodeDuration DESC".to_string(),
            "listen_progress" => "ORDER BY (episodes.ListenDuration / NULLIF(episodes.EpisodeDuration, 0)) DESC".to_string(),
            "completion" => "ORDER BY (episodes.ListenDuration / NULLIF(episodes.EpisodeDuration, 0)) DESC".to_string(),
            _ => "ORDER BY episodes.EpisodePubDate DESC".to_string(),
        }
    }
    
    // Check if this is an "Almost Done" playlist - matches Python logic
    pub fn is_almost_done(&self) -> bool {
        self.name == "Almost Done" || 
        (self.include_partially_played && 
         !self.include_unplayed && 
         !self.include_played &&
         self.play_progress_min.map_or(false, |min| min >= 75.0))
    }
    
    // Check if this is a "Currently Listening" playlist - matches Python logic
    pub fn is_currently_listening(&self) -> bool {
        self.name == "Currently Listening" ||
        (self.include_partially_played && 
         !self.include_unplayed && 
         !self.include_played &&
         self.play_progress_min.is_none() &&
         self.play_progress_max.is_none())
    }
    
    // Check if this is a "Fresh Releases" playlist - matches Python logic
    pub fn is_fresh_releases(&self) -> bool {
        self.name == "Fresh Releases" && self.is_system_playlist
    }
    
    // Get effective time filter hours - Fresh Releases defaults to 24 if not set
    pub fn get_effective_time_filter_hours(&self) -> Option<i32> {
        if self.is_fresh_releases() && self.time_filter_hours.is_none() {
            Some(24) // Default 24 hours for Fresh Releases
        } else {
            self.time_filter_hours
        }
    }
}

impl DatabasePool {
    // RSS key validation - matches Python get_rss_key_if_valid function
    pub async fn get_rss_key_if_valid(&self, api_key: &str, podcast_ids: Option<&Vec<i32>>) -> AppResult<Option<crate::handlers::feed::RssKeyInfo>> {
        use crate::handlers::feed::RssKeyInfo;
        
        let filter_podcast_ids = podcast_ids.map_or(false, |ids| !ids.is_empty() && !ids.contains(&-1));
        
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"
                    SELECT fk.userid, STRING_AGG(CAST(fkm.podcastid AS TEXT), ',') as podcastids
                    FROM "RssKeys" fk
                    LEFT JOIN "RssKeyMap" fkm ON fk.rsskeyid = fkm.rsskeyid
                    WHERE fk.rsskey = $1
                    GROUP BY fk.userid
                "#)
                    .bind(api_key)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let user_id: i32 = row.try_get("userid")?;
                    let podcast_ids_str: Option<String> = row.try_get("podcastids").ok();
                    
                    let key_podcast_ids = if let Some(ids_str) = podcast_ids_str {
                        ids_str.split(',')
                            .filter_map(|s| s.parse::<i32>().ok())
                            .collect::<Vec<i32>>()
                    } else {
                        vec![-1] // Universal access if no specific podcasts
                    };
                    
                    // Check if access is allowed
                    if filter_podcast_ids {
                        if let Some(requested_ids) = podcast_ids {
                            let has_universal = key_podcast_ids.contains(&-1);
                            let has_specific_access = requested_ids.iter()
                                .all(|id| key_podcast_ids.contains(id));
                            
                            if !has_universal && !has_specific_access {
                                return Ok(None);
                            }
                        }
                    }
                    
                    Ok(Some(RssKeyInfo {
                        podcast_ids: key_podcast_ids,
                        user_id,
                        key: api_key.to_string(),
                    }))
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query(r#"
                    SELECT fk.UserID, GROUP_CONCAT(fkm.PodcastID) as podcastids
                    FROM RssKeys fk
                    LEFT JOIN RssKeyMap fkm ON fk.RssKeyID = fkm.RssKeyID
                    WHERE fk.RssKey = ?
                    GROUP BY fk.UserID
                "#)
                    .bind(api_key)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let user_id: i32 = row.try_get("UserID")?;
                    let podcast_ids_str: Option<String> = row.try_get("podcastids").ok();
                    
                    let key_podcast_ids = if let Some(ids_str) = podcast_ids_str {
                        ids_str.split(',')
                            .filter_map(|s| s.parse::<i32>().ok())
                            .collect::<Vec<i32>>()
                    } else {
                        vec![-1] // Universal access if no specific podcasts
                    };
                    
                    // Check if access is allowed
                    if filter_podcast_ids {
                        if let Some(requested_ids) = podcast_ids {
                            let has_universal = key_podcast_ids.contains(&-1);
                            let has_specific_access = requested_ids.iter()
                                .all(|id| key_podcast_ids.contains(id));
                            
                            if !has_universal && !has_specific_access {
                                return Ok(None);
                            }
                        }
                    }
                    
                    Ok(Some(RssKeyInfo {
                        podcast_ids: key_podcast_ids,
                        user_id,
                        key: api_key.to_string(),
                    }))
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Get RSS feed status - matches Python get_rss_feed_status function
    pub async fn get_rss_feed_status(&self, user_id: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT enablerssfeeds FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let enabled: Option<bool> = row.try_get("enablerssfeeds").ok();
                    Ok(enabled.unwrap_or(false))
                } else {
                    Ok(false)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT EnableRSSFeeds FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let enabled: Option<bool> = row.try_get("EnableRSSFeeds").ok();
                    Ok(enabled.unwrap_or(false))
                } else {
                    Ok(false)
                }
            }
        }
    }

    // Generate podcast RSS feed - matches Python generate_podcast_rss function
    pub async fn generate_podcast_rss(
        &self,
        rss_key: crate::handlers::feed::RssKeyInfo,
        limit: i32,
        source_type: Option<&str>,
        domain: &str,
        podcast_ids: Option<&Vec<i32>>,
    ) -> AppResult<String> {
        
        let user_id = rss_key.user_id;
        let mut effective_podcast_ids = rss_key.podcast_ids.clone();
        
        // If podcast_id parameter is provided, use it; otherwise use RSS key podcast_ids
        let explicit_podcast_filter = podcast_ids.is_some();
        if let Some(ids) = podcast_ids {
            if !ids.is_empty() {
                effective_podcast_ids = ids.clone();
            }
        }
        
        // Only use podcast filter if explicitly requested via URL parameter
        // RSS key podcast_ids should not affect "All Podcasts" feed behavior
        let podcast_filter = explicit_podcast_filter;
        
        // Check if RSS feeds are enabled for user
        if !self.get_rss_feed_status(user_id).await? {
            return Err(AppError::forbidden("RSS feeds not enabled for this user"));
        }
        
        // Get user info for feed metadata
        let username = match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT username FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    row.try_get::<String, _>("username").unwrap_or_else(|_| "Unknown User".to_string())
                } else {
                    return Err(AppError::not_found("User not found"));
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT Username FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    row.try_get::<String, _>("Username").unwrap_or_else(|_| "Unknown User".to_string())
                } else {
                    return Err(AppError::not_found("User not found"));
                }
            }
        };
        
        // Get podcast details for feed metadata - exact Python logic
        let (podcast_name, feed_image, feed_description) = if podcast_filter {
            match self {
                DatabasePool::Postgres(pool) => {
                    let row = sqlx::query(r#"SELECT podcastname, artworkurl, description FROM "Podcasts" WHERE podcastid = ANY($1)"#)
                        .bind(&effective_podcast_ids)
                        .fetch_optional(pool)
                        .await?;
                    
                    if let Some(row) = row {
                        (
                            row.try_get::<String, _>("podcastname").unwrap_or_else(|_| "Unknown Podcast".to_string()),
                            row.try_get::<Option<String>, _>("artworkurl").unwrap_or_default().unwrap_or_else(|| format!("{}/static/assets/favicon.png", domain)),
                            row.try_get::<String, _>("description").unwrap_or_else(|_| "No description available".to_string()),
                        )
                    } else {
                        ("Unknown Podcast".to_string(), format!("{}/static/assets/favicon.png", domain), "No description available".to_string())
                    }
                }
                DatabasePool::MySQL(pool) => {
                    if effective_podcast_ids.len() == 1 {
                        let row = sqlx::query("SELECT PodcastName, ArtworkURL, Description FROM Podcasts WHERE PodcastID = ?")
                            .bind(effective_podcast_ids[0])
                            .fetch_optional(pool)
                            .await?;
                        
                        if let Some(row) = row {
                            (
                                row.try_get::<String, _>("PodcastName").unwrap_or_else(|_| "Unknown Podcast".to_string()),
                                row.try_get::<Option<String>, _>("ArtworkURL").unwrap_or_default().unwrap_or_else(|| format!("{}/static/assets/favicon.png", domain)),
                                row.try_get::<String, _>("Description").unwrap_or_else(|_| "No description available".to_string()),
                            )
                        } else {
                            ("Unknown Podcast".to_string(), format!("{}/static/assets/favicon.png", domain), "No description available".to_string())
                        }
                    } else {
                        let placeholders = vec!["?"; effective_podcast_ids.len()].join(",");
                        let query_str = format!("SELECT PodcastName, ArtworkURL, Description FROM Podcasts WHERE PodcastID IN ({})", placeholders);
                        let mut query = sqlx::query(&query_str);
                        for &id in &effective_podcast_ids {
                            query = query.bind(id);
                        }
                        let row = query.fetch_optional(pool).await?;
                        
                        if let Some(row) = row {
                            (
                                row.try_get::<String, _>("PodcastName").unwrap_or_else(|_| "Unknown Podcast".to_string()),
                                row.try_get::<Option<String>, _>("ArtworkURL").unwrap_or_default().unwrap_or_else(|| format!("{}/static/assets/favicon.png", domain)),
                                row.try_get::<String, _>("Description").unwrap_or_else(|_| "No description available".to_string()),
                            )
                        } else {
                            ("Unknown Podcast".to_string(), format!("{}/static/assets/favicon.png", domain), "No description available".to_string())
                        }
                    }
                }
            }
        } else {
            ("All Podcasts".to_string(), format!("{}/static/assets/favicon.png", domain), "RSS feed for all podcasts from Pinepods".to_string())
        };

        // Build RSS feed using quick-xml for proper XML escaping
        use quick_xml::events::{Event, BytesStart, BytesEnd, BytesText, BytesCData};
        use quick_xml::Writer;
        use std::io::Cursor;

        let mut writer = Writer::new(Cursor::new(Vec::new()));
        
        // XML declaration
        writer.write_event(Event::Decl(quick_xml::events::BytesDecl::new("1.0", Some("UTF-8"), None)))?;
        
        // RSS root element with namespace
        let mut rss_elem = BytesStart::new("rss");
        rss_elem.push_attribute(("version", "2.0"));
        rss_elem.push_attribute(("xmlns:itunes", "http://www.itunes.com/dtds/podcast-1.0.dtd"));
        writer.write_event(Event::Start(rss_elem))?;
        
        // Channel
        writer.write_event(Event::Start(BytesStart::new("channel")))?;
        
        // Channel metadata
        writer.write_event(Event::Start(BytesStart::new("title")))?;
        writer.write_event(Event::Text(BytesText::new(&format!("Pinepods - {}", podcast_name))))?;
        writer.write_event(Event::End(BytesEnd::new("title")))?;
        
        writer.write_event(Event::Start(BytesStart::new("link")))?;
        writer.write_event(Event::Text(BytesText::new("https://github.com/madeofpendletonwool/pinepods")))?;
        writer.write_event(Event::End(BytesEnd::new("link")))?;
        
        writer.write_event(Event::Start(BytesStart::new("description")))?;
        writer.write_event(Event::Text(BytesText::new(&feed_description)))?;
        writer.write_event(Event::End(BytesEnd::new("description")))?;
        
        writer.write_event(Event::Start(BytesStart::new("language")))?;
        writer.write_event(Event::Text(BytesText::new("en")))?;
        writer.write_event(Event::End(BytesEnd::new("language")))?;
        
        writer.write_event(Event::Start(BytesStart::new("itunes:author")))?;
        writer.write_event(Event::Text(BytesText::new(&username)))?;
        writer.write_event(Event::End(BytesEnd::new("itunes:author")))?;
        
        // iTunes image
        let mut itunes_image = BytesStart::new("itunes:image");
        itunes_image.push_attribute(("href", feed_image.as_str()));
        writer.write_event(Event::Empty(itunes_image))?;
        
        // RSS image block
        writer.write_event(Event::Start(BytesStart::new("image")))?;
        
        writer.write_event(Event::Start(BytesStart::new("url")))?;
        writer.write_event(Event::Text(BytesText::new(&feed_image)))?;
        writer.write_event(Event::End(BytesEnd::new("url")))?;
        
        writer.write_event(Event::Start(BytesStart::new("title")))?;
        writer.write_event(Event::Text(BytesText::new(&format!("Pinepods - {}", podcast_name))))?;
        writer.write_event(Event::End(BytesEnd::new("title")))?;
        
        writer.write_event(Event::Start(BytesStart::new("link")))?;
        writer.write_event(Event::Text(BytesText::new("https://github.com/madeofpendletonwool/pinepods")))?;
        writer.write_event(Event::End(BytesEnd::new("link")))?;
        
        writer.write_event(Event::End(BytesEnd::new("image")))?;
        
        // TTL
        writer.write_event(Event::Start(BytesStart::new("ttl")))?;
        writer.write_event(Event::Text(BytesText::new("60")))?;
        writer.write_event(Event::End(BytesEnd::new("ttl")))?;
        
        // Get or create RSS key for this user to use in stream URLs
        let user_rss_key = self.get_or_create_user_rss_key(user_id).await?;
        
        // Get episodes (use the user's RSS key for stream URLs, not the requesting key)
        let episodes = self.get_rss_episodes(user_id, limit, source_type, &effective_podcast_ids, podcast_filter, domain, &user_rss_key).await?;
        
        // Write episodes
        for episode in episodes {
            writer.write_event(Event::Start(BytesStart::new("item")))?;
            
            // Title (using CDATA for safety)
            writer.write_event(Event::Start(BytesStart::new("title")))?;
            writer.write_event(Event::CData(BytesCData::new(&episode.title)))?;
            writer.write_event(Event::End(BytesEnd::new("title")))?;
            
            // Link (URL will be properly escaped)
            writer.write_event(Event::Start(BytesStart::new("link")))?;
            writer.write_event(Event::Text(BytesText::new(&episode.url)))?;
            writer.write_event(Event::End(BytesEnd::new("link")))?;
            
            // Description (using CDATA for safety)
            writer.write_event(Event::Start(BytesStart::new("description")))?;
            writer.write_event(Event::CData(BytesCData::new(&episode.description)))?;
            writer.write_event(Event::End(BytesEnd::new("description")))?;
            
            // GUID
            writer.write_event(Event::Start(BytesStart::new("guid")))?;
            writer.write_event(Event::Text(BytesText::new(&episode.url)))?;
            writer.write_event(Event::End(BytesEnd::new("guid")))?;
            
            // Pub date
            writer.write_event(Event::Start(BytesStart::new("pubDate")))?;
            writer.write_event(Event::Text(BytesText::new(&episode.pub_date)))?;
            writer.write_event(Event::End(BytesEnd::new("pubDate")))?;
            
            // Author (if present)
            if let Some(ref author) = episode.author {
                writer.write_event(Event::Start(BytesStart::new("itunes:author")))?;
                writer.write_event(Event::Text(BytesText::new(author)))?;
                writer.write_event(Event::End(BytesEnd::new("itunes:author")))?;
            }
            
            // Artwork (if present)
            if let Some(ref artwork_url) = episode.artwork_url {
                let mut itunes_img = BytesStart::new("itunes:image");
                itunes_img.push_attribute(("href", artwork_url.as_str()));
                writer.write_event(Event::Empty(itunes_img))?;
            }
            
            // Duration (iTunes format: HH:MM:SS or MM:SS)
            if let Some(duration_seconds) = episode.duration {
                let hours = duration_seconds / 3600;
                let minutes = (duration_seconds % 3600) / 60;
                let seconds = duration_seconds % 60;
                
                let duration_str = if hours > 0 {
                    format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
                } else {
                    format!("{:02}:{:02}", minutes, seconds)
                };
                
                writer.write_event(Event::Start(BytesStart::new("itunes:duration")))?;
                writer.write_event(Event::Text(BytesText::new(&duration_str)))?;
                writer.write_event(Event::End(BytesEnd::new("itunes:duration")))?;
            }
            
            // Enclosure (length should be file size in bytes, using duration as placeholder)
            let mut enclosure = BytesStart::new("enclosure");
            enclosure.push_attribute(("url", episode.url.as_str()));
            enclosure.push_attribute(("length", episode.duration.unwrap_or(0).to_string().as_str()));
            enclosure.push_attribute(("type", "audio/mpeg"));
            writer.write_event(Event::Empty(enclosure))?;
            
            writer.write_event(Event::End(BytesEnd::new("item")))?;
        }
        
        // Close channel and RSS
        writer.write_event(Event::End(BytesEnd::new("channel")))?;
        writer.write_event(Event::End(BytesEnd::new("rss")))?;
        
        // Convert to string
        let result = writer.into_inner().into_inner();
        let rss_content = String::from_utf8(result)
            .map_err(|e| AppError::internal(&format!("Failed to convert RSS to UTF-8: {}", e)))?;
        
        Ok(rss_content)
    }
    
    // Get user RSS key - matches Python get_user_rss_key function
    pub async fn get_user_rss_key(&self, user_id: i32) -> AppResult<Option<String>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT rsskey FROM "RssKeys" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(row.try_get("rsskey").ok())
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT RssKey FROM RssKeys WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(row.try_get("RssKey").ok())
                } else {
                    Ok(None)
                }
            }
        }
    }
    
    // Get or create RSS key for user - ensures user always has an RSS key for stream URLs
    pub async fn get_or_create_user_rss_key(&self, user_id: i32) -> AppResult<String> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Try to get existing RSS key
                let existing_key = sqlx::query(r#"SELECT rsskey FROM "RssKeys" WHERE userid = $1 LIMIT 1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = existing_key {
                    let key: String = row.try_get("rsskey")?;
                    Ok(key)
                } else {
                    // Create new RSS key
                    let new_key = uuid::Uuid::new_v4().to_string();
                    sqlx::query(r#"INSERT INTO "RssKeys" (userid, rsskey) VALUES ($1, $2)"#)
                        .bind(user_id)
                        .bind(&new_key)
                        .execute(pool)
                        .await?;
                    Ok(new_key)
                }
            }
            DatabasePool::MySQL(pool) => {
                // Try to get existing RSS key
                let existing_key = sqlx::query("SELECT RssKey FROM RssKeys WHERE UserID = ? LIMIT 1")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = existing_key {
                    let key: String = row.try_get("RssKey")?;
                    Ok(key)
                } else {
                    // Create new RSS key
                    let new_key = uuid::Uuid::new_v4().to_string();
                    sqlx::query("INSERT INTO RssKeys (UserID, RssKey) VALUES (?, ?)")
                        .bind(user_id)
                        .bind(&new_key)
                        .execute(pool)
                        .await?;
                    Ok(new_key)
                }
            }
        }
    }
    
    // Helper function to get RSS episodes
    async fn get_rss_episodes(
        &self,
        user_id: i32,
        limit: i32,
        source_type: Option<&str>,
        podcast_ids: &[i32],
        podcast_filter: bool,
        domain: &str,
        api_key: &str,
    ) -> AppResult<Vec<RssEpisode>> {
        use chrono::{DateTime, Utc};
        
        match self {
            DatabasePool::Postgres(pool) => {
                let mut base_query = r#"
                    SELECT
                        e.episodeid,
                        e.podcastid,
                        e.episodetitle,
                        e.episodedescription,
                        CASE WHEN de.episodeid IS NULL
                                THEN e.episodeurl
                                ELSE CONCAT(CAST($1 AS TEXT), '/api/data/stream/', e.episodeid, '?api_key=', CAST($2 AS TEXT), '&user_id=', pp.userid)
                        END as episodeurl,
                        e.episodeartwork,
                        e.episodepubdate,
                        e.episodeduration,
                        pp.podcastname,
                        pp.author,
                        pp.artworkurl,
                        pp.description as podcastdescription
                    FROM "Episodes" e
                    JOIN "Podcasts" pp ON e.podcastid = pp.podcastid
                    LEFT JOIN "DownloadedEpisodes" de ON e.episodeid = de.episodeid
                    WHERE pp.userid = $3
                "#.to_string();

                let mut param_count = 3;
                if podcast_filter && !podcast_ids.is_empty() {
                    param_count += 1;
                    base_query.push_str(&format!(" AND pp.podcastid = ANY(${})", param_count));
                }

                // Add YouTube union if needed (exact Python logic)
                let add_youtube_union = source_type.is_none() || source_type == Some("youtube");
                if add_youtube_union {
                    base_query.push_str(r#"
                        UNION ALL
                        SELECT
                            y.videoid as episodeid,
                            y.podcastid,
                            y.videotitle as episodetitle,
                            y.videodescription as episodedescription,
                            CONCAT(CAST($1 AS TEXT), '/api/data/stream/', CAST(y.videoid AS TEXT), '?api_key=', CAST($2 AS TEXT), '&type=youtube&user_id=', pv.userid) as episodeurl,
                            y.thumbnailurl as episodeartwork,
                            y.publishedat as episodepubdate,
                            y.duration as episodeduration,
                            pv.podcastname,
                            pv.author,
                            pv.artworkurl,
                            pv.description as podcastdescription
                        FROM "YouTubeVideos" y
                        JOIN "Podcasts" pv on y.podcastid = pv.podcastid
                        WHERE pv.userid = $3
                    "#);

                    if podcast_filter && !podcast_ids.is_empty() {
                        base_query.push_str(&format!(" AND pv.podcastid = ANY(${})", param_count));
                    }
                }

                base_query.push_str(" ORDER BY episodepubdate DESC");
                if limit > 0 {
                    base_query.push_str(&format!(" LIMIT {}", limit));
                }

                // Execute query
                let mut query = sqlx::query(&base_query)
                    .bind(domain)
                    .bind(api_key)
                    .bind(user_id);

                if podcast_filter && !podcast_ids.is_empty() {
                    query = query.bind(podcast_ids);
                }

                let rows = query.fetch_all(pool).await?;
                
                let mut episodes = Vec::new();
                for row in rows {
                    let title: String = row.try_get("episodetitle").unwrap_or_else(|_| "Untitled Episode".to_string());
                    let description: String = row.try_get("episodedescription").unwrap_or_else(|_| String::new());
                    let url: String = row.try_get("episodeurl").unwrap_or_else(|_| String::new());
                    let duration: Option<i32> = row.try_get("episodeduration").ok();
                    let author: Option<String> = row.try_get("author").ok();
                    // Use episode-specific artwork if available, otherwise fall back to podcast artwork
                    let episode_artwork: Option<String> = row.try_get("episodeartwork").ok();
                    let podcast_artwork: Option<String> = row.try_get("artworkurl").ok();
                    let artwork_url = episode_artwork.filter(|url| !url.is_empty()).or(podcast_artwork);
                    
                    let pub_date = if let Ok(dt) = row.try_get::<DateTime<Utc>, _>("episodepubdate") {
                        dt.format("%a, %d %b %Y %H:%M:%S %z").to_string()
                    } else {
                        Utc::now().format("%a, %d %b %Y %H:%M:%S %z").to_string()
                    };

                    episodes.push(RssEpisode {
                        title,
                        description,
                        url,
                        pub_date,
                        duration,
                        author,
                        artwork_url,
                    });
                }
                
                Ok(episodes)
            }
            DatabasePool::MySQL(pool) => {
                let mut base_query = r#"
                    SELECT
                        e.EpisodeID,
                        e.PodcastID,
                        e.EpisodeTitle COLLATE utf8mb4_unicode_ci as EpisodeTitle,
                        e.EpisodeDescription COLLATE utf8mb4_unicode_ci as EpisodeDescription,
                        CASE WHEN de.EpisodeID IS NULL
                                THEN e.EpisodeURL COLLATE utf8mb4_unicode_ci
                                ELSE CONCAT(CAST(? AS CHAR), '/api/data/stream/', CAST(e.EpisodeID AS CHAR), '?api_key=', CAST(? AS CHAR), '&user_id=', pp.UserID)
                        END COLLATE utf8mb4_unicode_ci as EpisodeURL,
                        e.EpisodeArtwork COLLATE utf8mb4_unicode_ci as EpisodeArtwork,
                        e.EpisodePubDate,
                        e.EpisodeDuration,
                        pp.PodcastName COLLATE utf8mb4_unicode_ci as PodcastName,
                        pp.Author COLLATE utf8mb4_unicode_ci as Author,
                        pp.ArtworkURL COLLATE utf8mb4_unicode_ci as ArtworkURL,
                        pp.Description COLLATE utf8mb4_unicode_ci as PodcastDescription
                    FROM Episodes e
                    JOIN Podcasts pp ON e.PodcastID = pp.PodcastID
                    LEFT JOIN DownloadedEpisodes de ON e.EpisodeID = de.EpisodeID
                    WHERE pp.UserID = ?
                "#.to_string();

                if podcast_filter && !podcast_ids.is_empty() {
                    let placeholders = vec!["?"; podcast_ids.len()].join(",");
                    base_query.push_str(&format!(" AND pp.PodcastID IN ({})", placeholders));
                }

                // Add YouTube union if needed
                let add_youtube_union = source_type.is_none() || source_type == Some("youtube");
                if add_youtube_union {
                    base_query.push_str(r#"
                        UNION ALL
                        SELECT
                            y.VideoID as EpisodeID,
                            y.PodcastID as PodcastID,
                            y.VideoTitle COLLATE utf8mb4_unicode_ci as EpisodeTitle,
                            y.VideoDescription COLLATE utf8mb4_unicode_ci as EpisodeDescription,
                            CONCAT(CAST(? AS CHAR), '/api/data/stream/', CAST(y.VideoID AS CHAR), '?api_key=', CAST(? AS CHAR), '&type=youtube&user_id=', pv.UserID) COLLATE utf8mb4_unicode_ci as EpisodeURL,
                            y.ThumbnailURL COLLATE utf8mb4_unicode_ci as EpisodeArtwork,
                            y.PublishedAt as EpisodePubDate,
                            y.Duration as EpisodeDuration,
                            pv.PodcastName COLLATE utf8mb4_unicode_ci as PodcastName,
                            pv.Author COLLATE utf8mb4_unicode_ci as Author,
                            pv.ArtworkURL COLLATE utf8mb4_unicode_ci as ArtworkURL,
                            pv.Description COLLATE utf8mb4_unicode_ci as PodcastDescription
                        FROM YouTubeVideos y
                        JOIN Podcasts pv on y.PodcastID = pv.PodcastID
                        WHERE pv.UserID = ?
                    "#);

                    if podcast_filter && !podcast_ids.is_empty() {
                        let placeholders = vec!["?"; podcast_ids.len()].join(",");
                        base_query.push_str(&format!(" AND pv.PodcastID IN ({})", placeholders));
                    }
                }

                base_query.push_str(" ORDER BY EpisodePubDate DESC");
                if limit > 0 {
                    base_query.push_str(&format!(" LIMIT {}", limit));
                }

                // Build query with parameters
                let mut query = sqlx::query(&base_query)
                    .bind(domain)
                    .bind(api_key)
                    .bind(user_id);

                if podcast_filter && !podcast_ids.is_empty() {
                    for &id in podcast_ids {
                        query = query.bind(id);
                    }
                }

                if add_youtube_union {
                    query = query.bind(domain).bind(api_key).bind(user_id);
                    if podcast_filter && !podcast_ids.is_empty() {
                        for &id in podcast_ids {
                            query = query.bind(id);
                        }
                    }
                }

                let rows = query.fetch_all(pool).await?;
                
                let mut episodes = Vec::new();
                for row in rows {
                    let title: String = row.try_get("EpisodeTitle").unwrap_or_else(|_| "Untitled Episode".to_string());
                    let description: String = row.try_get("EpisodeDescription").unwrap_or_else(|_| String::new());
                    let url: String = row.try_get("EpisodeURL").unwrap_or_else(|_| String::new());
                    let duration: Option<i32> = row.try_get("EpisodeDuration").ok();
                    let author: Option<String> = row.try_get("Author").ok();
                    // Use episode-specific artwork if available, otherwise fall back to podcast artwork
                    let episode_artwork: Option<String> = row.try_get("EpisodeArtwork").ok();
                    let podcast_artwork: Option<String> = row.try_get("ArtworkURL").ok();
                    let artwork_url = episode_artwork.filter(|url| !url.is_empty()).or(podcast_artwork);
                    
                    let pub_date = if let Ok(dt) = row.try_get::<DateTime<Utc>, _>("EpisodePubDate") {
                        dt.format("%a, %d %b %Y %H:%M:%S %z").to_string()
                    } else {
                        Utc::now().format("%a, %d %b %Y %H:%M:%S %z").to_string()
                    };

                    episodes.push(RssEpisode {
                        title,
                        description,
                        url,
                        pub_date,
                        duration,
                        author,
                        artwork_url,
                    });
                }
                
                Ok(episodes)
            }
        }
    }

    // Get podcast notification status - matches Python get_podcast_notification_status function
    pub async fn get_podcast_notification_status(&self, podcast_id: i32, user_id: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"
                    SELECT notificationsenabled
                    FROM "Podcasts"
                    WHERE podcastid = $1 AND userid = $2
                "#)
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(row.try_get("notificationsenabled").unwrap_or(false))
                } else {
                    Ok(false)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("
                    SELECT NotificationsEnabled
                    FROM Podcasts
                    WHERE PodcastID = ? AND UserID = ?
                ")
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    let enabled: i8 = row.try_get("NotificationsEnabled").unwrap_or(0);
                    Ok(enabled != 0)
                } else {
                    Ok(false)
                }
            }
        }
    }

    // Get MFA secret - matches Python get_mfa_secret function
    pub async fn get_mfa_secret(&self, user_id: i32) -> AppResult<Option<String>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT mfa_secret FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(row.try_get("mfa_secret").ok())
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT MFA_Secret FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(row.try_get("MFA_Secret").ok())
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Return YouTube episodes - matches Python return_youtube_episodes function exactly
    pub async fn return_youtube_episodes(
        &self,
        user_id: i32,
        podcast_id: i32,
    ) -> AppResult<Option<Vec<serde_json::Value>>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(r#"
                    SELECT "Podcasts".podcastid, "Podcasts".podcastname, "YouTubeVideos".videoid AS episodeid,
                    "YouTubeVideos".videotitle AS episodetitle, "YouTubeVideos".publishedat AS episodepubdate,
                    "YouTubeVideos".videodescription AS episodedescription,
                    "YouTubeVideos".thumbnailurl AS episodeartwork, "YouTubeVideos".videourl AS episodeurl,
                    "YouTubeVideos".duration AS episodeduration,
                    "YouTubeVideos".listenposition AS listenduration,
                    "YouTubeVideos".youtubevideoid AS guid
                    FROM "YouTubeVideos"
                    INNER JOIN "Podcasts" ON "YouTubeVideos".podcastid = "Podcasts".podcastid
                    WHERE "Podcasts".podcastid = $1 AND "Podcasts".userid = $2
                    ORDER BY "YouTubeVideos".publishedat DESC
                "#)
                .bind(podcast_id)
                .bind(user_id)
                .fetch_all(pool)
                .await?;

                if rows.is_empty() {
                    return Ok(None);
                }

                let mut episodes = Vec::new();
                for row in rows {
                    let episode = serde_json::json!({
                        "Podcastid": row.try_get::<i32, _>("podcastid").unwrap_or(0),
                        "Podcastname": row.try_get::<String, _>("podcastname").unwrap_or_default(),
                        "Episodeid": row.try_get::<i32, _>("episodeid").unwrap_or(0),
                        "Episodetitle": row.try_get::<String, _>("episodetitle").unwrap_or_default(),
                        "Episodepubdate": row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")
                            .map(|dt| dt.and_utc().to_rfc3339())
                            .unwrap_or_default(),
                        "Episodedescription": row.try_get::<String, _>("episodedescription").unwrap_or_default(),
                        "Episodeartwork": row.try_get::<String, _>("episodeartwork").unwrap_or_default(),
                        "Episodeurl": row.try_get::<String, _>("episodeurl").unwrap_or_default(),
                        "Episodeduration": row.try_get::<i32, _>("episodeduration").unwrap_or(0),
                        "Listenduration": row.try_get::<i32, _>("listenduration").unwrap_or(0),
                        "Guid": row.try_get::<String, _>("guid").unwrap_or_default()
                    });
                    episodes.push(episode);
                }

                Ok(Some(episodes))
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(r#"
                    SELECT Podcasts.PodcastID, Podcasts.PodcastName, YouTubeVideos.VideoID AS EpisodeID,
                    YouTubeVideos.VideoTitle AS EpisodeTitle, YouTubeVideos.PublishedAt AS EpisodePubDate,
                    YouTubeVideos.VideoDescription AS EpisodeDescription,
                    YouTubeVideos.ThumbnailURL AS EpisodeArtwork, YouTubeVideos.VideoURL AS EpisodeURL,
                    YouTubeVideos.Duration AS EpisodeDuration,
                    YouTubeVideos.ListenPosition AS ListenDuration,
                    YouTubeVideos.YouTubeVideoID AS guid
                    FROM YouTubeVideos
                    INNER JOIN Podcasts ON YouTubeVideos.PodcastID = Podcasts.PodcastID
                    WHERE Podcasts.PodcastID = ? AND Podcasts.UserID = ?
                    ORDER BY YouTubeVideos.PublishedAt DESC
                "#)
                .bind(podcast_id)
                .bind(user_id)
                .fetch_all(pool)
                .await?;

                if rows.is_empty() {
                    return Ok(None);
                }

                let mut episodes = Vec::new();
                for row in rows {
                    let episode = serde_json::json!({
                        "Podcastid": row.try_get::<i32, _>("PodcastID").unwrap_or(0),
                        "Podcastname": row.try_get::<String, _>("PodcastName").unwrap_or_default(),
                        "Episodeid": row.try_get::<i32, _>("EpisodeID").unwrap_or(0),
                        "Episodetitle": row.try_get::<String, _>("EpisodeTitle").unwrap_or_default(),
                        "Episodepubdate": row.try_get::<chrono::NaiveDateTime, _>("EpisodePubDate")
                            .map(|dt| dt.and_utc().to_rfc3339())
                            .unwrap_or_default(),
                        "Episodedescription": row.try_get::<String, _>("EpisodeDescription").unwrap_or_default(),
                        "Episodeartwork": row.try_get::<String, _>("EpisodeArtwork").unwrap_or_default(),
                        "Episodeurl": row.try_get::<String, _>("EpisodeURL").unwrap_or_default(),
                        "Episodeduration": row.try_get::<i32, _>("EpisodeDuration").unwrap_or(0),
                        "Listenduration": row.try_get::<i32, _>("ListenDuration").unwrap_or(0),
                        "Guid": row.try_get::<String, _>("guid").unwrap_or_default()
                    });
                    episodes.push(episode);
                }

                Ok(Some(episodes))
            }
        }
    }

    // Remove YouTube channel by URL - matches Python remove_youtube_channel_by_url function exactly
    pub async fn remove_youtube_channel_by_url(
        &self,
        channel_name: &str,
        channel_url: &str,
        user_id: i32,
    ) -> AppResult<()> {
        println!("got to remove youtube channel");
        
        // Get the PodcastID first
        let podcast_id = match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"
                    SELECT podcastid
                    FROM "Podcasts"
                    WHERE podcastname = $1
                    AND feedurl = $2
                    AND userid = $3
                    AND isyoutubechannel = TRUE
                "#)
                .bind(channel_name)
                .bind(channel_url)
                .bind(user_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    row.try_get::<i32, _>("podcastid")?
                } else {
                    return Err(AppError::external_error(&format!("No YouTube channel found with name {}", channel_name)));
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query(r#"
                    SELECT PodcastID
                    FROM Podcasts
                    WHERE PodcastName = ?
                    AND FeedURL = ?
                    AND UserID = ?
                    AND IsYouTubeChannel = TRUE
                "#)
                .bind(channel_name)
                .bind(channel_url)
                .bind(user_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    row.try_get::<i32, _>("PodcastID")?
                } else {
                    return Err(AppError::external_error(&format!("No YouTube channel found with name {}", channel_name)));
                }
            }
        };

        // Remove the channel by ID
        self.remove_youtube_channel_by_id(podcast_id, user_id).await
    }

    // Remove YouTube channel by ID - matches Python remove_youtube_channel function exactly
    pub async fn remove_youtube_channel_by_id(
        &self,
        podcast_id: i32,
        user_id: i32,
    ) -> AppResult<()> {
        // First, get all video IDs for the podcast so we can delete the files
        let video_ids: Vec<String> = match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(r#"SELECT youtubevideoid FROM "YouTubeVideos" WHERE podcastid = $1"#)
                    .bind(podcast_id)
                    .fetch_all(pool)
                    .await?;

                rows.into_iter()
                    .map(|row| row.try_get::<String, _>("youtubevideoid").unwrap_or_default())
                    .collect()
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query("SELECT YouTubeVideoID FROM YouTubeVideos WHERE PodcastID = ?")
                    .bind(podcast_id)
                    .fetch_all(pool)
                    .await?;

                rows.into_iter()
                    .map(|row| row.try_get::<String, _>("YouTubeVideoID").unwrap_or_default())
                    .collect()
            }
        };

        // Delete the MP3 files for each video
        for video_id in &video_ids {
            let file_paths = vec![
                format!("/opt/pinepods/downloads/youtube/{}.mp3", video_id),
                format!("/opt/pinepods/downloads/youtube/{}.mp3.mp3", video_id), // In case of double extension
            ];

            for file_path in file_paths {
                if tokio::fs::metadata(&file_path).await.is_ok() {
                    match tokio::fs::remove_file(&file_path).await {
                        Ok(_) => println!("Deleted file: {}", file_path),
                        Err(e) => println!("Failed to delete file {}: {}", file_path, e),
                    }
                }
            }
        }

        // Delete from the related tables in the correct order
        match self {
            DatabasePool::Postgres(pool) => {
                let delete_queries = vec![
                    r#"DELETE FROM "PlaylistContents" WHERE episodeid IN (SELECT videoid FROM "YouTubeVideos" WHERE podcastid = $1)"#,
                    r#"DELETE FROM "UserEpisodeHistory" WHERE episodeid IN (SELECT videoid FROM "YouTubeVideos" WHERE podcastid = $1)"#,
                    r#"DELETE FROM "UserVideoHistory" WHERE videoid IN (SELECT videoid FROM "YouTubeVideos" WHERE podcastid = $1)"#,
                    r#"DELETE FROM "DownloadedEpisodes" WHERE episodeid IN (SELECT videoid FROM "YouTubeVideos" WHERE podcastid = $1)"#,
                    r#"DELETE FROM "DownloadedVideos" WHERE videoid IN (SELECT videoid FROM "YouTubeVideos" WHERE podcastid = $1)"#,
                    r#"DELETE FROM "SavedVideos" WHERE videoid IN (SELECT videoid FROM "YouTubeVideos" WHERE podcastid = $1)"#,
                    r#"DELETE FROM "SavedEpisodes" WHERE episodeid IN (SELECT videoid FROM "YouTubeVideos" WHERE podcastid = $1)"#,
                    r#"DELETE FROM "EpisodeQueue" WHERE episodeid IN (SELECT videoid FROM "YouTubeVideos" WHERE podcastid = $1)"#,
                    r#"DELETE FROM "YouTubeVideos" WHERE podcastid = $1"#,
                    r#"DELETE FROM "Podcasts" WHERE podcastid = $1 AND isyoutubechannel = TRUE"#,
                ];

                for query in delete_queries {
                    sqlx::query(query)
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                }

                // Update user stats
                sqlx::query(r#"UPDATE "UserStats" SET podcastsadded = podcastsadded - 1 WHERE userid = $1"#)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                let delete_queries = vec![
                    "DELETE FROM PlaylistContents WHERE EpisodeID IN (SELECT VideoID FROM YouTubeVideos WHERE PodcastID = ?)",
                    "DELETE FROM UserEpisodeHistory WHERE EpisodeID IN (SELECT VideoID FROM YouTubeVideos WHERE PodcastID = ?)",
                    "DELETE FROM UserVideoHistory WHERE VideoID IN (SELECT VideoID FROM YouTubeVideos WHERE PodcastID = ?)",
                    "DELETE FROM DownloadedEpisodes WHERE EpisodeID IN (SELECT VideoID FROM YouTubeVideos WHERE PodcastID = ?)",
                    "DELETE FROM DownloadedVideos WHERE VideoID IN (SELECT VideoID FROM YouTubeVideos WHERE PodcastID = ?)",
                    "DELETE FROM SavedVideos WHERE VideoID IN (SELECT VideoID FROM YouTubeVideos WHERE PodcastID = ?)",
                    "DELETE FROM SavedEpisodes WHERE EpisodeID IN (SELECT VideoID FROM YouTubeVideos WHERE PodcastID = ?)",
                    "DELETE FROM EpisodeQueue WHERE EpisodeID IN (SELECT VideoID FROM YouTubeVideos WHERE PodcastID = ?)",
                    "DELETE FROM YouTubeVideos WHERE PodcastID = ?",
                    "DELETE FROM Podcasts WHERE PodcastID = ? AND IsYouTubeChannel = TRUE",
                ];

                for query in delete_queries {
                    sqlx::query(query)
                        .bind(podcast_id)
                        .execute(pool)
                        .await?;
                }

                // Update user stats
                sqlx::query("UPDATE UserStats SET PodcastsAdded = PodcastsAdded - 1 WHERE UserID = ?")
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }

        Ok(())
    }

    // Get podcast ID by feed URL and title - for get_podcast_details_dynamic
    pub async fn get_podcast_id_by_feed(&self, user_id: i32, feed_url: &str, _podcast_title: &str) -> AppResult<i32> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(
                    r#"SELECT podcastid FROM "Podcasts" WHERE feedurl = $1 AND userid = $2"#
                )
                .bind(feed_url)
                .bind(user_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    Ok(row.try_get("podcastid")?)
                } else {
                    Err(AppError::not_found("Podcast not found"))
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query(
                    "SELECT PodcastID FROM Podcasts WHERE FeedURL = ? AND UserID = ?"
                )
                .bind(feed_url)
                .bind(user_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    Ok(row.try_get("PodcastID")?)
                } else {
                    Err(AppError::not_found("Podcast not found"))
                }
            }
        }
    }

    // Get raw podcast details - returns all fields as JSON for get_podcast_details_dynamic
    pub async fn get_podcast_details_raw(&self, user_id: i32, podcast_id: i32) -> AppResult<Option<serde_json::Value>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(
                    r#"SELECT * FROM "Podcasts" WHERE podcastid = $1 AND userid = $2"#
                )
                .bind(podcast_id)
                .bind(user_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    let mut details = serde_json::Map::new();
                    
                    details.insert("podcastname".to_string(), serde_json::Value::String(row.try_get::<String, _>("podcastname").unwrap_or_default()));
                    details.insert("feedurl".to_string(), serde_json::Value::String(row.try_get::<String, _>("feedurl").unwrap_or_default()));
                    details.insert("description".to_string(), serde_json::Value::String(row.try_get::<String, _>("description").unwrap_or_default()));
                    details.insert("author".to_string(), serde_json::Value::String(row.try_get::<String, _>("author").unwrap_or_default()));
                    details.insert("artworkurl".to_string(), serde_json::Value::String(row.try_get::<Option<String>, _>("artworkurl").unwrap_or_default().unwrap_or_default()));
                    details.insert("explicit".to_string(), serde_json::Value::Bool(row.try_get::<bool, _>("explicit").unwrap_or(false)));
                    details.insert("episodecount".to_string(), serde_json::Value::Number(serde_json::Number::from(row.try_get::<i32, _>("episodecount").unwrap_or(0))));
                    let categories_str = row.try_get::<String, _>("categories").unwrap_or_default();
                    let categories_parsed = self.parse_categories_json(&categories_str).unwrap_or_default();
                    details.insert("categories".to_string(), serde_json::to_value(categories_parsed).unwrap_or(serde_json::Value::Object(serde_json::Map::new())));
                    details.insert("websiteurl".to_string(), serde_json::Value::String(row.try_get::<String, _>("websiteurl").unwrap_or_default()));
                    details.insert("podcastindexid".to_string(), serde_json::Value::Number(serde_json::Number::from(row.try_get::<i32, _>("podcastindexid").unwrap_or(0))));
                    details.insert("isyoutubechannel".to_string(), serde_json::Value::Bool(row.try_get::<bool, _>("isyoutubechannel").unwrap_or(false)));

                    Ok(Some(serde_json::Value::Object(details)))
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query(
                    "SELECT * FROM Podcasts WHERE PodcastID = ? AND UserID = ?"
                )
                .bind(podcast_id)
                .bind(user_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    let mut details = serde_json::Map::new();
                    
                    details.insert("podcastname".to_string(), serde_json::Value::String(row.try_get::<String, _>("PodcastName").unwrap_or_default()));
                    details.insert("feedurl".to_string(), serde_json::Value::String(row.try_get::<String, _>("FeedURL").unwrap_or_default()));
                    details.insert("description".to_string(), serde_json::Value::String(row.try_get::<String, _>("Description").unwrap_or_default()));
                    details.insert("author".to_string(), serde_json::Value::String(row.try_get::<String, _>("Author").unwrap_or_default()));
                    details.insert("artworkurl".to_string(), serde_json::Value::String(row.try_get::<Option<String>, _>("ArtworkURL").unwrap_or_default().unwrap_or_default()));
                    details.insert("explicit".to_string(), serde_json::Value::Bool(row.try_get::<bool, _>("Explicit").unwrap_or(false)));
                    details.insert("episodecount".to_string(), serde_json::Value::Number(serde_json::Number::from(row.try_get::<i32, _>("EpisodeCount").unwrap_or(0))));
                    let categories_str = row.try_get::<String, _>("Categories").unwrap_or_default();
                    let categories_parsed = self.parse_categories_json(&categories_str).unwrap_or_default();
                    details.insert("categories".to_string(), serde_json::to_value(categories_parsed).unwrap_or(serde_json::Value::Object(serde_json::Map::new())));
                    details.insert("websiteurl".to_string(), serde_json::Value::String(row.try_get::<String, _>("WebsiteURL").unwrap_or_default()));
                    details.insert("podcastindexid".to_string(), serde_json::Value::Number(serde_json::Number::from(row.try_get::<i32, _>("PodcastIndexID").unwrap_or(0))));
                    details.insert("isyoutubechannel".to_string(), serde_json::Value::Bool(row.try_get::<bool, _>("IsYouTubeChannel").unwrap_or(false)));

                    Ok(Some(serde_json::Value::Object(details)))
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Get podcast values from feed - for get_podcast_details_dynamic when podcast is not added
    pub async fn get_podcast_values_from_feed(&self, feed_url: &str, user_id: i32, _display_only: bool) -> AppResult<serde_json::Value> {
        // Use the real get_podcast_values function that exists in the codebase
        let podcast_values = self.get_podcast_values(feed_url, user_id, None, None).await?;
        
        // Convert HashMap to the expected JSON format for get_podcast_details_dynamic
        let response = serde_json::json!({
            "pod_title": podcast_values.get("podcastname").unwrap_or(&"Unknown Podcast".to_string()),
            "pod_feed_url": feed_url,
            "pod_description": podcast_values.get("description").unwrap_or(&"".to_string()),
            "pod_author": podcast_values.get("author").unwrap_or(&"Unknown Author".to_string()),
            "pod_artwork": podcast_values.get("artworkurl").unwrap_or(&"/static/assets/default-podcast.png".to_string()),
            "pod_explicit": podcast_values.get("explicit").unwrap_or(&"False".to_string()) == "True",
            "pod_episode_count": podcast_values.get("episodecount").unwrap_or(&"0".to_string()).parse::<i32>().unwrap_or(0),
            "categories": podcast_values.get("categories").unwrap_or(&"{}".to_string()),
            "pod_website": podcast_values.get("websiteurl").unwrap_or(&"".to_string()),
        });

        Ok(response)
    }

    // Update feed cutoff days - for update_feed_cutoff_days endpoint
    pub async fn update_feed_cutoff_days(&self, podcast_id: i32, user_id: i32, feed_cutoff_days: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                // First verify podcast exists and belongs to user
                let existing = sqlx::query(r#"SELECT podcastid FROM "Podcasts" WHERE podcastid = $1 AND userid = $2"#)
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                if existing.is_none() {
                    return Ok(false);
                }

                // Update the feed cutoff days
                let result = sqlx::query(r#"UPDATE "Podcasts" SET feedcutoffdays = $1 WHERE podcastid = $2 AND userid = $3"#)
                    .bind(feed_cutoff_days)
                    .bind(podcast_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;

                Ok(result.rows_affected() > 0)
            }
            DatabasePool::MySQL(pool) => {
                // First verify podcast exists and belongs to user
                let existing = sqlx::query("SELECT PodcastID FROM Podcasts WHERE PodcastID = ? AND UserID = ?")
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;

                if existing.is_none() {
                    return Ok(false);
                }

                // Update the feed cutoff days
                let result = sqlx::query("UPDATE Podcasts SET FeedCutoffDays = ? WHERE PodcastID = ? AND UserID = ?")
                    .bind(feed_cutoff_days)
                    .bind(podcast_id)
                    .bind(user_id)
                    .execute(pool)
                    .await?;

                Ok(result.rows_affected() > 0)
            }
        }
    }

    // Update podcast basic info - for edit podcast functionality
    pub async fn update_podcast_info(&self, podcast_id: i32, user_id: i32, feed_url: Option<String>, username: Option<String>, password: Option<String>, podcast_name: Option<String>, description: Option<String>, author: Option<String>, artwork_url: Option<String>, website_url: Option<String>, podcast_index_id: Option<i64>) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                // First verify podcast exists and belongs to user
                let existing = sqlx::query(r#"SELECT podcastid FROM "Podcasts" WHERE podcastid = $1 AND userid = $2"#)
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                if existing.is_none() {
                    return Ok(false);
                }

                // Build dynamic update query based on provided fields
                let mut update_parts = Vec::new();
                let mut bind_count = 1;
                
                if feed_url.is_some() {
                    update_parts.push(format!("feedurl = ${}", bind_count));
                    bind_count += 1;
                }
                if username.is_some() {
                    update_parts.push(format!("username = ${}", bind_count));
                    bind_count += 1;
                }
                if password.is_some() {
                    update_parts.push(format!("password = ${}", bind_count));
                    bind_count += 1;
                }
                if podcast_name.is_some() {
                    update_parts.push(format!("podcastname = ${}", bind_count));
                    bind_count += 1;
                }
                if description.is_some() {
                    update_parts.push(format!("description = ${}", bind_count));
                    bind_count += 1;
                }
                if author.is_some() {
                    update_parts.push(format!("author = ${}", bind_count));
                    bind_count += 1;
                }
                if artwork_url.is_some() {
                    update_parts.push(format!("artworkurl = ${}", bind_count));
                    bind_count += 1;
                }
                if website_url.is_some() {
                    update_parts.push(format!("websiteurl = ${}", bind_count));
                    bind_count += 1;
                }
                if podcast_index_id.is_some() {
                    update_parts.push(format!("podcastindexid = ${}", bind_count));
                    bind_count += 1;
                }

                if update_parts.is_empty() {
                    return Ok(false);
                }

                let query_str = format!(
                    r#"UPDATE "Podcasts" SET {} WHERE podcastid = ${} AND userid = ${}"#,
                    update_parts.join(", "),
                    bind_count,
                    bind_count + 1
                );

                let mut query = sqlx::query(&query_str);
                
                if let Some(url) = feed_url {
                    query = query.bind(url);
                }
                if let Some(uname) = username {
                    query = query.bind(uname);
                }
                if let Some(pwd) = password {
                    query = query.bind(pwd);
                }
                if let Some(name) = podcast_name {
                    query = query.bind(name);
                }
                if let Some(desc) = description {
                    query = query.bind(desc);
                }
                if let Some(auth) = author {
                    query = query.bind(auth);
                }
                if let Some(artwork) = artwork_url {
                    query = query.bind(artwork);
                }
                if let Some(website) = website_url {
                    query = query.bind(website);
                }
                if let Some(idx_id) = podcast_index_id {
                    query = query.bind(idx_id);
                }
                
                query = query.bind(podcast_id).bind(user_id);
                
                let result = query.execute(pool).await?;
                Ok(result.rows_affected() > 0)
            }
            DatabasePool::MySQL(pool) => {
                // First verify podcast exists and belongs to user
                let existing = sqlx::query("SELECT PodcastID FROM Podcasts WHERE PodcastID = ? AND UserID = ?")
                    .bind(podcast_id)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                if existing.is_none() {
                    return Ok(false);
                }

                // Build dynamic update query based on provided fields
                let mut update_parts = Vec::new();
                
                if feed_url.is_some() {
                    update_parts.push("FeedURL = ?");
                }
                if username.is_some() {
                    update_parts.push("Username = ?");
                }
                if password.is_some() {
                    update_parts.push("Password = ?");
                }
                if podcast_name.is_some() {
                    update_parts.push("PodcastName = ?");
                }
                if description.is_some() {
                    update_parts.push("Description = ?");
                }
                if author.is_some() {
                    update_parts.push("Author = ?");
                }
                if artwork_url.is_some() {
                    update_parts.push("ArtworkURL = ?");
                }
                if website_url.is_some() {
                    update_parts.push("WebsiteURL = ?");
                }
                if podcast_index_id.is_some() {
                    update_parts.push("PodcastIndexID = ?");
                }

                if update_parts.is_empty() {
                    return Ok(false);
                }

                let query_str = format!(
                    "UPDATE Podcasts SET {} WHERE PodcastID = ? AND UserID = ?",
                    update_parts.join(", ")
                );

                let mut query = sqlx::query(&query_str);
                
                if let Some(url) = feed_url {
                    query = query.bind(url);
                }
                if let Some(uname) = username {
                    query = query.bind(uname);
                }
                if let Some(pwd) = password {
                    query = query.bind(pwd);
                }
                if let Some(name) = podcast_name {
                    query = query.bind(name);
                }
                if let Some(desc) = description {
                    query = query.bind(desc);
                }
                if let Some(auth) = author {
                    query = query.bind(auth);
                }
                if let Some(artwork) = artwork_url {
                    query = query.bind(artwork);
                }
                if let Some(website) = website_url {
                    query = query.bind(website);
                }
                if let Some(idx_id) = podcast_index_id {
                    query = query.bind(idx_id);
                }
                
                query = query.bind(podcast_id).bind(user_id);
                
                let result = query.execute(pool).await?;
                Ok(result.rows_affected() > 0)
            }
        }
    }

    // Bulk episode operations for efficient batch processing
    pub async fn bulk_mark_episodes_completed(&self, episode_ids: Vec<i32>, user_id: i32, is_youtube: bool) -> AppResult<(i32, i32)> {
        if episode_ids.is_empty() {
            return Ok((0, 0));
        }

        let mut processed = 0;
        let mut failed = 0;

        match self {
            DatabasePool::Postgres(pool) => {
                let mut tx = pool.begin().await?;
                
                if is_youtube {
                    for episode_id in episode_ids {
                        match self.mark_episode_completed(episode_id, user_id, is_youtube).await {
                            Ok(_) => processed += 1,
                            Err(_) => failed += 1,
                        }
                    }
                } else {
                    // Batch update regular episodes
                    let episode_ids_str: Vec<String> = episode_ids.iter().map(|id| id.to_string()).collect();
                    let ids_clause = episode_ids_str.join(",");
                    
                    let query = format!(
                        r#"UPDATE "Episodes" SET completed = TRUE WHERE episodeid IN ({})"#,
                        ids_clause
                    );
                    
                    let result = sqlx::query(&query).execute(&mut *tx).await?;
                    processed = result.rows_affected() as i32;
                }
                
                tx.commit().await?;
            }
            DatabasePool::MySQL(pool) => {
                let mut tx = pool.begin().await?;
                
                if is_youtube {
                    for episode_id in episode_ids {
                        match self.mark_episode_completed(episode_id, user_id, is_youtube).await {
                            Ok(_) => processed += 1,
                            Err(_) => failed += 1,
                        }
                    }
                } else {
                    // Batch update regular episodes
                    let episode_ids_str: Vec<String> = episode_ids.iter().map(|id| id.to_string()).collect();
                    let ids_clause = episode_ids_str.join(",");
                    
                    let query = format!(
                        "UPDATE Episodes SET Completed = TRUE WHERE EpisodeID IN ({})",
                        ids_clause
                    );
                    
                    let result = sqlx::query(&query).execute(&mut *tx).await?;
                    processed = result.rows_affected() as i32;
                }
                
                tx.commit().await?;
            }
        }

        Ok((processed, failed))
    }

    pub async fn bulk_save_episodes(&self, episode_ids: Vec<i32>, user_id: i32, is_youtube: bool) -> AppResult<(i32, i32)> {
        if episode_ids.is_empty() {
            return Ok((0, 0));
        }

        let mut processed = 0;
        let mut failed = 0;

        match self {
            DatabasePool::Postgres(pool) => {
                let mut tx = pool.begin().await?;
                
                if is_youtube {
                    for episode_id in episode_ids {
                        // Check if already saved to avoid duplicates
                        let existing = sqlx::query(
                            r#"SELECT "SaveID" FROM "SavedVideos" WHERE "VideoID" = $1 AND "UserID" = $2"#
                        )
                        .bind(episode_id)
                        .bind(user_id)
                        .fetch_optional(&mut *tx)
                        .await?;
                        
                        if existing.is_none() {
                            match sqlx::query(
                                r#"INSERT INTO "SavedVideos" ("VideoID", "UserID") VALUES ($1, $2)"#
                            )
                            .bind(episode_id)
                            .bind(user_id)
                            .execute(&mut *tx)
                            .await {
                                Ok(_) => processed += 1,
                                Err(_) => failed += 1,
                            }
                        }
                    }
                } else {
                    for episode_id in episode_ids {
                        // Check if already saved to avoid duplicates  
                        let existing = sqlx::query(
                            r#"SELECT saveid FROM "SavedEpisodes" WHERE episodeid = $1 AND userid = $2"#
                        )
                        .bind(episode_id)
                        .bind(user_id)
                        .fetch_optional(&mut *tx)
                        .await?;
                        
                        if existing.is_none() {
                            match sqlx::query(
                                r#"INSERT INTO "SavedEpisodes" (episodeid, userid) VALUES ($1, $2)"#
                            )
                            .bind(episode_id)
                            .bind(user_id)
                            .execute(&mut *tx)
                            .await {
                                Ok(_) => processed += 1,
                                Err(_) => failed += 1,
                            }
                        }
                    }
                }
                
                tx.commit().await?;
            }
            DatabasePool::MySQL(pool) => {
                let mut tx = pool.begin().await?;
                
                if is_youtube {
                    for episode_id in episode_ids {
                        let existing = sqlx::query(
                            "SELECT SaveID FROM SavedVideos WHERE VideoID = ? AND UserID = ?"
                        )
                        .bind(episode_id)
                        .bind(user_id)
                        .fetch_optional(&mut *tx)
                        .await?;
                        
                        if existing.is_none() {
                            match sqlx::query(
                                "INSERT INTO SavedVideos (VideoID, UserID) VALUES (?, ?)"
                            )
                            .bind(episode_id)
                            .bind(user_id)
                            .execute(&mut *tx)
                            .await {
                                Ok(_) => processed += 1,
                                Err(_) => failed += 1,
                            }
                        }
                    }
                } else {
                    for episode_id in episode_ids {
                        let existing = sqlx::query(
                            "SELECT SaveID FROM SavedEpisodes WHERE EpisodeID = ? AND UserID = ?"
                        )
                        .bind(episode_id)
                        .bind(user_id)
                        .fetch_optional(&mut *tx)
                        .await?;
                        
                        if existing.is_none() {
                            match sqlx::query(
                                "INSERT INTO SavedEpisodes (EpisodeID, UserID) VALUES (?, ?)"
                            )
                            .bind(episode_id)
                            .bind(user_id)
                            .execute(&mut *tx)
                            .await {
                                Ok(_) => processed += 1,
                                Err(_) => failed += 1,
                            }
                        }
                    }
                }
                
                tx.commit().await?;
            }
        }

        Ok((processed, failed))
    }

    pub async fn bulk_queue_episodes(&self, episode_ids: Vec<i32>, user_id: i32, is_youtube: bool) -> AppResult<(i32, i32)> {
        if episode_ids.is_empty() {
            return Ok((0, 0));
        }

        let mut processed = 0;
        let mut failed = 0;

        match self {
            DatabasePool::Postgres(pool) => {
                let mut tx = pool.begin().await?;
                
                if is_youtube {
                    for episode_id in episode_ids {
                        // Check if already queued to avoid duplicates
                        let existing = sqlx::query(
                            r#"SELECT "QueueID" FROM "QueuedVideos" WHERE "VideoID" = $1 AND "UserID" = $2"#
                        )
                        .bind(episode_id)
                        .bind(user_id)
                        .fetch_optional(&mut *tx)
                        .await?;
                        
                        if existing.is_none() {
                            match sqlx::query(
                                r#"INSERT INTO "QueuedVideos" ("VideoID", "UserID") VALUES ($1, $2)"#
                            )
                            .bind(episode_id)
                            .bind(user_id)
                            .execute(&mut *tx)
                            .await {
                                Ok(_) => processed += 1,
                                Err(_) => failed += 1,
                            }
                        }
                    }
                } else {
                    // Get max queue position for user
                    let max_pos_row = sqlx::query(
                        r#"SELECT COALESCE(MAX(queueposition), 0) as max_pos FROM "EpisodeQueue" WHERE userid = $1"#
                    )
                    .bind(user_id)
                    .fetch_one(&mut *tx)
                    .await?;
                    
                    let mut max_pos: i32 = max_pos_row.try_get("max_pos")?;
                    
                    for episode_id in episode_ids {
                        // Check if already queued to avoid duplicates
                        let existing = sqlx::query(
                            r#"SELECT queueid FROM "EpisodeQueue" WHERE episodeid = $1 AND userid = $2 AND is_youtube = $3"#
                        )
                        .bind(episode_id)
                        .bind(user_id)
                        .bind(is_youtube)
                        .fetch_optional(&mut *tx)
                        .await?;
                        
                        if existing.is_none() {
                            max_pos += 1;
                            match sqlx::query(
                                r#"INSERT INTO "EpisodeQueue" (episodeid, userid, queueposition, is_youtube) VALUES ($1, $2, $3, $4)"#
                            )
                            .bind(episode_id)
                            .bind(user_id)
                            .bind(max_pos)
                            .bind(is_youtube)
                            .execute(&mut *tx)
                            .await {
                                Ok(_) => processed += 1,
                                Err(_) => failed += 1,
                            }
                        }
                    }
                }
                
                tx.commit().await?;
            }
            DatabasePool::MySQL(pool) => {
                let mut tx = pool.begin().await?;
                
                if is_youtube {
                    for episode_id in episode_ids {
                        let existing = sqlx::query(
                            "SELECT QueueID FROM QueuedVideos WHERE VideoID = ? AND UserID = ?"
                        )
                        .bind(episode_id)
                        .bind(user_id)
                        .fetch_optional(&mut *tx)
                        .await?;
                        
                        if existing.is_none() {
                            match sqlx::query(
                                "INSERT INTO QueuedVideos (VideoID, UserID) VALUES (?, ?)"
                            )
                            .bind(episode_id)
                            .bind(user_id)
                            .execute(&mut *tx)
                            .await {
                                Ok(_) => processed += 1,
                                Err(_) => failed += 1,
                            }
                        }
                    }
                } else {
                    // Get max queue position for user
                    let max_pos_row = sqlx::query(
                        "SELECT COALESCE(MAX(QueuePosition), 0) as max_pos FROM EpisodeQueue WHERE UserID = ?"
                    )
                    .bind(user_id)
                    .fetch_one(&mut *tx)
                    .await?;
                    
                    let mut max_pos: i32 = max_pos_row.try_get("max_pos")?;
                    
                    for episode_id in episode_ids {
                        let existing = sqlx::query(
                            "SELECT QueueID FROM EpisodeQueue WHERE EpisodeID = ? AND UserID = ? AND is_youtube = ?"
                        )
                        .bind(episode_id)
                        .bind(user_id)
                        .bind(is_youtube)
                        .fetch_optional(&mut *tx)
                        .await?;
                        
                        if existing.is_none() {
                            max_pos += 1;
                            match sqlx::query(
                                "INSERT INTO EpisodeQueue (EpisodeID, UserID, QueuePosition, is_youtube) VALUES (?, ?, ?, ?)"
                            )
                            .bind(episode_id)
                            .bind(user_id)
                            .bind(max_pos)
                            .bind(is_youtube)
                            .execute(&mut *tx)
                            .await {
                                Ok(_) => processed += 1,
                                Err(_) => failed += 1,
                            }
                        }
                    }
                }
                
                tx.commit().await?;
            }
        }

        Ok((processed, failed))
    }

    // Bulk delete downloaded episodes - efficient batch processing for mass deletion
    pub async fn bulk_delete_downloaded_episodes(&self, episode_ids: Vec<i32>, user_id: i32, is_youtube: bool) -> AppResult<(i32, i32)> {
        if episode_ids.is_empty() {
            return Ok((0, 0));
        }

        let mut processed = 0;
        let mut failed = 0;

        match self {
            DatabasePool::Postgres(pool) => {
                let mut tx = pool.begin().await?;
                
                if is_youtube {
                    // Delete YouTube videos from DownloadedEpisodes (they use the same table but different logic)
                    for episode_id in episode_ids {
                        match sqlx::query(
                            r#"DELETE FROM "DownloadedEpisodes" WHERE episodeid = $1 AND userid = $2"#
                        )
                        .bind(episode_id)
                        .bind(user_id)
                        .execute(&mut *tx)
                        .await {
                            Ok(result) => {
                                if result.rows_affected() > 0 {
                                    processed += 1;
                                } else {
                                    failed += 1; // Episode wasn't downloaded by this user
                                }
                            },
                            Err(_) => failed += 1,
                        }
                    }
                } else {
                    // Batch delete regular episodes using IN clause for efficiency
                    let episode_ids_str: Vec<String> = episode_ids.iter().map(|id| id.to_string()).collect();
                    let ids_clause = episode_ids_str.join(",");
                    
                    let query = format!(
                        r#"DELETE FROM "DownloadedEpisodes" WHERE episodeid IN ({}) AND userid = $1"#,
                        ids_clause
                    );
                    
                    let result = sqlx::query(&query)
                        .bind(user_id)
                        .execute(&mut *tx)
                        .await?;
                    processed = result.rows_affected() as i32;
                    failed = episode_ids.len() as i32 - processed; // Assume failures are episodes not found
                }
                
                tx.commit().await?;
            }
            DatabasePool::MySQL(pool) => {
                let mut tx = pool.begin().await?;
                
                if is_youtube {
                    // Delete YouTube videos from DownloadedEpisodes 
                    for episode_id in episode_ids {
                        match sqlx::query(
                            "DELETE FROM DownloadedEpisodes WHERE EpisodeID = ? AND UserID = ?"
                        )
                        .bind(episode_id)
                        .bind(user_id)
                        .execute(&mut *tx)
                        .await {
                            Ok(result) => {
                                if result.rows_affected() > 0 {
                                    processed += 1;
                                } else {
                                    failed += 1; // Episode wasn't downloaded by this user
                                }
                            },
                            Err(_) => failed += 1,
                        }
                    }
                } else {
                    // Batch delete regular episodes using IN clause for efficiency
                    let episode_ids_str: Vec<String> = episode_ids.iter().map(|id| id.to_string()).collect();
                    let ids_clause = episode_ids_str.join(",");
                    
                    let query = format!(
                        "DELETE FROM DownloadedEpisodes WHERE EpisodeID IN ({}) AND UserID = ?",
                        ids_clause
                    );
                    
                    let result = sqlx::query(&query)
                        .bind(user_id)
                        .execute(&mut *tx)
                        .await?;
                    processed = result.rows_affected() as i32;
                    failed = episode_ids.len() as i32 - processed; // Assume failures are episodes not found
                }
                
                tx.commit().await?;
            }
        }

        Ok((processed, failed))
    }

    // Set up internal gpodder sync - matches Python set_gpodder_internal_sync function exactly
    pub async fn set_gpodder_internal_sync(&self, user_id: i32) -> AppResult<serde_json::Value> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Get the username and current sync type
                let user_row = sqlx::query(r#"SELECT username, pod_sync_type FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                let (username, current_sync_type) = if let Some(row) = user_row {
                    let username: String = row.try_get("username")?;
                    let sync_type: Option<String> = row.try_get("pod_sync_type")?;
                    (username, sync_type.unwrap_or_else(|| "None".to_string()))
                } else {
                    return Err(AppError::not_found("User not found"));
                };

                // Generate new sync type based on current
                let new_sync_type = match current_sync_type.as_str() {
                    "external" => "both",
                    "None" | "" => "gpodder",
                    _ => &current_sync_type,
                };

                // Generate a secure internal token (64 characters alphanumeric)
                use rand::{distr::Alphanumeric, Rng};
                let internal_token: String = rand::rng()
                    .sample_iter(&Alphanumeric)
                    .take(64)
                    .map(char::from)
                    .collect();

                let local_gpodder_url = "http://localhost:8042";

                // Create default device name
                let default_device_name = format!("pinepods-internal-{}", user_id);

                // Update user with internal gpodder settings and set default device
                sqlx::query(r#"
                    UPDATE "Users" 
                    SET gpodderurl = $1, gpoddertoken = $2, gpodderloginname = $3, pod_sync_type = $4, defaultgpodderdevice = $6
                    WHERE userid = $5
                "#)
                .bind(local_gpodder_url)
                .bind(&internal_token)
                .bind(&username)
                .bind(new_sync_type)
                .bind(user_id)
                .bind(&default_device_name)
                .execute(pool)
                .await?;
                
                // Create device via gPodder API (matches Python version exactly)
                let device_result = match self.create_device_via_gpodder_api(local_gpodder_url, &username, &internal_token, &default_device_name).await {
                    Ok(device_id) => {
                        serde_json::json!({
                            "device_name": default_device_name,
                            "device_id": device_id,
                            "success": true
                        })
                    }
                    Err(e) => {
                        tracing::warn!("Failed to create device via API: {}, continuing anyway", e);
                        // Even if device creation fails, still return success (matches Python behavior)
                        serde_json::json!({
                            "device_name": default_device_name,
                            "device_id": user_id,
                            "success": true
                        })
                    }
                };
                
                // Perform initial full sync to get ALL user subscriptions from all devices
                if let Err(e) = self.call_gpodder_initial_full_sync(user_id, local_gpodder_url, &username, &internal_token, &default_device_name).await {
                    tracing::warn!("Initial GPodder full sync failed during setup: {}", e);
                    // Don't fail setup if initial sync fails
                }
                
                Ok(device_result)
            }
            DatabasePool::MySQL(pool) => {
                // Get the username and current sync type
                let user_row = sqlx::query("SELECT Username, Pod_Sync_Type FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                let (username, current_sync_type) = if let Some(row) = user_row {
                    let username: String = row.try_get("Username")?;
                    let sync_type: Option<String> = row.try_get("Pod_Sync_Type")?;
                    (username, sync_type.unwrap_or_else(|| "None".to_string()))
                } else {
                    return Err(AppError::not_found("User not found"));
                };

                // Generate new sync type based on current
                let new_sync_type = match current_sync_type.as_str() {
                    "external" => "both",
                    "None" | "" => "gpodder",
                    _ => &current_sync_type,
                };

                // Generate a secure internal token (64 characters alphanumeric)
                use rand::{distr::Alphanumeric, Rng};
                let internal_token: String = rand::rng()
                    .sample_iter(&Alphanumeric)
                    .take(64)
                    .map(char::from)
                    .collect();

                let local_gpodder_url = "http://localhost:8042";

                // Create default device name
                let default_device_name = format!("pinepods-internal-{}", user_id);

                // Update user with internal gpodder settings and set default device
                sqlx::query("
                    UPDATE Users 
                    SET GpodderUrl = ?, GpodderToken = ?, GpodderLoginName = ?, Pod_Sync_Type = ?, DefaultGpodderDevice = ?
                    WHERE UserID = ?
                ")
                .bind(local_gpodder_url)
                .bind(&internal_token)
                .bind(&username)
                .bind(new_sync_type)
                .bind(&default_device_name)
                .bind(user_id)
                .execute(pool)
                .await?;
                
                // Create device via gPodder API (matches Python version exactly)
                let device_result = match self.create_device_via_gpodder_api(local_gpodder_url, &username, &internal_token, &default_device_name).await {
                    Ok(device_id) => {
                        serde_json::json!({
                            "device_name": default_device_name,
                            "device_id": device_id,
                            "success": true
                        })
                    }
                    Err(e) => {
                        tracing::warn!("Failed to create device via API: {}, continuing anyway", e);
                        // Even if device creation fails, still return success (matches Python behavior)
                        serde_json::json!({
                            "device_name": default_device_name,
                            "device_id": user_id,
                            "success": true
                        })
                    }
                };
                
                // Perform initial full sync to get ALL user subscriptions from all devices
                if let Err(e) = self.call_gpodder_initial_full_sync(user_id, local_gpodder_url, &username, &internal_token, &default_device_name).await {
                    tracing::warn!("Initial GPodder full sync failed during setup: {}", e);
                    // Don't fail setup if initial sync fails
                }
                
                Ok(device_result)
            }
        }
    }

    // Disable internal gpodder sync - matches Python disable_gpodder_internal_sync function exactly
    pub async fn disable_gpodder_internal_sync(&self, user_id: i32) -> AppResult<bool> {
        // Get current user gpodder status
        let user_status = self.gpodder_get_status(user_id).await?;
        let current_sync_type = &user_status.sync_type;

        // Determine new sync type
        let new_sync_type = match current_sync_type.as_str() {
            "both" => "external",
            "gpodder" => "None",
            _ => current_sync_type,
        };

        match self {
            DatabasePool::Postgres(pool) => {
                // If internal API is being used, clear the settings
                if user_status.gpodder_url.as_deref() == Some("http://localhost:8042") {
                    sqlx::query(r#"
                        UPDATE "Users" 
                        SET gpodderurl = '', gpoddertoken = '', gpodderloginname = '', pod_sync_type = $1
                        WHERE userid = $2
                    "#)
                    .bind(new_sync_type)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                } else {
                    // Just update the sync type
                    sqlx::query(r#"UPDATE "Users" SET pod_sync_type = $1 WHERE userid = $2"#)
                        .bind(new_sync_type)
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                }
            }
            DatabasePool::MySQL(pool) => {
                // If internal API is being used, clear the settings
                if user_status.gpodder_url.as_deref() == Some("http://localhost:8042") {
                    sqlx::query("
                        UPDATE Users 
                        SET GpodderUrl = '', GpodderToken = '', GpodderLoginName = '', Pod_Sync_Type = ?
                        WHERE UserID = ?
                    ")
                    .bind(new_sync_type)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                } else {
                    // Just update the sync type
                    sqlx::query("UPDATE Users SET Pod_Sync_Type = ? WHERE UserID = ?")
                        .bind(new_sync_type)
                        .bind(user_id)
                        .execute(pool)
                        .await?;
                }
            }
        }

        Ok(true)
    }

    // Helper function to create device via gPodder API - matches Python create device logic exactly
    pub async fn create_device_via_gpodder_api(&self, gpodder_url: &str, username: &str, token: &str, device_name: &str) -> AppResult<String> {
        use reqwest;
        use serde_json;
        
        // Use correct authentication based on internal vs external
        let (client, auth_method) = if gpodder_url == "http://localhost:8042" {
            // Internal GPodder API - use X-GPodder-Token header
            let client = reqwest::Client::new();
            (client, "internal")
        } else {
            // External GPodder API - use session auth with basic fallback
            let decrypted_password = self.decrypt_password(token).await?;
            let session = self.create_gpodder_session_with_password(gpodder_url, username, &decrypted_password).await?;
            (session.client, "external")
        };
        
        // First, check if device already exists
        let device_list_url = format!("{}/api/2/devices/{}.json", gpodder_url.trim_end_matches('/'), username);
        
        let response = if auth_method == "internal" {
            client.get(&device_list_url)
                .header("X-GPodder-Token", token)
                .send()
                .await
        } else {
            // External - session auth with basic fallback handled by session client
            let decrypted_password = self.decrypt_password(token).await?;
            client.get(&device_list_url)
                .basic_auth(username, Some(&decrypted_password))
                .send()
                .await
        };
        
        match response {
            Ok(response) if response.status().is_success() => {
                if let Ok(devices) = response.json::<Vec<serde_json::Value>>().await {
                    for device in devices {
                        if device.get("id").and_then(|v| v.as_str()) == Some(device_name) {
                            tracing::info!("Found existing device with ID: {}", device_name);
                            return Ok(device_name.to_string());
                        }
                    }
                }
            }
            Ok(response) => {
                tracing::warn!("Failed to fetch device list: {}", response.status());
            }
            Err(e) => {
                tracing::warn!("Error fetching device list: {}", e);
            }
        }
        
        // Device doesn't exist, create it
        let device_url = format!("{}/api/2/devices/{}/{}.json", gpodder_url.trim_end_matches('/'), username, device_name);
        let device_data = serde_json::json!({
            "caption": format!("PinePods Internal Device {}", device_name.split('-').last().unwrap_or("unknown")),
            "type": "server"
        });
        
        let create_response = if auth_method == "internal" {
            client.post(&device_url)
                .header("X-GPodder-Token", token)
                .json(&device_data)
                .send()
                .await
        } else {
            let decrypted_password = self.decrypt_password(token).await?;
            client.post(&device_url)
                .basic_auth(username, Some(&decrypted_password))
                .json(&device_data)
                .send()
                .await
        };
        
        match create_response
        {
            Ok(response) if response.status().is_success() => {
                tracing::info!("Created device with ID: {}", device_name);
                Ok(device_name.to_string())
            }
            Ok(response) => {
                let status = response.status();
                let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
                Err(AppError::internal(&format!("Failed to create device: {} - {}", status, error_text)))
            }
            Err(e) => {
                Err(AppError::internal(&format!("Error creating device via API: {}", e)))
            }
        }
    }

    // Background task for GPodder subscription refresh - matches Python refresh_gpodder_subscription_for_background
    pub async fn refresh_gpodder_subscription_background(&self, user_id: i32) -> AppResult<bool> {
        // Get user sync settings
        let settings_opt = self.get_user_sync_settings(user_id).await?;
        let settings = match settings_opt {
            Some(s) => s,
            None => return Ok(false), // No sync configured
        };

        // Get default device
        let device_name = match self.get_default_gpodder_device_name(user_id).await? {
            Some(name) => name,
            None => format!("pinepods-internal-{}", user_id), // Fallback device name
        };

        // Call the appropriate sync method based on sync type
        match settings.sync_type.as_str() {
            "gpodder" => {
                // Internal GPodder API - token is already unencrypted for internal use
                self.call_gpodder_service_sync(user_id, "http://localhost:8042", &settings.username, &settings.token, &device_name, false).await
            }
            "external" => {
                // External GPodder server - decrypt token using existing encryption system
                let decrypted_token = match self.decrypt_gpodder_token(&settings.token).await {
                    Ok(token) => token,
                    Err(_) => settings.token.clone(), // Fallback to original token if decryption fails
                };
                self.call_gpodder_service_sync(user_id, &settings.url, &settings.username, &decrypted_token, &device_name, false).await
            }
            "both" => {
                // Both internal and external
                let internal_result = self.call_gpodder_service_sync(user_id, "http://localhost:8042", &settings.username, &settings.token, &device_name, false).await?;
                let decrypted_token = match self.decrypt_gpodder_token(&settings.token).await {
                    Ok(token) => token,
                    Err(_) => settings.token.clone(),
                };
                let external_result = self.call_gpodder_service_sync(user_id, &settings.url, &settings.username, &decrypted_token, &device_name, false).await?;
                Ok(internal_result || external_result)
            }
            "nextcloud" => {
                // Nextcloud sync - use existing nextcloud refresh functionality
                self.refresh_nextcloud_subscription_background(user_id).await
            }
            _ => Ok(false), // No sync or unsupported type
        }
    }

    // Helper to get default device name
    async fn get_default_gpodder_device_name(&self, user_id: i32) -> AppResult<Option<String>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT devicename FROM "GpodderDevices" WHERE userid = $1 AND isdefault = true LIMIT 1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(row.and_then(|r| r.try_get("devicename").ok()))
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT DeviceName FROM GpodderDevices WHERE UserID = ? AND IsDefault = 1 LIMIT 1")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                Ok(row.and_then(|r| r.try_get("DeviceName").ok()))
            }
        }
    }

    // Decrypt GPodder token using existing encryption system - matches Python token decryption
    // Get comprehensive GPodder server statistics by calling actual GPodder API endpoints
    pub async fn get_gpodder_server_statistics(&self, user_id: i32) -> AppResult<crate::handlers::sync::GpodderStatistics> {
        use crate::handlers::sync::{GpodderStatistics, ServerDevice, ServerSubscription, ServerEpisodeAction, EndpointTest};
        use std::time::Instant;
        
        // Get user's sync settings using the same method as sync operations
        let sync_settings = self.get_user_sync_settings(user_id).await?;
        let settings = match sync_settings {
            Some(s) => s,
            None => {
                return Ok(GpodderStatistics {
                    server_url: "No sync configured".to_string(),
                    sync_type: "None".to_string(),
                    sync_enabled: false,
                    server_devices: vec![],
                    total_devices: 0,
                    server_subscriptions: vec![],
                    total_subscriptions: 0,
                    recent_episode_actions: vec![],
                    total_episode_actions: 0,
                    connection_status: "Not configured".to_string(),
                    last_sync_timestamp: None,
                    api_endpoints_tested: vec![],
                });
            }
        };

        // Use the same authentication logic as sync operations
        let (gpodder_url, username, password) = match settings.sync_type.as_str() {
            "gpodder" => {
                // Internal gPodder API - use token directly (no decryption needed)
                ("http://localhost:8042".to_string(), settings.username.clone(), settings.token.clone())
            }
            "external" => {
                // External gPodder server - decrypt token first
                let decrypted_token = self.decrypt_password(&settings.token).await?;
                (settings.url.clone(), settings.username.clone(), decrypted_token)
            }
            "nextcloud" => {
                // Nextcloud sync - decrypt token first
                let decrypted_token = self.decrypt_password(&settings.token).await?;
                (settings.url.clone(), settings.username.clone(), decrypted_token)
            }
            _ => {
                return Ok(GpodderStatistics {
                    server_url: settings.url.clone(),
                    sync_type: settings.sync_type.clone(),
                    sync_enabled: false,
                    server_devices: vec![],
                    total_devices: 0,
                    server_subscriptions: vec![],
                    total_subscriptions: 0,
                    recent_episode_actions: vec![],
                    total_episode_actions: 0,
                    connection_status: "Unsupported sync type".to_string(),
                    last_sync_timestamp: None,
                    api_endpoints_tested: vec![],
                });
            }
        };

        let mut api_endpoints_tested = Vec::new();
        let mut server_devices = Vec::new();
        let mut server_subscriptions = Vec::new();
        let mut recent_episode_actions = Vec::new();

        // Handle Nextcloud differently from standard GPodder API
        if settings.sync_type == "nextcloud" {
            // Nextcloud uses different endpoints and doesn't have devices concept
            let client = reqwest::Client::new();
            
            // Test 1: Get subscriptions from Nextcloud
            let subscriptions_url = format!("{}/index.php/apps/gpoddersync/subscriptions", gpodder_url.trim_end_matches('/'));
            let start = Instant::now();
            
            let subscriptions_response = client
                .get(&subscriptions_url)
                .basic_auth(&username, Some(&password))
                .send()
                .await;
            
            match subscriptions_response {
                Ok(resp) if resp.status().is_success() => {
                    let duration = start.elapsed().as_millis() as i64;
                    api_endpoints_tested.push(EndpointTest {
                        endpoint: "GET /index.php/apps/gpoddersync/subscriptions".to_string(),
                        status: "success".to_string(),
                        response_time_ms: Some(duration),
                        error: None,
                    });

                    match resp.json::<serde_json::Value>().await {
                        Ok(subs_data) => {
                            tracing::info!("Nextcloud subscriptions response: {:?}", subs_data);
                            if let Some(subs_array) = subs_data.as_array() {
                                tracing::info!("Found {} subscriptions in Nextcloud array", subs_array.len());
                                for sub in subs_array {
                                    if let Some(url) = sub.as_str() {
                                        server_subscriptions.push(ServerSubscription {
                                            url: url.to_string(),
                                            title: None,
                                            description: None,
                                        });
                                    }
                                }
                            } else {
                                tracing::warn!("Nextcloud subscriptions response is not an array: {:?}", subs_data);
                            }
                        }
                        Err(e) => {
                            tracing::warn!("Failed to parse Nextcloud subscriptions response: {}", e);
                        }
                    }
                }
                Ok(resp) => {
                    let duration = start.elapsed().as_millis() as i64;
                    api_endpoints_tested.push(EndpointTest {
                        endpoint: "GET /index.php/apps/gpoddersync/subscriptions".to_string(),
                        status: "failed".to_string(),
                        response_time_ms: Some(duration),
                        error: Some(format!("HTTP {}", resp.status())),
                    });
                }
                Err(e) => {
                    let duration = start.elapsed().as_millis() as i64;
                    api_endpoints_tested.push(EndpointTest {
                        endpoint: "GET /index.php/apps/gpoddersync/subscriptions".to_string(),
                        status: "failed".to_string(),
                        response_time_ms: Some(duration),
                        error: Some(e.to_string()),
                    });
                }
            }

            // Test 2: Get episode actions from Nextcloud
            let episode_actions_url = format!("{}/index.php/apps/gpoddersync/episode_action", gpodder_url.trim_end_matches('/'));
            let start = Instant::now();

            let episode_response = client
                .get(&episode_actions_url)
                .basic_auth(&username, Some(&password))
                .send()
                .await;

            match episode_response {
                Ok(resp) if resp.status().is_success() => {
                    let duration = start.elapsed().as_millis() as i64;
                    api_endpoints_tested.push(EndpointTest {
                        endpoint: "GET /index.php/apps/gpoddersync/episode_action".to_string(),
                        status: "success".to_string(),
                        response_time_ms: Some(duration),
                        error: None,
                    });

                    match resp.json::<serde_json::Value>().await {
                        Ok(episode_data) => {
                            if let Some(actions) = episode_data.get("actions").and_then(|v| v.as_array()) {
                                for action in actions.iter().take(10) { // Show last 10 actions
                                    recent_episode_actions.push(ServerEpisodeAction {
                                        podcast: action["podcast"].as_str().unwrap_or("").to_string(),
                                        episode: action["episode"].as_str().unwrap_or("").to_string(),
                                        action: action["action"].as_str().unwrap_or("").to_string(),
                                        timestamp: action["timestamp"].as_str().unwrap_or("").to_string(),
                                        position: action["position"].as_i64().map(|p| p as i32),
                                        device: Some("nextcloud".to_string()),
                                    });
                                }
                            }
                        }
                        Err(e) => {
                            tracing::warn!("Failed to parse Nextcloud episode actions response: {}", e);
                        }
                    }
                }
                Ok(resp) => {
                    let duration = start.elapsed().as_millis() as i64;
                    api_endpoints_tested.push(EndpointTest {
                        endpoint: "GET /index.php/apps/gpoddersync/episode_action".to_string(),
                        status: "failed".to_string(),
                        response_time_ms: Some(duration),
                        error: Some(format!("HTTP {}", resp.status())),
                    });
                }
                Err(e) => {
                    let duration = start.elapsed().as_millis() as i64;
                    api_endpoints_tested.push(EndpointTest {
                        endpoint: "GET /index.php/apps/gpoddersync/episode_action".to_string(),
                        status: "failed".to_string(),
                        response_time_ms: Some(duration),
                        error: Some(e.to_string()),
                    });
                }
            }

            // Nextcloud doesn't have devices concept, so add a fake device entry
            server_devices.push(ServerDevice {
                id: "nextcloud".to_string(),
                caption: "Nextcloud gPodder Sync".to_string(),
                device_type: "cloud".to_string(),
                subscriptions: server_subscriptions.len() as i32,
            });
        } else {
            // Standard GPodder API (internal or external)
            // Create GPodder session directly with already-decrypted password to avoid double decryption
            let session = self.create_gpodder_session_with_password(&gpodder_url, &username, &password).await?;

            // Test 1: Get devices from GPodder API
            let devices_url = format!("{}/api/2/devices/{}.json", gpodder_url.trim_end_matches('/'), username);
            let start = Instant::now();
        
        let devices_response = if session.authenticated {
            session.client.get(&devices_url).send().await
        } else {
            session.client.get(&devices_url).basic_auth(&username, Some(&password)).send().await
        };
        
        match devices_response
        {
            Ok(resp) if resp.status().is_success() => {
                let duration = start.elapsed().as_millis() as i64;
                api_endpoints_tested.push(EndpointTest {
                    endpoint: "GET /api/2/devices/{username}.json".to_string(),
                    status: "success".to_string(),
                    response_time_ms: Some(duration),
                    error: None,
                });

                match resp.json::<serde_json::Value>().await {
                    Ok(devices_data) => {
                        if let Some(devices_array) = devices_data.as_array() {
                            for device in devices_array {
                                server_devices.push(ServerDevice {
                                    id: device["id"].as_str().unwrap_or("unknown").to_string(),
                                    caption: device["caption"].as_str().unwrap_or("").to_string(),
                                    device_type: device["type"].as_str().unwrap_or("unknown").to_string(),
                                    subscriptions: device["subscriptions"].as_i64().unwrap_or(0) as i32,
                                });
                            }
                        }
                    }
                    Err(e) => {
                        tracing::warn!("Failed to parse devices response: {}", e);
                    }
                }
            }
            Ok(resp) => {
                let duration = start.elapsed().as_millis() as i64;
                api_endpoints_tested.push(EndpointTest {
                    endpoint: "GET /api/2/devices/{username}.json".to_string(),
                    status: "failed".to_string(),
                    response_time_ms: Some(duration),
                    error: Some(format!("HTTP {}", resp.status())),
                });
            }
            Err(e) => {
                let duration = start.elapsed().as_millis() as i64;
                api_endpoints_tested.push(EndpointTest {
                    endpoint: "GET /api/2/devices/{username}.json".to_string(),
                    status: "failed".to_string(),
                    response_time_ms: Some(duration),
                    error: Some(e.to_string()),
                });
            }
        }

        // Test 2: Get subscriptions from GPodder API - use the user's actual default device
        let device_name = self.get_or_create_default_device(user_id).await?;
        let subscriptions_url = format!("{}/api/2/subscriptions/{}/{}.json?since=0", 
            gpodder_url.trim_end_matches('/'), username, device_name);
        let start = Instant::now();

        let subscriptions_response = if session.authenticated {
            session.client.get(&subscriptions_url).send().await
        } else {
            session.client.get(&subscriptions_url).basic_auth(&username, Some(&password)).send().await
        };

        match subscriptions_response
        {
            Ok(resp) if resp.status().is_success() => {
                let duration = start.elapsed().as_millis() as i64;
                api_endpoints_tested.push(EndpointTest {
                    endpoint: "GET /api/2/subscriptions/{username}/{device}.json?since=0".to_string(),
                    status: "success".to_string(),
                    response_time_ms: Some(duration),
                    error: None,
                });

                match resp.json::<serde_json::Value>().await {
                    Ok(subs_data) => {
                        // GPodder API returns subscriptions in format: {"add": ["url1", "url2"], "remove": ["url3"]}
                        if let Some(add_array) = subs_data["add"].as_array() {
                            for sub in add_array {
                                if let Some(url) = sub.as_str() {
                                    server_subscriptions.push(ServerSubscription {
                                        url: url.to_string(),
                                        title: None,
                                        description: None,
                                    });
                                }
                            }
                        }
                    }
                    Err(e) => {
                        tracing::warn!("Failed to parse subscriptions response: {}", e);
                    }
                }
            }
            Ok(resp) => {
                let duration = start.elapsed().as_millis() as i64;
                api_endpoints_tested.push(EndpointTest {
                    endpoint: "GET /api/2/subscriptions/{username}/{device}.json?since=0".to_string(),
                    status: "failed".to_string(),
                    response_time_ms: Some(duration),
                    error: Some(format!("HTTP {}", resp.status())),
                });
            }
            Err(e) => {
                let duration = start.elapsed().as_millis() as i64;
                api_endpoints_tested.push(EndpointTest {
                    endpoint: "GET /api/2/subscriptions/{username}/{device}.json?since=0".to_string(),
                    status: "failed".to_string(),
                    response_time_ms: Some(duration),
                    error: Some(e.to_string()),
                });
            }
        }

        // Test 3: Get episode actions from GPodder API  
        let episodes_url = format!("{}/api/2/episodes/{}.json?since=0&device={}", 
            gpodder_url.trim_end_matches('/'), username, device_name);
        let start = Instant::now();

        let episodes_response = if session.authenticated {
            session.client.get(&episodes_url).send().await
        } else {
            session.client.get(&episodes_url).basic_auth(&username, Some(&password)).send().await
        };

        match episodes_response
        {
            Ok(resp) if resp.status().is_success() => {
                let duration = start.elapsed().as_millis() as i64;
                api_endpoints_tested.push(EndpointTest {
                    endpoint: "GET /api/2/episodes/{username}.json?since=0&device={device}".to_string(),
                    status: "success".to_string(),
                    response_time_ms: Some(duration),
                    error: None,
                });

                match resp.json::<serde_json::Value>().await {
                    Ok(episodes_data) => {
                        if let Some(actions) = episodes_data["actions"].as_array() {
                            for action in actions.iter().take(10) { // Show last 10 actions
                                recent_episode_actions.push(ServerEpisodeAction {
                                    podcast: action["podcast"].as_str().unwrap_or("").to_string(),
                                    episode: action["episode"].as_str().unwrap_or("").to_string(),
                                    action: action["action"].as_str().unwrap_or("").to_string(),
                                    timestamp: action["timestamp"].as_str().unwrap_or("").to_string(),
                                    position: action["position"].as_i64().map(|p| p as i32),
                                    device: action["device"].as_str().map(|s| s.to_string()),
                                });
                            }
                        }
                    }
                    Err(e) => {
                        tracing::warn!("Failed to parse episode actions response: {}", e);
                    }
                }
            }
            Ok(resp) => {
                let duration = start.elapsed().as_millis() as i64;
                api_endpoints_tested.push(EndpointTest {
                    endpoint: "GET /api/2/episodes/{username}.json?since=0&device={device}".to_string(),
                    status: "failed".to_string(),
                    response_time_ms: Some(duration),
                    error: Some(format!("HTTP {}", resp.status())),
                });
            }
            Err(e) => {
                let duration = start.elapsed().as_millis() as i64;
                api_endpoints_tested.push(EndpointTest {
                    endpoint: "GET /api/2/episodes/{username}.json?since=0&device={device}".to_string(),
                    status: "failed".to_string(),
                    response_time_ms: Some(duration),
                    error: Some(e.to_string()),
                });
            }
        }
        }

        // Get sync status
        let status = self.gpodder_get_status(user_id).await?;
        let last_sync = self.get_last_sync_timestamp(user_id).await?;
        
        // Determine overall connection status
        let connection_status = if api_endpoints_tested.iter().any(|t| t.status == "success") {
            if api_endpoints_tested.iter().all(|t| t.status == "success") {
                "All endpoints working"
            } else {
                "Partial connectivity"
            }
        } else {
            "Connection failed"
        };

        Ok(GpodderStatistics {
            server_url: gpodder_url,
            sync_type: status.sync_type.clone(),
            sync_enabled: status.sync_type != "None",
            server_devices: server_devices.clone(),
            total_devices: server_devices.len() as i32,
            server_subscriptions: server_subscriptions.clone(),
            total_subscriptions: server_subscriptions.len() as i32,
            recent_episode_actions: recent_episode_actions.clone(),
            total_episode_actions: recent_episode_actions.len() as i32,
            connection_status: connection_status.to_string(),
            last_sync_timestamp: last_sync.map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string()),
            api_endpoints_tested,
        })
    }

    async fn decrypt_gpodder_token(&self, encrypted_token: &str) -> AppResult<String> {
        // Get encryption key from app settings
        let encryption_key = match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT encryptionkey FROM "AppSettings" WHERE appsettingsid = 1"#)
                    .fetch_optional(pool)
                    .await?;
                
                row.and_then(|r| r.try_get("encryptionkey").ok())
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT EncryptionKey FROM AppSettings WHERE AppSettingsID = 1")
                    .fetch_optional(pool)
                    .await?;
                
                row.and_then(|r| r.try_get("EncryptionKey").ok())
            }
        };

        let encryption_key: String = encryption_key
            .ok_or_else(|| AppError::internal("Encryption key not found"))?;

        // Decrypt using Fernet (matches Python implementation)
        use fernet::Fernet;
        let fernet = match Fernet::new(&encryption_key) {
            Some(f) => f,
            None => return Err(AppError::internal("Failed to create Fernet cipher with provided key")),
        };
        
        let decrypted = fernet.decrypt(encrypted_token)
            .map_err(|e| AppError::internal(&format!("Failed to decrypt token: {}", e)))?;
        
        String::from_utf8(decrypted)
            .map_err(|e| AppError::internal(&format!("Failed to parse decrypted token: {}", e)))
    }

    // Nextcloud subscription refresh for background tasks - matches Python nextcloud refresh
    async fn refresh_nextcloud_subscription_background(&self, user_id: i32) -> AppResult<bool> {
        // Get user nextcloud settings
        let settings = match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT gpodderurl, gpoddertoken, gpodderloginname FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(r) = row {
                    (
                        r.try_get::<Option<String>, _>("gpodderurl")?.unwrap_or_default(),
                        r.try_get::<Option<String>, _>("gpoddertoken")?.unwrap_or_default(),
                        r.try_get::<Option<String>, _>("gpodderloginname")?.unwrap_or_default(),
                    )
                } else {
                    return Ok(false);
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT GpodderUrl, GpodderToken, GpodderLoginName FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(r) = row {
                    (
                        r.try_get::<Option<String>, _>("GpodderUrl")?.unwrap_or_default(),
                        r.try_get::<Option<String>, _>("GpodderToken")?.unwrap_or_default(),
                        r.try_get::<Option<String>, _>("GpodderLoginName")?.unwrap_or_default(),
                    )
                } else {
                    return Ok(false);
                }
            }
        };

        let (gpodder_url, _gpodder_token, gpodder_login) = settings;

        if gpodder_url.is_empty() || gpodder_login.is_empty() {
            return Ok(false);
        }

        // Call existing nextcloud sync functionality
        self.sync_with_nextcloud_for_user(user_id).await
    }

    // Get last sync timestamp for incremental sync - PROPER GPodder spec implementation
    async fn get_last_sync_timestamp(&self, user_id: i32) -> AppResult<Option<chrono::DateTime<chrono::Utc>>> {
        match self {
            DatabasePool::Postgres(pool) => {                
                let row = sqlx::query(r#"SELECT lastsynctime FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(row.try_get("lastsynctime").unwrap_or(None))
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {                
                let row = sqlx::query("SELECT LastSyncTime FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(row.try_get("LastSyncTime").unwrap_or(None))
                } else {
                    Ok(None)
                }
            }
        }
    }
    
    // Update last sync timestamp - PROPER GPodder spec implementation for incremental sync
    async fn update_last_sync_timestamp(&self, user_id: i32) -> AppResult<()> {
        let now = chrono::Utc::now();
        
        match self {
            DatabasePool::Postgres(pool) => {                
                sqlx::query(r#"UPDATE "Users" SET lastsynctime = $1 WHERE userid = $2"#)
                    .bind(now)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {                
                sqlx::query("UPDATE Users SET LastSyncTime = ? WHERE UserID = ?")
                    .bind(now)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        
        Ok(())
    }

    // Clear last sync timestamp - for initial full sync to start fresh
    async fn clear_last_sync_timestamp(&self, user_id: i32) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {                
                sqlx::query(r#"UPDATE "Users" SET lastsynctime = NULL WHERE userid = $1"#)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {                
                sqlx::query("UPDATE Users SET LastSyncTime = NULL WHERE UserID = ?")
                    .bind(user_id)
                    .execute(pool)
                    .await?;
            }
        }
        
        Ok(())
    }

    // Get user episode actions since timestamp - CRITICAL for incremental sync performance
    async fn get_user_episode_actions_since(&self, user_id: i32, since: chrono::DateTime<chrono::Utc>) -> AppResult<Vec<serde_json::Value>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(r#"
                    SELECT 
                        e.episodeurl as podcast,
                        e.episodeurl as episode,
                        eh.listenduration as position,
                        CASE 
                            WHEN eh.listenduration > 0 THEN 'play'
                            WHEN d.episodeid IS NOT NULL THEN 'download'
                            ELSE 'new'
                        END as action,
                        COALESCE(eh.listendate, '1970-01-01'::timestamp) as timestamp
                    FROM "Episodes" e
                    LEFT JOIN "UserEpisodeHistory" eh ON e.episodeid = eh.episodeid AND eh.userid = $1
                    LEFT JOIN "DownloadedEpisodes" d ON e.episodeid = d.episodeid AND d.userid = $1
                    WHERE (eh.userid = $1 OR d.userid = $1)
                    AND COALESCE(eh.listendate, '1970-01-01'::timestamp) > $2
                    ORDER BY timestamp DESC
                "#)
                .bind(user_id)
                .bind(since)
                .fetch_all(pool)
                .await?;
                
                let mut actions = Vec::new();
                for row in rows {
                    // Handle PostgreSQL TIMESTAMP (not TIMESTAMPTZ) column
                    let naive_timestamp: chrono::NaiveDateTime = row.try_get("timestamp")?;
                    let utc_timestamp = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(naive_timestamp, chrono::Utc);
                    let timestamp_str = utc_timestamp.to_rfc3339();
                    
                    actions.push(serde_json::json!({
                        "podcast": row.try_get::<String, _>("podcast")?,
                        "episode": row.try_get::<String, _>("episode")?,
                        "action": row.try_get::<String, _>("action")?,
                        "timestamp": timestamp_str,
                        "position": row.try_get::<Option<i32>, _>("position").unwrap_or(None)
                    }));
                }
                Ok(actions)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query("
                    SELECT 
                        e.EpisodeURL as podcast,
                        e.EpisodeURL as episode,
                        eh.ListenDuration as position,
                        CASE 
                            WHEN eh.ListenDuration > 0 THEN 'play'
                            WHEN d.EpisodeID IS NOT NULL THEN 'download'
                            ELSE 'new'
                        END as action,
                        COALESCE(eh.ListenDate, '1970-01-01 00:00:00') as timestamp
                    FROM Episodes e
                    LEFT JOIN UserEpisodeHistory eh ON e.EpisodeID = eh.EpisodeID AND eh.UserID = ?
                    LEFT JOIN DownloadedEpisodes d ON e.EpisodeID = d.EpisodeID AND d.UserID = ?
                    WHERE (eh.UserID = ? OR d.UserID = ?)
                    AND COALESCE(eh.ListenDate, '1970-01-01 00:00:00') > ?
                    ORDER BY timestamp DESC
                ")
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(user_id)
                .bind(since)
                .fetch_all(pool)
                .await?;
                
                let mut actions = Vec::new();
                for row in rows {
                    // Handle MySQL DATETIME column 
                    let naive_timestamp: chrono::NaiveDateTime = row.try_get("timestamp")?;
                    let utc_timestamp = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(naive_timestamp, chrono::Utc);
                    let timestamp_str = utc_timestamp.to_rfc3339();
                    
                    actions.push(serde_json::json!({
                        "podcast": row.try_get::<String, _>("podcast")?,
                        "episode": row.try_get::<String, _>("episode")?,
                        "action": row.try_get::<String, _>("action")?,
                        "timestamp": timestamp_str,
                        "position": row.try_get::<Option<i32>, _>("position").unwrap_or(None)
                    }));
                }
                Ok(actions)
            }
        }
    }

    // Get all user episode actions - fallback for first sync
    async fn get_user_episode_actions(&self, user_id: i32) -> AppResult<Vec<serde_json::Value>> {
        // Use since timestamp of epoch (1970) to get all actions
        let epoch = chrono::DateTime::from_timestamp(0, 0).unwrap_or_else(chrono::Utc::now);
        self.get_user_episode_actions_since(user_id, epoch).await
    }
}

#[derive(Debug)]
struct RssEpisode {
    title: String,
    description: String,
    url: String,
    pub_date: String,
    duration: Option<i32>,
    author: Option<String>,
    artwork_url: Option<String>,
}

impl DatabasePool {
    // Get all users with nextcloud sync enabled - matches Python get_all_users_with_nextcloud_sync
    pub async fn get_all_users_with_nextcloud_sync(&self) -> AppResult<Vec<i32>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let user_ids: Vec<i32> = sqlx::query_scalar(
                    r#"SELECT userid FROM "Users" WHERE pod_sync_type = 'nextcloud'"#
                )
                .fetch_all(pool)
                .await?;
                Ok(user_ids)
            }
            DatabasePool::MySQL(pool) => {
                let user_ids: Vec<i32> = sqlx::query_scalar(
                    "SELECT UserID FROM Users WHERE Pod_Sync_Type = 'nextcloud'"
                )
                .fetch_all(pool)
                .await?;
                Ok(user_ids)
            }
        }
    }

    // Complete implementation of sync_with_nextcloud_for_user - matches Python nextcloud sync functionality  
    pub async fn sync_with_nextcloud_for_user(&self, user_id: i32) -> AppResult<bool> {
        tracing::info!("Starting Nextcloud sync for user {}", user_id);
        
        // Get user's Nextcloud configuration
        let gpodder_status = self.gpodder_get_status(user_id).await?;
        
        // Only proceed if sync type is nextcloud
        if gpodder_status.sync_type != "nextcloud" {
            tracing::info!("User {} does not have Nextcloud sync enabled", user_id);
            return Ok(false);
        }
        
        // Get Nextcloud credentials from database
        let (gpodder_url, username, encrypted_token) = match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT gpodderurl, gpodderloginname, gpoddertoken FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                    
                if let Some(row) = row {
                    let url: Option<String> = row.try_get("gpodderurl")?;
                    let login: Option<String> = row.try_get("gpodderloginname")?;
                    let token: Option<String> = row.try_get("gpoddertoken")?;
                    
                    (
                        url.ok_or_else(|| AppError::internal("Nextcloud URL not configured"))?,
                        login.ok_or_else(|| AppError::internal("Nextcloud username not configured"))?,
                        token.ok_or_else(|| AppError::internal("Nextcloud token not configured"))?
                    )
                } else {
                    return Err(AppError::not_found("User not found"));
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT GpodderUrl, GpodderLoginName, GpodderToken FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                    
                if let Some(row) = row {
                    let url: Option<String> = row.try_get("GpodderUrl")?;
                    let login: Option<String> = row.try_get("GpodderLoginName")?;
                    let token: Option<String> = row.try_get("GpodderToken")?;
                    
                    (
                        url.ok_or_else(|| AppError::internal("Nextcloud URL not configured"))?,
                        login.ok_or_else(|| AppError::internal("Nextcloud username not configured"))?,
                        token.ok_or_else(|| AppError::internal("Nextcloud token not configured"))?
                    )
                } else {
                    return Err(AppError::not_found("User not found"));
                }
            }
        };
        
        // Decrypt token using existing decrypt_password method
        let password = self.decrypt_password(&encrypted_token).await?;
        
        // Get last sync timestamp for incremental sync
        let since_timestamp = if let Some(last_sync) = self.get_last_sync_timestamp(user_id).await? {
            last_sync.timestamp()
        } else {
            0
        };
        
        // Build Nextcloud API endpoint URLs
        let base_url = if gpodder_url.ends_with('/') {
            gpodder_url.trim_end_matches('/').to_string()
        } else {
            gpodder_url.clone()
        };
        
        let subscriptions_url = format!("{}/index.php/apps/gpoddersync/subscriptions", base_url);
        let episode_action_url = format!("{}/index.php/apps/gpoddersync/episode_action", base_url);
        
        let client = reqwest::Client::new();
        let mut has_changes = false;
        
        // Sync subscriptions from Nextcloud
        let subscriptions_response = client
            .get(&subscriptions_url)
            .basic_auth(&username, Some(&password))
            .query(&[("since", since_timestamp.to_string())])
            .send()
            .await
            .map_err(|e| AppError::internal(&format!("Failed to fetch Nextcloud subscriptions: {}", e)))?;
            
        if subscriptions_response.status().is_success() {
            let subscription_data: serde_json::Value = subscriptions_response.json().await
                .map_err(|e| AppError::internal(&format!("Failed to parse subscription response: {}", e)))?;
            
            // Process subscription changes
            if let Some(add_list) = subscription_data.get("add").and_then(|v| v.as_array()) {
                for url in add_list {
                    if let Some(podcast_url) = url.as_str() {
                        tracing::info!("Adding Nextcloud subscription: {}", podcast_url);
                        if let Err(e) = self.add_podcast_from_url(user_id, podcast_url, None).await {
                            tracing::error!("Failed to add podcast {}: {}", podcast_url, e);
                        } else {
                            has_changes = true;
                        }
                    }
                }
            }
            
            if let Some(remove_list) = subscription_data.get("remove").and_then(|v| v.as_array()) {
                for url in remove_list {
                    if let Some(podcast_url) = url.as_str() {
                        tracing::info!("Removing Nextcloud subscription: {}", podcast_url);
                        if let Err(e) = self.remove_podcast_by_url(user_id, podcast_url).await {
                            tracing::error!("Failed to remove podcast {}: {}", podcast_url, e);
                        } else {
                            has_changes = true;
                        }
                    }
                }
            }
        }
        
        // Sync episode actions from Nextcloud
        let episode_actions_response = client
            .get(&episode_action_url)
            .basic_auth(&username, Some(&password))
            .query(&[("since", since_timestamp.to_string())])
            .send()
            .await
            .map_err(|e| AppError::internal(&format!("Failed to fetch Nextcloud episode actions: {}", e)))?;
            
        if episode_actions_response.status().is_success() {
            let episode_actions_data: serde_json::Value = episode_actions_response.json().await
                .map_err(|e| AppError::internal(&format!("Failed to parse episode actions response: {}", e)))?;
            
            if let Some(actions) = episode_actions_data.get("actions").and_then(|v| v.as_array()) {
                for action in actions {
                    if let Err(e) = self.process_nextcloud_episode_action(user_id, action).await {
                        tracing::error!("Failed to process episode action: {}", e);
                    } else {
                        has_changes = true;
                    }
                }
            }
        }
        
        // Update last sync timestamp 
        if let Err(e) = self.update_last_sync_timestamp(user_id).await {
            tracing::error!("Failed to update sync timestamp for user {}: {}", user_id, e);
        }
        
        tracing::info!("Nextcloud sync completed for user {} - changes: {}", user_id, has_changes);
        Ok(has_changes)
    }
    
    // Process individual episode action from Nextcloud
    async fn process_nextcloud_episode_action(&self, user_id: i32, action: &serde_json::Value) -> AppResult<()> {
        let episode_url = action.get("episode") 
            .and_then(|v| v.as_str())
            .ok_or_else(|| AppError::internal("Missing episode URL in episode action"))?;
            
        let action_type = action.get("action")
            .and_then(|v| v.as_str())
            .ok_or_else(|| AppError::internal("Missing action type in episode action"))?;
        
        // Find the episode by URL
        let episode_id = match self.get_episode_id_by_url(episode_url).await {
            Ok(Some(id)) => id,
            Ok(None) => {
                tracing::warn!("Episode not found for URL: {}", episode_url);
                return Ok(());
            }
            Err(_) => {
                tracing::warn!("Error finding episode for URL: {}", episode_url);
                return Ok(());
            }
        };
        
        match action_type {
            "play" => {
                if let Some(position) = action.get("position").and_then(|v| v.as_i64()) {
                    self.save_episode_history(user_id, episode_id, position as i32, 0).await?;
                }
            }
            "download" => {
                self.mark_episode_completed(episode_id, user_id, false).await?;
            }  
            "delete" => {
                // Remove episode from user's history
                self.remove_episode_from_history(user_id, episode_id).await?;
            }
            _ => {
                tracing::debug!("Unknown action type: {}", action_type);
            }
        }
        
        Ok(())
    }

    // Add podcast from URL - used by Nextcloud sync
    pub async fn add_podcast_from_url(&self, user_id: i32, feed_url: &str, _feed_cutoff: Option<i32>) -> AppResult<()> {
        // Check if podcast already exists for this user
        if self.podcast_exists_for_user(user_id, feed_url).await? {
            tracing::info!("Podcast {} already exists for user {}", feed_url, user_id);
            return Ok(());
        }

        // Get podcast metadata from feed URL using existing function
        let podcast_values = self.get_podcast_values(feed_url, user_id, None, None).await?;
        
        // Add podcast using existing function
        let _result = self.add_podcast_from_values(&podcast_values, user_id, 30, None, None).await?;
        
        tracing::info!("Successfully added podcast {} for user {}", feed_url, user_id);
        Ok(())
    }

    // Remove podcast by URL - used by Nextcloud sync  
    pub async fn remove_podcast_by_url(&self, user_id: i32, feed_url: &str) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"DELETE FROM "Podcasts" WHERE feedurl = $1 AND userid = $2"#)
                    .bind(feed_url)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                    
                if result.rows_affected() > 0 {
                    tracing::info!("Successfully removed podcast {} for user {}", feed_url, user_id);
                } else {
                    tracing::info!("Podcast {} not found for user {}", feed_url, user_id);
                }
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("DELETE FROM Podcasts WHERE FeedURL = ? AND UserID = ?")
                    .bind(feed_url)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                    
                if result.rows_affected() > 0 {
                    tracing::info!("Successfully removed podcast {} for user {}", feed_url, user_id);
                } else {
                    tracing::info!("Podcast {} not found for user {}", feed_url, user_id);
                }
            }
        }
        Ok(())
    }

    // Get episode ID by URL - used by Nextcloud episode actions
    pub async fn get_episode_id_by_url(&self, episode_url: &str) -> AppResult<Option<i32>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT episodeid FROM "Episodes" WHERE episodeurl = $1 LIMIT 1"#)
                    .bind(episode_url)
                    .fetch_optional(pool)
                    .await?;
                    
                if let Some(row) = row {
                    Ok(Some(row.try_get("episodeid")?))
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT EpisodeID FROM Episodes WHERE EpisodeURL = ? LIMIT 1")
                    .bind(episode_url)
                    .fetch_optional(pool)
                    .await?;
                    
                if let Some(row) = row {
                    Ok(Some(row.try_get("EpisodeID")?))
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Save episode history - used by Nextcloud episode actions  
    pub async fn save_episode_history(&self, user_id: i32, episode_id: i32, position: i32, _total_time: i32) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"
                    INSERT INTO "UserEpisodeHistory" (userid, episodeid, listenduration, episodecompleted, episodeprogress)
                    VALUES ($1, $2, $3, FALSE, $4)
                    ON CONFLICT (userid, episodeid) 
                    DO UPDATE SET listenduration = $3, episodeprogress = $4
                "#)
                .bind(user_id)
                .bind(episode_id)
                .bind(position)
                .bind(position)
                .execute(pool)
                .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query(r#"
                    INSERT INTO UserEpisodeHistory (UserID, EpisodeID, ListenDuration, EpisodeCompleted, EpisodeProgress)
                    VALUES (?, ?, ?, FALSE, ?)
                    ON DUPLICATE KEY UPDATE ListenDuration = ?, EpisodeProgress = ?
                "#)
                .bind(user_id)
                .bind(episode_id)
                .bind(position)
                .bind(position)
                .bind(position)
                .bind(position)
                .execute(pool)
                .await?;
            }
        }
        Ok(())
    }

    // Remove episode from history - used by Nextcloud episode actions
    pub async fn remove_episode_from_history(&self, user_id: i32, episode_id: i32) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"DELETE FROM "UserEpisodeHistory" WHERE userid = $1 AND episodeid = $2"#)
                    .bind(user_id)
                    .bind(episode_id)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("DELETE FROM UserEpisodeHistory WHERE UserID = ? AND EpisodeID = ?")
                    .bind(user_id)
                    .bind(episode_id)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Remove GPodder sync settings for a user - matches Python remove_gpodder_settings function exactly
    pub async fn remove_gpodder_settings(&self, user_id: i32) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let mut tx = pool.begin().await?;
                
                // First delete any device records
                sqlx::query(r#"DELETE FROM "GpodderDevices" WHERE userid = $1"#)
                    .bind(user_id)
                    .execute(&mut *tx)
                    .await?;
                
                sqlx::query(r#"DELETE FROM "GpodderSyncState" WHERE userid = $1"#)
                    .bind(user_id)
                    .execute(&mut *tx)
                    .await?;
                
                // Then clear GPodder settings from user record
                sqlx::query(r#"
                    UPDATE "Users"
                    SET gpodderurl = '', gpodderloginname = '', gpoddertoken = '', pod_sync_type = 'None'
                    WHERE userid = $1
                "#)
                    .bind(user_id)
                    .execute(&mut *tx)
                    .await?;
                
                tx.commit().await?;
                Ok(true)
            }
            DatabasePool::MySQL(pool) => {
                let mut tx = pool.begin().await?;
                
                // First delete any device records
                sqlx::query("DELETE FROM GpodderDevices WHERE UserID = ?")
                    .bind(user_id)
                    .execute(&mut *tx)
                    .await?;
                
                sqlx::query("DELETE FROM GpodderSyncState WHERE UserID = ?")
                    .bind(user_id)
                    .execute(&mut *tx)
                    .await?;
                
                // Then clear GPodder settings from user record
                sqlx::query(r#"
                    UPDATE Users
                    SET GpodderUrl = '', GpodderLoginName = '', GpodderToken = '', Pod_Sync_Type = 'None'
                    WHERE UserID = ?
                "#)
                    .bind(user_id)
                    .execute(&mut *tx)
                    .await?;
                
                tx.commit().await?;
                Ok(true)
            }
        }
    }

    // Check if a user exists with the given username and email
    pub async fn check_reset_user(&self, username: &str, email: &str) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"SELECT userid FROM "Users" WHERE username = $1 AND email = $2"#)
                    .bind(username)
                    .bind(email)
                    .fetch_optional(pool)
                    .await?;
                Ok(result.is_some())
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("SELECT UserID FROM Users WHERE Username = ? AND Email = ?")
                    .bind(username)
                    .bind(email)
                    .fetch_optional(pool)
                    .await?;
                Ok(result.is_some())
            }
        }
    }

    // Create a password reset code for the user
    pub async fn reset_password_create_code(&self, user_email: &str) -> AppResult<Option<String>> {
        use rand::Rng;
        use chrono::{Utc, Duration};
        
        // Generate 6-character reset code with uppercase letters and digits
        let reset_code: String = (0..6)
            .map(|_| {
                let chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
                chars[rand::rng().random_range(0..chars.len())] as char
            })
            .collect();

        let reset_expiry: sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc> = (Utc::now() + Duration::hours(1)).into();

        match self {
            DatabasePool::Postgres(pool) => {
                // Check if user exists first
                let user_exists = sqlx::query(r#"SELECT userid FROM "Users" WHERE email = $1"#)
                    .bind(user_email)
                    .fetch_optional(pool)
                    .await?;
                
                if user_exists.is_none() {
                    return Ok(None);
                }

                // Update reset code and expiry
                let result = sqlx::query(r#"
                    UPDATE "Users"
                    SET reset_code = $1, reset_expiry = $2
                    WHERE email = $3
                "#)
                    .bind(&reset_code)
                    .bind(reset_expiry)
                    .bind(user_email)
                    .execute(pool)
                    .await?;

                if result.rows_affected() > 0 {
                    Ok(Some(reset_code))
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                // Check if user exists first
                let user_exists = sqlx::query("SELECT UserID FROM Users WHERE Email = ?")
                    .bind(user_email)
                    .fetch_optional(pool)
                    .await?;
                
                if user_exists.is_none() {
                    return Ok(None);
                }

                // Update reset code and expiry
                let result = sqlx::query(r#"
                    UPDATE Users
                    SET Reset_Code = ?, Reset_Expiry = ?
                    WHERE Email = ?
                "#)
                    .bind(&reset_code)
                    .bind(reset_expiry)
                    .bind(user_email)
                    .execute(pool)
                    .await?;

                if result.rows_affected() > 0 {
                    Ok(Some(reset_code))
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Remove reset code from user (used when email sending fails)
    pub async fn reset_password_remove_code(&self, email: &str) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"UPDATE "Users" SET reset_code = NULL, reset_expiry = NULL WHERE email = $1"#)
                    .bind(email)
                    .execute(pool)
                    .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query("UPDATE Users SET Reset_Code = NULL, Reset_Expiry = NULL WHERE Email = ?")
                    .bind(email)
                    .execute(pool)
                    .await?;
            }
        }
        Ok(())
    }

    // Verify reset code is valid and not expired
    pub async fn verify_reset_code(&self, user_email: &str, reset_code: &str) -> AppResult<Option<bool>> {
        use chrono::Utc;
        
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"SELECT reset_code, reset_expiry FROM "Users" WHERE email = $1"#)
                    .bind(user_email)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = result {
                    let stored_code: Option<String> = row.try_get("reset_code").ok();
                    let expiry: Option<chrono::NaiveDateTime> = row.try_get("reset_expiry").ok();
                    
                    if let (Some(stored_code), Some(expiry)) = (stored_code.clone(), expiry.clone()) {
                        // Convert NaiveDateTime to UTC for comparison
                        let expiry_utc = expiry.and_utc();
                        let is_valid = stored_code == reset_code && Utc::now() < expiry_utc;
                        Ok(Some(is_valid))
                    } else {
                        Ok(Some(false)) // No reset code set
                    }
                } else {
                    Ok(None) // User not found
                }
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("SELECT Reset_Code, Reset_Expiry FROM Users WHERE Email = ?")
                    .bind(user_email)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = result {
                    let stored_code: Option<String> = row.try_get("Reset_Code").ok();
                    let expiry: Option<sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>> = row.try_get("Reset_Expiry").ok();
                    
                    if let (Some(stored_code), Some(expiry)) = (stored_code, expiry) {
                        Ok(Some(stored_code == reset_code && Utc::now() < expiry))
                    } else {
                        Ok(Some(false)) // No reset code set
                    }
                } else {
                    Ok(None) // User not found
                }
            }
        }
    }

    // Reset password and clear reset code/expiry
    pub async fn reset_password_prompt(&self, user_email: &str, hashed_password: &str) -> AppResult<Option<String>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"
                    UPDATE "Users"
                    SET hashed_pw = $1, reset_code = NULL, reset_expiry = NULL
                    WHERE email = $2
                "#)
                    .bind(hashed_password)
                    .bind(user_email)
                    .execute(pool)
                    .await?;
                
                if result.rows_affected() > 0 {
                    Ok(Some("Password Reset Successfully".to_string()))
                } else {
                    Ok(None)
                }
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query(r#"
                    UPDATE Users
                    SET Hashed_PW = ?, Reset_Code = NULL, Reset_Expiry = NULL
                    WHERE Email = ?
                "#)
                    .bind(hashed_password)
                    .bind(user_email)
                    .execute(pool)
                    .await?;
                
                if result.rows_affected() > 0 {
                    Ok(Some("Password Reset Successfully".to_string()))
                } else {
                    Ok(None)
                }
            }
        }
    }

    // Set scheduled backup configuration
    pub async fn set_scheduled_backup(&self, user_id: i32, cron_schedule: &str, enabled: bool) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(r#"
                    INSERT INTO "ScheduledBackups" (userid, cron_schedule, enabled, created_at, updated_at)
                    VALUES ($1, $2, $3, NOW(), NOW())
                    ON CONFLICT (userid)
                    DO UPDATE SET 
                        cron_schedule = EXCLUDED.cron_schedule,
                        enabled = EXCLUDED.enabled,
                        updated_at = NOW()
                "#)
                .bind(user_id)
                .bind(cron_schedule)
                .bind(enabled)
                .execute(pool)
                .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query(r#"
                    INSERT INTO ScheduledBackups (UserID, CronSchedule, Enabled, CreatedAt, UpdatedAt)
                    VALUES (?, ?, ?, NOW(), NOW())
                    ON DUPLICATE KEY UPDATE 
                        CronSchedule = VALUES(CronSchedule),
                        Enabled = VALUES(Enabled),
                        UpdatedAt = NOW()
                "#)
                .bind(user_id)
                .bind(cron_schedule)
                .bind(enabled)
                .execute(pool)
                .await?;
            }
        }
        Ok(())
    }

    // Get scheduled backup configuration
    pub async fn get_scheduled_backup(&self, user_id: i32) -> AppResult<serde_json::Value> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"
                    SELECT cron_schedule, enabled, created_at, updated_at
                    FROM "ScheduledBackups"
                    WHERE userid = $1
                "#)
                .bind(user_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    Ok(serde_json::json!({
                        "schedule": row.get::<String, _>("cron_schedule"),
                        "enabled": row.get::<bool, _>("enabled"),
                        "created_at": row.get::<chrono::NaiveDateTime, _>("created_at").format("%Y-%m-%dT%H:%M:%S").to_string(),
                        "updated_at": row.get::<chrono::NaiveDateTime, _>("updated_at").format("%Y-%m-%dT%H:%M:%S").to_string()
                    }))
                } else {
                    Ok(serde_json::json!({
                        "schedule": null,
                        "enabled": false,
                        "created_at": null,
                        "updated_at": null
                    }))
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query(r#"
                    SELECT CronSchedule, Enabled, CreatedAt, UpdatedAt
                    FROM ScheduledBackups
                    WHERE UserID = ?
                "#)
                .bind(user_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    let created_datetime = row.try_get::<sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>, _>("CreatedAt")?;
                    let updated_datetime = row.try_get::<sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>, _>("UpdatedAt")?;
                    
                    Ok(serde_json::json!({
                        "schedule": row.try_get::<String, _>("CronSchedule")?,
                        "enabled": row.try_get::<bool, _>("Enabled")?,
                        "created_at": created_datetime.format("%Y-%m-%dT%H:%M:%S").to_string(),
                        "updated_at": updated_datetime.format("%Y-%m-%dT%H:%M:%S").to_string()
                    }))
                } else {
                    Ok(serde_json::json!({
                        "schedule": null,
                        "enabled": false,
                        "created_at": null,
                        "updated_at": null
                    }))
                }
            }
        }
    }

    // Execute backup to file (called by scheduler)
    pub async fn execute_scheduled_backup(&self, _user_id: i32) -> AppResult<String> {
        use tokio::process::Command;
        use chrono::Utc;

        // Generate backup filename with timestamp
        let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
        let backup_filename = format!("scheduled_backup_{}.sql", timestamp);
        let backup_path = format!("/opt/pinepods/backups/{}", backup_filename);

        // Get database password from environment
        let db_password = std::env::var("DB_PASSWORD")
            .map_err(|_| AppError::internal("Database password not found in environment"))?;

        match self {
            DatabasePool::Postgres(_) => {
                let db_host = std::env::var("DB_HOST").unwrap_or_else(|_| "localhost".to_string());
                let db_port = std::env::var("DB_PORT").unwrap_or_else(|_| "5432".to_string());
                let db_user = std::env::var("DB_USER").unwrap_or_else(|_| "postgres".to_string());
                let db_name = std::env::var("DB_NAME").unwrap_or_else(|_| "pinepods_database".to_string());

                let mut cmd = Command::new("pg_dump");
                cmd.arg("-h").arg(&db_host)
                   .arg("-p").arg(&db_port)
                   .arg("-U").arg(&db_user)
                   .arg("-d").arg(&db_name)
                   .arg("-f").arg(&backup_path)
                   .arg("--verbose")
                   .env("PGPASSWORD", &db_password);

                let output = cmd.output().await
                    .map_err(|e| AppError::internal(&format!("Failed to execute backup: {}", e)))?;

                if !output.status.success() {
                    let error_msg = String::from_utf8_lossy(&output.stderr);
                    return Err(AppError::internal(&format!("Backup failed: {}", error_msg)));
                }
            }
            DatabasePool::MySQL(_) => {
                let db_host = std::env::var("DB_HOST").unwrap_or_else(|_| "localhost".to_string());
                let db_port = std::env::var("DB_PORT").unwrap_or_else(|_| "3306".to_string());
                let db_user = std::env::var("DB_USER").unwrap_or_else(|_| "mysql".to_string());
                let db_name = std::env::var("DB_NAME").unwrap_or_else(|_| "pinepods_database".to_string());

                let mut cmd = Command::new("mysqldump");
                cmd.arg("-h").arg(&db_host)
                   .arg("-P").arg(&db_port)
                   .arg("-u").arg(&db_user)
                   .arg(format!("-p{}", &db_password))
                   .arg(&db_name)
                   .arg("--result-file").arg(&backup_path)
                   .arg("--single-transaction")
                   .arg("--routines")
                   .arg("--triggers");

                let output = cmd.output().await
                    .map_err(|e| AppError::internal(&format!("Failed to execute backup: {}", e)))?;

                if !output.status.success() {
                    let error_msg = String::from_utf8_lossy(&output.stderr);
                    return Err(AppError::internal(&format!("Backup failed: {}", error_msg)));
                }
            }
        }

        Ok(backup_filename)
    }

    // Get podcasts that have podcast_index_id = 0 (imported without podcast index match)
    pub async fn get_unmatched_podcasts(&self, user_id: i32) -> AppResult<Vec<serde_json::Value>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT podcastid, podcastname, artworkurl, author, description, feedurl
                       FROM "Podcasts" 
                       WHERE userid = $1 AND (podcastindexid = 0 OR podcastindexid IS NULL) 
                       AND (ignorepodcastindex = FALSE OR ignorepodcastindex IS NULL)
                       ORDER BY podcastname"#
                )
                .bind(user_id)
                .fetch_all(pool)
                .await?;

                let mut podcasts = Vec::new();
                for row in rows {
                    let podcast = serde_json::json!({
                        "podcast_id": row.try_get::<i32, _>("podcastid")?,
                        "podcast_name": row.try_get::<String, _>("podcastname")?,
                        "artwork_url": row.try_get::<Option<String>, _>("artworkurl")?,
                        "author": row.try_get::<Option<String>, _>("author")?,
                        "description": row.try_get::<Option<String>, _>("description")?,
                        "feed_url": row.try_get::<String, _>("feedurl")?
                    });
                    podcasts.push(podcast);
                }

                Ok(podcasts)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(
                    "SELECT PodcastID, PodcastName, ArtworkURL, Author, Description, FeedURL
                     FROM Podcasts 
                     WHERE UserID = ? AND (PodcastIndexID = 0 OR PodcastIndexID IS NULL) 
                     AND (IgnorePodcastIndex = 0 OR IgnorePodcastIndex IS NULL)
                     ORDER BY PodcastName"
                )
                .bind(user_id)
                .fetch_all(pool)
                .await?;

                let mut podcasts = Vec::new();
                for row in rows {
                    let podcast = serde_json::json!({
                        "podcast_id": row.try_get::<i32, _>("PodcastID")?,
                        "podcast_name": row.try_get::<String, _>("PodcastName")?,
                        "artwork_url": row.try_get::<Option<String>, _>("ArtworkURL")?,
                        "author": row.try_get::<Option<String>, _>("Author")?,
                        "description": row.try_get::<Option<String>, _>("Description")?,
                        "feed_url": row.try_get::<String, _>("FeedURL")?
                    });
                    podcasts.push(podcast);
                }

                Ok(podcasts)
            }
        }
    }

    // Update a podcast's podcast_index_id
    pub async fn update_podcast_index_id(&self, user_id: i32, podcast_id: i32, podcast_index_id: i32) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(
                    r#"UPDATE "Podcasts" 
                       SET podcastindexid = $1 
                       WHERE podcastid = $2 AND userid = $3"#
                )
                .bind(podcast_index_id)
                .bind(podcast_id)
                .bind(user_id)
                .execute(pool)
                .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query(
                    "UPDATE Podcasts 
                     SET PodcastIndexID = ? 
                     WHERE PodcastID = ? AND UserID = ?"
                )
                .bind(podcast_index_id)
                .bind(podcast_id)
                .bind(user_id)
                .execute(pool)
                .await?;
            }
        }
        Ok(())
    }

    // Ignore/unignore a podcast's index ID requirement
    pub async fn ignore_podcast_index_id(&self, user_id: i32, podcast_id: i32, ignore: bool) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                sqlx::query(
                    r#"UPDATE "Podcasts" 
                       SET ignorepodcastindex = $1 
                       WHERE podcastid = $2 AND userid = $3"#
                )
                .bind(ignore)
                .bind(podcast_id)
                .bind(user_id)
                .execute(pool)
                .await?;
            }
            DatabasePool::MySQL(pool) => {
                sqlx::query(
                    "UPDATE Podcasts 
                     SET IgnorePodcastIndex = ? 
                     WHERE PodcastID = ? AND UserID = ?"
                )
                .bind(ignore)
                .bind(podcast_id)
                .bind(user_id)
                .execute(pool)
                .await?;
            }
        }
        Ok(())
    }

    // Get ignored podcasts for a user  
    pub async fn get_ignored_podcasts(&self, user_id: i32) -> AppResult<Vec<serde_json::Value>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let rows = sqlx::query(
                    r#"SELECT podcastid, podcastname, artworkurl, author, description, feedurl
                       FROM "Podcasts" 
                       WHERE userid = $1 AND ignorepodcastindex = TRUE
                       ORDER BY podcastname"#
                )
                .bind(user_id)
                .fetch_all(pool)
                .await?;

                let mut podcasts = Vec::new();
                for row in rows {
                    let podcast = serde_json::json!({
                        "podcast_id": row.try_get::<i32, _>("podcastid")?,
                        "podcast_name": row.try_get::<String, _>("podcastname")?,
                        "artwork_url": row.try_get::<Option<String>, _>("artworkurl")?,
                        "author": row.try_get::<Option<String>, _>("author")?,
                        "description": row.try_get::<Option<String>, _>("description")?,
                        "feed_url": row.try_get::<String, _>("feedurl")?
                    });
                    podcasts.push(podcast);
                }

                Ok(podcasts)
            }
            DatabasePool::MySQL(pool) => {
                let rows = sqlx::query(
                    "SELECT PodcastID, PodcastName, ArtworkURL, Author, Description, FeedURL
                     FROM Podcasts 
                     WHERE UserID = ? AND IgnorePodcastIndex = 1
                     ORDER BY PodcastName"
                )
                .bind(user_id)
                .fetch_all(pool)
                .await?;

                let mut podcasts = Vec::new();
                for row in rows {
                    let podcast = serde_json::json!({
                        "podcast_id": row.try_get::<i32, _>("PodcastID")?,
                        "podcast_name": row.try_get::<String, _>("PodcastName")?,
                        "artwork_url": row.try_get::<Option<String>, _>("ArtworkURL")?,
                        "author": row.try_get::<Option<String>, _>("Author")?,
                        "description": row.try_get::<Option<String>, _>("Description")?,
                        "feed_url": row.try_get::<String, _>("FeedURL")?
                    });
                    podcasts.push(podcast);
                }

                Ok(podcasts)
            }
        }
    }

    // Add shared episode - uses current database schema with ShareCode
    pub async fn add_shared_episode(&self, episode_id: i32, shared_by: i32, share_code: &str, expiration_date: chrono::DateTime<chrono::Utc>) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"
                    INSERT INTO "SharedEpisodes" (episodeid, sharedby, sharecode, expirationdate)
                    VALUES ($1, $2, $3, $4)
                "#)
                .bind(episode_id)
                .bind(shared_by)
                .bind(share_code)
                .bind(expiration_date)
                .execute(pool)
                .await;

                match result {
                    Ok(_) => Ok(true),
                    Err(e) => {
                        tracing::error!("Error sharing episode: {}", e);
                        Ok(false)
                    }
                }
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query(
                    "INSERT INTO SharedEpisodes (EpisodeID, SharedBy, ShareCode, ExpirationDate) VALUES (?, ?, ?, ?)"
                )
                .bind(episode_id)
                .bind(shared_by)
                .bind(share_code)
                .bind(expiration_date)
                .execute(pool)
                .await;

                match result {
                    Ok(_) => Ok(true),
                    Err(e) => {
                        tracing::error!("Error sharing episode: {}", e);
                        Ok(false)
                    }
                }
            }
        }
    }

    // Get episode ID by share code (for shared episode access)
    pub async fn get_episode_id_by_share_code(&self, share_code: &str) -> AppResult<Option<i32>> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query_as::<_, (i32,)>(r#"
                    SELECT episodeid FROM "SharedEpisodes" 
                    WHERE sharecode = $1 AND expirationdate > NOW()
                "#)
                .bind(share_code)
                .fetch_optional(pool)
                .await?;
                
                Ok(result.map(|row| row.0))
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query_as::<_, (i32,)>(
                    "SELECT EpisodeID FROM SharedEpisodes 
                     WHERE ShareCode = ? AND ExpirationDate > NOW()"
                )
                .bind(share_code)
                .fetch_optional(pool)
                .await?;
                
                Ok(result.map(|row| row.0))
            }
        }
    }

    // Get shared episode metadata - bypasses user restrictions for public access
    pub async fn get_shared_episode_metadata(&self, episode_id: i32) -> AppResult<serde_json::Value> {
        match self {
            DatabasePool::Postgres(pool) => {
                // First try regular episodes
                let row = sqlx::query(
                    r#"SELECT 
                        "Podcasts".podcastid, 
                        "Podcasts".feedurl,
                        "Podcasts".podcastname, 
                        "Podcasts".artworkurl,
                        "Episodes".episodetitle,
                        "Episodes".episodepubdate,
                        "Episodes".episodedescription,
                        "Episodes".episodeartwork,
                        "Episodes".episodeurl,
                        "Episodes".episodeduration,
                        "Episodes".episodeid,
                        "Podcasts".websiteurl,
                        "Episodes".completed,
                        FALSE::boolean as is_youtube
                    FROM "Episodes"
                    INNER JOIN "Podcasts" ON "Episodes".podcastid = "Podcasts".podcastid
                    WHERE "Episodes".episodeid = $1"#
                )
                .bind(episode_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    let episodepubdate = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?
                        .format("%Y-%m-%dT%H:%M:%S").to_string();
                    
                    Ok(serde_json::json!({
                        "podcastid": row.try_get::<i32, _>("podcastid")?,
                        "feedurl": row.try_get::<Option<String>, _>("feedurl")?,
                        "podcastname": row.try_get::<String, _>("podcastname")?,
                        "artworkurl": row.try_get::<Option<String>, _>("artworkurl")?,
                        "episodetitle": row.try_get::<String, _>("episodetitle")?,
                        "episodepubdate": episodepubdate,
                        "episodedescription": row.try_get::<Option<String>, _>("episodedescription")?,
                        "episodeartwork": row.try_get::<Option<String>, _>("episodeartwork")?,
                        "episodeurl": row.try_get::<String, _>("episodeurl")?,
                        "episodeduration": row.try_get::<Option<i32>, _>("episodeduration")?,
                        "episodeid": row.try_get::<i32, _>("episodeid")?,
                        "websiteurl": row.try_get::<Option<String>, _>("websiteurl")?,
                        "listenduration": None::<i32>, // No user-specific data for shared episodes
                        "completed": row.try_get::<Option<bool>, _>("completed")?,
                        "is_youtube": false
                    }))
                } else {
                    // Try YouTube videos
                    let row = sqlx::query(
                        r#"SELECT 
                            "Podcasts".podcastid, 
                            "Podcasts".feedurl,
                            "Podcasts".podcastname, 
                            "Podcasts".artworkurl,
                            "YouTubeVideos".videotitle as episodetitle,
                            "YouTubeVideos".publishedat as episodepubdate,
                            "YouTubeVideos".videodescription as episodedescription,
                            "YouTubeVideos".thumbnailurl as episodeartwork,
                            "YouTubeVideos".videourl as episodeurl,
                            "YouTubeVideos".duration as episodeduration,
                            "YouTubeVideos".videoid as episodeid,
                            "Podcasts".websiteurl,
                            "YouTubeVideos".completed,
                            TRUE::boolean as is_youtube
                        FROM "YouTubeVideos"
                        INNER JOIN "Podcasts" ON "YouTubeVideos".podcastid = "Podcasts".podcastid
                        WHERE "YouTubeVideos".videoid = $1"#
                    )
                    .bind(episode_id)
                    .fetch_optional(pool)
                    .await?;

                    if let Some(row) = row {
                        let episodepubdate = row.try_get::<chrono::NaiveDateTime, _>("episodepubdate")?
                            .format("%Y-%m-%dT%H:%M:%S").to_string();
                        
                        Ok(serde_json::json!({
                            "podcastid": row.try_get::<i32, _>("podcastid")?,
                            "feedurl": row.try_get::<Option<String>, _>("feedurl")?,
                            "podcastname": row.try_get::<String, _>("podcastname")?,
                            "artworkurl": row.try_get::<Option<String>, _>("artworkurl")?,
                            "episodetitle": row.try_get::<String, _>("episodetitle")?,
                            "episodepubdate": episodepubdate,
                            "episodedescription": row.try_get::<Option<String>, _>("episodedescription")?,
                            "episodeartwork": row.try_get::<Option<String>, _>("episodeartwork")?,
                            "episodeurl": row.try_get::<String, _>("episodeurl")?,
                            "episodeduration": row.try_get::<Option<i32>, _>("episodeduration")?,
                            "episodeid": row.try_get::<i32, _>("episodeid")?,
                            "websiteurl": row.try_get::<Option<String>, _>("websiteurl")?,
                            "listenposition": None::<i32>, // No user-specific data for shared episodes  
                            "completed": row.try_get::<Option<bool>, _>("completed")?,
                            "is_youtube": true
                        }))
                    } else {
                        Err(AppError::not_found("Episode not found"))
                    }
                }
            }
            DatabasePool::MySQL(pool) => {
                // First try regular episodes
                let row = sqlx::query(
                    "SELECT 
                        Podcasts.PodcastID, 
                        Podcasts.FeedURL,
                        Podcasts.PodcastName, 
                        Podcasts.ArtworkURL,
                        Episodes.EpisodeTitle,
                        Episodes.EpisodePubDate,
                        Episodes.EpisodeDescription,
                        Episodes.EpisodeArtwork,
                        Episodes.EpisodeURL,
                        Episodes.EpisodeDuration,
                        Episodes.EpisodeID,
                        Podcasts.WebsiteURL,
                        Episodes.Completed,
                        FALSE as is_youtube
                    FROM Episodes
                    INNER JOIN Podcasts ON Episodes.PodcastID = Podcasts.PodcastID
                    WHERE Episodes.EpisodeID = ?"
                )
                .bind(episode_id)
                .fetch_optional(pool)
                .await?;

                if let Some(row) = row {
                    let episodepubdate = row.try_get::<chrono::NaiveDateTime, _>("EpisodePubDate")?
                        .format("%Y-%m-%dT%H:%M:%S").to_string();
                    
                    Ok(serde_json::json!({
                        "podcastid": row.try_get::<i32, _>("PodcastID")?,
                        "feedurl": row.try_get::<Option<String>, _>("FeedURL")?,
                        "podcastname": row.try_get::<String, _>("PodcastName")?,
                        "artworkurl": row.try_get::<Option<String>, _>("ArtworkURL")?,
                        "episodetitle": row.try_get::<String, _>("EpisodeTitle")?,
                        "episodepubdate": episodepubdate,
                        "episodedescription": row.try_get::<Option<String>, _>("EpisodeDescription")?,
                        "episodeartwork": row.try_get::<Option<String>, _>("EpisodeArtwork")?,
                        "episodeurl": row.try_get::<String, _>("EpisodeURL")?,
                        "episodeduration": row.try_get::<Option<i32>, _>("EpisodeDuration")?,
                        "episodeid": row.try_get::<i32, _>("EpisodeID")?,
                        "websiteurl": row.try_get::<Option<String>, _>("WebsiteURL")?,
                        "listenduration": None::<i32>, // No user-specific data for shared episodes
                        "completed": row.try_get::<Option<bool>, _>("Completed")?,
                        "is_youtube": false
                    }))
                } else {
                    // Try YouTube videos
                    let row = sqlx::query(
                        "SELECT 
                            Podcasts.PodcastID, 
                            Podcasts.FeedURL,
                            Podcasts.PodcastName, 
                            Podcasts.ArtworkURL,
                            YouTubeVideos.VideoTitle as EpisodeTitle,
                            YouTubeVideos.PublishedAt as EpisodePubDate,
                            YouTubeVideos.VideoDescription as EpisodeDescription,
                            YouTubeVideos.ThumbnailURL as EpisodeArtwork,
                            YouTubeVideos.VideoURL as EpisodeURL,
                            YouTubeVideos.Duration as EpisodeDuration,
                            YouTubeVideos.VideoID as EpisodeID,
                            Podcasts.WebsiteURL,
                            YouTubeVideos.Completed,
                            TRUE as is_youtube
                        FROM YouTubeVideos
                        INNER JOIN Podcasts ON YouTubeVideos.PodcastID = Podcasts.PodcastID
                        WHERE YouTubeVideos.VideoID = ?"
                    )
                    .bind(episode_id)
                    .fetch_optional(pool)
                    .await?;

                    if let Some(row) = row {
                        let episodepubdate = row.try_get::<chrono::NaiveDateTime, _>("EpisodePubDate")?
                            .format("%Y-%m-%dT%H:%M:%S").to_string();
                        
                        Ok(serde_json::json!({
                            "podcastid": row.try_get::<i32, _>("PodcastID")?,
                            "feedurl": row.try_get::<Option<String>, _>("FeedURL")?,
                            "podcastname": row.try_get::<String, _>("PodcastName")?,
                            "artworkurl": row.try_get::<Option<String>, _>("ArtworkURL")?,
                            "episodetitle": row.try_get::<String, _>("EpisodeTitle")?,
                            "episodepubdate": episodepubdate,
                            "episodedescription": row.try_get::<Option<String>, _>("EpisodeDescription")?,
                            "episodeartwork": row.try_get::<Option<String>, _>("EpisodeArtwork")?,
                            "episodeurl": row.try_get::<String, _>("EpisodeURL")?,
                            "episodeduration": row.try_get::<Option<i32>, _>("EpisodeDuration")?,
                            "episodeid": row.try_get::<i32, _>("EpisodeID")?,
                            "websiteurl": row.try_get::<Option<String>, _>("WebsiteURL")?,
                            "listenposition": None::<i32>, // No user-specific data for shared episodes  
                            "completed": row.try_get::<Option<bool>, _>("Completed")?,
                            "is_youtube": true
                        }))
                    } else {
                        Err(AppError::not_found("Episode not found"))
                    }
                }
            }
        }
    }

    pub async fn delete_playlist(&self, user_id: i32, playlist_id: i32) -> AppResult<()> {
        match self {
            DatabasePool::Postgres(pool) => {
                // Check if playlist exists and belongs to user
                let playlist = sqlx::query(
                    r#"SELECT issystemplaylist, userid FROM "Playlists" WHERE playlistid = $1"#
                )
                .bind(playlist_id)
                .fetch_optional(pool)
                .await?;

                let playlist = playlist.ok_or_else(|| AppError::not_found("Playlist not found"))?;

                let is_system: bool = playlist.try_get("issystemplaylist")?;
                let owner_id: i32 = playlist.try_get("userid")?;

                if is_system {
                    return Err(AppError::bad_request("Cannot delete system playlists"));
                }

                if owner_id != user_id {
                    return Err(AppError::forbidden("Unauthorized to delete this playlist"));
                }

                // Delete the playlist
                sqlx::query(r#"DELETE FROM "Playlists" WHERE playlistid = $1"#)
                    .bind(playlist_id)
                    .execute(pool)
                    .await?;

                Ok(())
            }
            DatabasePool::MySQL(pool) => {
                // Check if playlist exists and belongs to user
                let playlist = sqlx::query("SELECT IsSystemPlaylist, UserID FROM Playlists WHERE PlaylistID = ?")
                    .bind(playlist_id)
                    .fetch_optional(pool)
                    .await?;

                let playlist = playlist.ok_or_else(|| AppError::not_found("Playlist not found"))?;

                let is_system: i8 = playlist.try_get("IsSystemPlaylist")?;
                let owner_id: i32 = playlist.try_get("UserID")?;

                if is_system != 0 {
                    return Err(AppError::bad_request("Cannot delete system playlists"));
                }

                if owner_id != user_id {
                    return Err(AppError::forbidden("Unauthorized to delete this playlist"));
                }

                // Delete the playlist
                sqlx::query("DELETE FROM Playlists WHERE PlaylistID = ?")
                    .bind(playlist_id)
                    .execute(pool)
                    .await?;

                Ok(())
            }
        }
    }

    // Get user's language preference
    pub async fn get_user_language(&self, user_id: i32) -> AppResult<String> {
        match self {
            DatabasePool::Postgres(pool) => {
                let row = sqlx::query(r#"SELECT language FROM "Users" WHERE userid = $1"#)
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(row.get::<Option<String>, _>("language").unwrap_or_else(|| "en".to_string()))
                } else {
                    Ok("en".to_string())
                }
            }
            DatabasePool::MySQL(pool) => {
                let row = sqlx::query("SELECT Language FROM Users WHERE UserID = ?")
                    .bind(user_id)
                    .fetch_optional(pool)
                    .await?;
                
                if let Some(row) = row {
                    Ok(row.get::<Option<String>, _>("Language").unwrap_or_else(|| "en".to_string()))
                } else {
                    Ok("en".to_string())
                }
            }
        }
    }

    // Update user's language preference
    pub async fn update_user_language(&self, user_id: i32, language: &str) -> AppResult<bool> {
        match self {
            DatabasePool::Postgres(pool) => {
                let result = sqlx::query(r#"UPDATE "Users" SET language = $1 WHERE userid = $2"#)
                    .bind(language)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
            DatabasePool::MySQL(pool) => {
                let result = sqlx::query("UPDATE Users SET Language = ? WHERE UserID = ?")
                    .bind(language)
                    .bind(user_id)
                    .execute(pool)
                    .await?;
                
                Ok(result.rows_affected() > 0)
            }
        }
    }

    // Create missing default playlists for existing users
    pub async fn create_missing_default_playlists(&self) -> AppResult<()> {
        use tracing::{info, warn, error};
        
        info!("🎵 Checking for missing default playlists for existing users...");
        
        // Define default playlists (same as migration 032)
        let default_playlists = vec![
            ("Quick Listens", "Short episodes under 15 minutes, perfect for quick breaks", Some(1), Some(900), "duration_asc", false, true, true, None, false, Some(1000), "ph-fast-forward"),
            ("Longform", "Extended episodes over 1 hour, ideal for long drives or deep dives", Some(3600), None, "duration_desc", true, true, true, None, false, Some(1000), "ph-car"),
            ("Currently Listening", "Episodes you've started but haven't finished", None, None, "date_desc", false, true, false, None, false, None, "ph-play"),
            ("Fresh Releases", "Latest episodes from the last 24 hours", None, None, "date_desc", true, false, false, Some(24), false, None, "ph-sparkle"),
            ("Weekend Marathon", "Longer episodes (30+ minutes) perfect for weekend listening", Some(1800), None, "duration_desc", true, true, true, None, true, Some(1000), "ph-couch"),
            ("Commuter Mix", "Perfect-length episodes (15-45 minutes) for your daily commute", Some(900), Some(2700), "date_desc", true, true, true, None, false, Some(1000), "ph-car-simple"),
        ];
        
        match self {
            DatabasePool::Postgres(pool) => {
                // Get all existing users (excluding background user if present)
                let user_rows = sqlx::query(r#"SELECT userid FROM "Users" WHERE userid > 1"#)
                    .fetch_all(pool)
                    .await?;
                
                info!("Found {} users to check for missing default playlists", user_rows.len());
                
                for user_row in user_rows {
                    let user_id: i32 = user_row.try_get("userid")?;
                    
                    for (name, description, min_duration, max_duration, sort_order, include_unplayed, include_partially_played, include_played, time_filter_hours, group_by_podcast, max_episodes, icon_name) in &default_playlists {
                        // Check if this playlist already exists for this user
                        let exists: bool = sqlx::query_scalar(
                            r#"SELECT EXISTS(SELECT 1 FROM "Playlists" WHERE userid = $1 AND name = $2)"#
                        ).bind(user_id).bind(name).fetch_one(pool).await?;
                        
                        if !exists {
                            // Create the playlist for this user
                            match sqlx::query(r#"
                                INSERT INTO "Playlists" (
                                    userid, name, description, issystemplaylist, minduration, maxduration, sortorder,
                                    includeunplayed, includepartiallyplayed, includeplayed, timefilterhours,
                                    groupbypodcast, maxepisodes, playprogressmin, playprogressmax, podcastids,
                                    iconname, episodecount
                                ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
                                "#)
                                .bind(user_id).bind(name).bind(description).bind(false)
                                .bind(min_duration).bind(max_duration).bind(sort_order)
                                .bind(include_unplayed).bind(include_partially_played).bind(include_played)
                                .bind(time_filter_hours).bind(group_by_podcast).bind(max_episodes)
                                .bind(0.0).bind(100.0).bind(&[] as &[i32]).bind(icon_name).bind(0)
                                .execute(pool).await {
                                Ok(_) => info!("Created playlist '{}' for user {}", name, user_id),
                                Err(e) => warn!("Failed to create playlist '{}' for user {}: {}", name, user_id, e),
                            }
                        }
                    }
                }
            }
            DatabasePool::MySQL(pool) => {
                // Get all existing users (excluding background user if present)
                let user_rows = sqlx::query("SELECT UserID FROM Users WHERE UserID > 1")
                    .fetch_all(pool)
                    .await?;
                
                info!("Found {} users to check for missing default playlists", user_rows.len());
                
                for user_row in user_rows {
                    let user_id: i32 = user_row.try_get("UserID")?;
                    
                    for (name, description, min_duration, max_duration, sort_order, include_unplayed, include_partially_played, include_played, time_filter_hours, group_by_podcast, max_episodes, icon_name) in &default_playlists {
                        // Check if this playlist already exists for this user
                        let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM Playlists WHERE UserID = ? AND Name = ?")
                            .bind(user_id)
                            .bind(name)
                            .fetch_one(pool)
                            .await?;
                        
                        if count == 0 {
                            // Create the playlist for this user
                            match sqlx::query("
                                INSERT INTO Playlists (
                                    UserID, Name, Description, IsSystemPlaylist, MinDuration, MaxDuration, SortOrder,
                                    IncludeUnplayed, IncludePartiallyPlayed, IncludePlayed, TimeFilterHours,
                                    GroupByPodcast, MaxEpisodes, PlayProgressMin, PlayProgressMax, PodcastIDs,
                                    IconName, EpisodeCount
                                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                                ")
                                .bind(user_id).bind(name).bind(description).bind(false)
                                .bind(min_duration).bind(max_duration).bind(sort_order)
                                .bind(include_unplayed).bind(include_partially_played).bind(include_played)
                                .bind(time_filter_hours).bind(group_by_podcast).bind(max_episodes)
                                .bind(0.0).bind(100.0).bind("[]").bind(icon_name).bind(0)
                                .execute(pool).await {
                                Ok(_) => info!("Created playlist '{}' for user {}", name, user_id),
                                Err(e) => warn!("Failed to create playlist '{}' for user {}: {}", name, user_id, e),
                            }
                        }
                    }
                }
            }
        }
        
        info!("✅ Finished checking for missing default playlists");
        Ok(())
    }

    // Create default playlists for a single user - shared by user creation and startup check
    async fn create_default_playlists_for_user(&self, user_id: i32) -> AppResult<()> {
        use tracing::{info, warn};
        
        // Define default playlists (same as migration 032)
        let default_playlists = vec![
            ("Quick Listens", "Short episodes under 15 minutes, perfect for quick breaks", Some(1), Some(900), "duration_asc", true, true, true, None, false, Some(1000), "ph-fast-forward"),
            ("Longform", "Extended episodes over 1 hour, ideal for long drives or deep dives", Some(3600), None, "duration_desc", true, true, true, None, false, Some(1000), "ph-car"),
            ("Currently Listening", "Episodes you've started but haven't finished", None, None, "date_desc", false, true, false, None, false, None, "ph-play"),
            ("Fresh Releases", "Latest episodes from the last 24 hours", None, None, "date_desc", true, false, false, Some(24), false, None, "ph-sparkle"),
            ("Weekend Marathon", "Longer episodes (30+ minutes) perfect for weekend listening", Some(1800), None, "duration_desc", true, true, true, None, true, Some(1000), "ph-couch"),
            ("Commuter Mix", "Perfect-length episodes (15-45 minutes) for your daily commute", Some(900), Some(2700), "date_desc", true, true, true, None, false, Some(1000), "ph-car-simple"),
        ];
        
        match self {
            DatabasePool::Postgres(pool) => {
                for (name, description, min_duration, max_duration, sort_order, include_unplayed, include_partially_played, include_played, time_filter_hours, group_by_podcast, max_episodes, icon_name) in &default_playlists {
                    // Check if this playlist already exists for this user
                    let exists: bool = sqlx::query_scalar(
                        r#"SELECT EXISTS(SELECT 1 FROM "Playlists" WHERE userid = $1 AND name = $2)"#
                    ).bind(user_id).bind(name).fetch_one(pool).await?;
                    
                    if !exists {
                        // Create the playlist for this user
                        match sqlx::query(r#"
                            INSERT INTO "Playlists" (
                                userid, name, description, issystemplaylist, minduration, maxduration, sortorder,
                                includeunplayed, includepartiallyplayed, includeplayed, timefilterhours,
                                groupbypodcast, maxepisodes, playprogressmin, playprogressmax, podcastids,
                                iconname, episodecount
                            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
                            "#)
                            .bind(user_id).bind(name).bind(description).bind(false)
                            .bind(min_duration).bind(max_duration).bind(sort_order)
                            .bind(include_unplayed).bind(include_partially_played).bind(include_played)
                            .bind(time_filter_hours).bind(group_by_podcast).bind(max_episodes)
                            .bind(0.0).bind(100.0).bind(&[] as &[i32]).bind(icon_name).bind(0)
                            .execute(pool).await {
                            Ok(_) => info!("Created playlist '{}' for user {}", name, user_id),
                            Err(e) => warn!("Failed to create playlist '{}' for user {}: {}", name, user_id, e),
                        }
                    }
                }
            }
            DatabasePool::MySQL(pool) => {
                for (name, description, min_duration, max_duration, sort_order, include_unplayed, include_partially_played, include_played, time_filter_hours, group_by_podcast, max_episodes, icon_name) in &default_playlists {
                    // Check if this playlist already exists for this user
                    let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM Playlists WHERE UserID = ? AND Name = ?")
                        .bind(user_id)
                        .bind(name)
                        .fetch_one(pool)
                        .await?;
                    
                    if count == 0 {
                        // Create the playlist for this user
                        match sqlx::query("
                            INSERT INTO Playlists (
                                UserID, Name, Description, IsSystemPlaylist, MinDuration, MaxDuration, SortOrder,
                                IncludeUnplayed, IncludePartiallyPlayed, IncludePlayed, TimeFilterHours,
                                GroupByPodcast, MaxEpisodes, PlayProgressMin, PlayProgressMax, PodcastIDs,
                                IconName, EpisodeCount
                            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                            ")
                            .bind(user_id).bind(name).bind(description).bind(false)
                            .bind(min_duration).bind(max_duration).bind(sort_order)
                            .bind(include_unplayed).bind(include_partially_played).bind(include_played)
                            .bind(time_filter_hours).bind(group_by_podcast).bind(max_episodes)
                            .bind(0.0).bind(100.0).bind("[]").bind(icon_name).bind(0)
                            .execute(pool).await {
                            Ok(_) => info!("Created playlist '{}' for user {}", name, user_id),
                            Err(e) => warn!("Failed to create playlist '{}' for user {}: {}", name, user_id, e),
                        }
                    }
                }
            }
        }
        
        Ok(())
    }

    // Update episode counts for all playlists - replaces complex playlist content updates
    pub async fn update_playlist_episode_counts(&self) -> AppResult<()> {
        use tracing::{info, warn, debug};
        
        info!("📊 Starting playlist episode count updates...");
        
        match self {
            DatabasePool::Postgres(pool) => {
                // Get all playlists
                let playlists = sqlx::query(
                    r#"SELECT playlistid, userid, name FROM "Playlists" ORDER BY userid, playlistid"#
                ).fetch_all(pool).await?;
                
                info!("Found {} playlists to update counts for", playlists.len());
                
                let mut updated_count = 0;
                let mut failed_count = 0;
                
                for playlist in playlists {
                    let playlist_id: i32 = playlist.try_get("playlistid")?;
                    let user_id: i32 = playlist.try_get("userid")?;
                    let name: String = playlist.try_get("name")?;
                    
                    match self.count_playlist_episodes_dynamic(playlist_id, user_id).await {
                        Ok(count) => {
                            // Update the episode_count in the playlist
                            match sqlx::query(
                                r#"UPDATE "Playlists" SET episodecount = $1 WHERE playlistid = $2"#
                            ).bind(count).bind(playlist_id).execute(pool).await {
                                Ok(_) => {
                                    debug!("Updated playlist '{}' (ID: {}) count to {}", name, playlist_id, count);
                                    updated_count += 1;
                                }
                                Err(e) => {
                                    warn!("Failed to update count for playlist '{}' (ID: {}): {}", name, playlist_id, e);
                                    failed_count += 1;
                                }
                            }
                        }
                        Err(e) => {
                            warn!("Failed to count episodes for playlist '{}' (ID: {}): {}", name, playlist_id, e);
                            failed_count += 1;
                        }
                    }
                }
                
                info!("✅ Playlist count update completed: {} updated, {} failed", updated_count, failed_count);
            }
            DatabasePool::MySQL(pool) => {
                // Get all playlists
                let playlists = sqlx::query(
                    r#"SELECT PlaylistID, UserID, Name FROM Playlists ORDER BY UserID, PlaylistID"#
                ).fetch_all(pool).await?;
                
                info!("Found {} playlists to update counts for (MySQL)", playlists.len());
                
                let mut updated_count = 0;
                let mut failed_count = 0;
                
                for playlist in playlists {
                    let playlist_id: i32 = playlist.try_get("PlaylistID")?;
                    let user_id: i32 = playlist.try_get("UserID")?;
                    let name: String = playlist.try_get("Name")?;
                    
                    match self.count_playlist_episodes_dynamic(playlist_id, user_id).await {
                        Ok(count) => {
                            // Update the episode_count in the playlist
                            match sqlx::query(
                                r#"UPDATE Playlists SET EpisodeCount = ? WHERE PlaylistID = ?"#
                            ).bind(count).bind(playlist_id).execute(pool).await {
                                Ok(_) => {
                                    debug!("Updated MySQL playlist '{}' (ID: {}) count to {}", name, playlist_id, count);
                                    updated_count += 1;
                                }
                                Err(e) => {
                                    warn!("Failed to update MySQL count for playlist '{}' (ID: {}): {}", name, playlist_id, e);
                                    failed_count += 1;
                                }
                            }
                        }
                        Err(e) => {
                            warn!("Failed to count MySQL episodes for playlist '{}' (ID: {}): {}", name, playlist_id, e);
                            failed_count += 1;
                        }
                    }
                }
                
                info!("✅ MySQL playlist count update completed: {} updated, {} failed", updated_count, failed_count);
            }
        }
        
        Ok(())
    }

    // Count episodes for a playlist using the same dynamic logic (without pagination)
    async fn count_playlist_episodes_dynamic(&self, playlist_id: i32, user_id: i32) -> AppResult<i32> {
        use tracing::{debug, warn};
        
        match self {
            DatabasePool::Postgres(pool) => {
                // Get user timezone for proper date calculations
                let raw_user_timezone: String = sqlx::query_scalar(
                    r#"SELECT timezone FROM "Users" WHERE userid = $1"#
                ).bind(user_id).fetch_optional(pool).await?.unwrap_or_else(|| "UTC".to_string());
                let raw_timezone = if raw_user_timezone.is_empty() { "UTC".to_string() } else { raw_user_timezone };
                let user_timezone = Self::map_timezone_for_postgres(&raw_timezone);
                
                // Get playlist configuration
                let playlist_row = sqlx::query(
                    r#"SELECT userid, name, minduration, maxduration, sortorder, 
                       includeunplayed, includepartiallyplayed, includeplayed, timefilterhours,
                       groupbypodcast, maxepisodes, playprogressmin, playprogressmax, podcastids
                       FROM "Playlists" WHERE playlistid = $1"#
                ).bind(playlist_id).fetch_optional(pool).await?;
                
                let playlist = playlist_row.ok_or_else(|| crate::error::AppError::not_found("Playlist not found"))?;
                
                // Build count query using same logic as dynamic function
                let mut query_parts = Vec::new();
                let mut where_conditions = Vec::new();
                
                query_parts.push(format!(r#"
                    SELECT COUNT(DISTINCT e.episodeid)
                    FROM "Episodes" e
                    JOIN "Podcasts" p ON e.podcastid = p.podcastid AND p.userid = {}
                    LEFT JOIN "UserEpisodeHistory" h ON e.episodeid = h.episodeid AND h.userid = {}"#, 
                    user_id, user_id
                ));
                
                where_conditions.push("p.userid = $1".to_string());
                
                // Apply all the same filters as dynamic function
                if let Some(min_dur) = playlist.try_get::<Option<i32>, _>("minduration")? {
                    where_conditions.push(format!("e.episodeduration >= {}", min_dur));
                }
                if let Some(max_dur) = playlist.try_get::<Option<i32>, _>("maxduration")? {
                    where_conditions.push(format!("e.episodeduration <= {}", max_dur));
                }
                
                if let Some(hours) = playlist.try_get::<Option<i32>, _>("timefilterhours")? {
                    where_conditions.push(format!(
                        "e.episodepubdate >= (NOW() AT TIME ZONE '{}' - INTERVAL '{} hours') AT TIME ZONE '{}' AT TIME ZONE 'UTC'", 
                        user_timezone, hours, user_timezone
                    ));
                }
                
                // Handle PostgreSQL array type for podcast IDs
                if let Some(podcast_ids) = playlist.try_get::<Option<Vec<i32>>, _>("podcastids")? {
                    if !podcast_ids.is_empty() && !podcast_ids.contains(&-1) {
                        let podcast_ids_str = podcast_ids.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(",");
                        where_conditions.push(format!("p.podcastid IN ({})", podcast_ids_str));
                    }
                }
                
                // Play state filters
                let mut play_state_conditions = Vec::new();
                if playlist.try_get::<bool, _>("includeunplayed")? {
                    play_state_conditions.push("(h.listenduration IS NULL OR h.listenduration = 0)".to_string());
                }
                if playlist.try_get::<bool, _>("includepartiallyplayed")? {
                    play_state_conditions.push(
                        "(h.listenduration > 0 AND h.listenduration < e.episodeduration * 0.9 AND (e.episodeduration - h.listenduration) > 30)".to_string()
                    );
                }
                if playlist.try_get::<bool, _>("includeplayed")? {
                    play_state_conditions.push(
                        "(h.listenduration IS NOT NULL AND (h.listenduration >= e.episodeduration * 0.9 OR (e.episodeduration - h.listenduration) <= 30))".to_string()
                    );
                }
                
                if !play_state_conditions.is_empty() {
                    where_conditions.push(format!("({})", play_state_conditions.join(" OR ")));
                } else {
                    where_conditions.push("FALSE".to_string());
                }
                
                // Progress filters
                if let Some(min_progress) = playlist.try_get::<Option<f64>, _>("playprogressmin")? {
                    where_conditions.push(format!(
                        "(COALESCE(h.listenduration, 0)::float / NULLIF(e.episodeduration, 0)) >= {}", 
                        min_progress / 100.0
                    ));
                }
                if let Some(max_progress) = playlist.try_get::<Option<f64>, _>("playprogressmax")? {
                    where_conditions.push(format!(
                        "(COALESCE(h.listenduration, 0)::float / NULLIF(e.episodeduration, 0)) <= {}", 
                        max_progress / 100.0
                    ));
                }
                
                let where_clause = if where_conditions.is_empty() {
                    String::new()
                } else {
                    format!(" WHERE {}", where_conditions.join(" AND "))
                };
                
                let final_query = format!("{}{}", query_parts.join(" "), where_clause);
                
                let count: i64 = sqlx::query_scalar(&final_query)
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                
                // Apply MaxEpisodes limit if specified
                let final_count = if let Some(max_eps) = playlist.try_get::<Option<i32>, _>("maxepisodes")? {
                    if max_eps > 0 {
                        std::cmp::min(count as i32, max_eps)
                    } else {
                        count as i32
                    }
                } else {
                    count as i32
                };
                
                Ok(final_count)
            }
            DatabasePool::MySQL(pool) => {
                // Similar implementation for MySQL with adjusted syntax
                let raw_user_timezone: String = sqlx::query_scalar("SELECT TimeZone FROM Users WHERE UserID = ?")
                    .bind(user_id).fetch_optional(pool).await?.unwrap_or_else(|| "UTC".to_string());
                let raw_timezone = if raw_user_timezone.is_empty() { "UTC".to_string() } else { raw_user_timezone };
                let user_timezone = Self::map_timezone_for_postgres(&raw_timezone);
                
                let playlist_row = sqlx::query(
                    r#"SELECT UserID, Name, MinDuration, MaxDuration, SortOrder, 
                       IncludeUnplayed, IncludePartiallyPlayed, IncludePlayed, TimeFilterHours,
                       GroupByPodcast, MaxEpisodes, PlayProgressMin, PlayProgressMax, PodcastIDs
                       FROM Playlists WHERE PlaylistID = ?"#
                ).bind(playlist_id).fetch_optional(pool).await?;
                
                let playlist = playlist_row.ok_or_else(|| crate::error::AppError::not_found("Playlist not found"))?;
                
                // MySQL count query with similar logic
                let mut query_parts = Vec::new();
                let mut where_conditions = Vec::new();
                
                query_parts.push(format!(r#"
                    SELECT COUNT(DISTINCT e.EpisodeID)
                    FROM Episodes e
                    JOIN Podcasts p ON e.PodcastID = p.PodcastID AND p.UserID = {}
                    LEFT JOIN UserEpisodeHistory h ON e.EpisodeID = h.EpisodeID AND h.UserID = {}"#, 
                    user_id, user_id
                ));
                
                where_conditions.push("p.UserID = ?".to_string());
                
                // Apply all MySQL filters
                if let Some(min_dur) = playlist.try_get::<Option<i32>, _>("MinDuration")? {
                    where_conditions.push(format!("e.EpisodeDuration >= {}", min_dur));
                }
                if let Some(max_dur) = playlist.try_get::<Option<i32>, _>("MaxDuration")? {
                    where_conditions.push(format!("e.EpisodeDuration <= {}", max_dur));
                }
                
                if let Some(hours) = playlist.try_get::<Option<i32>, _>("TimeFilterHours")? {
                    where_conditions.push(format!(
                        "e.EpisodePubDate >= DATE_SUB(CONVERT_TZ(NOW(), 'UTC', '{}'), INTERVAL {} HOUR)", 
                        user_timezone, hours
                    ));
                }
                
                // Continue with all other MySQL filters...
                let where_clause = if where_conditions.is_empty() {
                    String::new()
                } else {
                    format!(" WHERE {}", where_conditions.join(" AND "))
                };
                
                let final_query = format!("{}{}", query_parts.join(" "), where_clause);
                
                let count: i64 = sqlx::query_scalar(&final_query)
                    .bind(user_id)
                    .fetch_one(pool)
                    .await?;
                
                let final_count = if let Some(max_eps) = playlist.try_get::<Option<i32>, _>("MaxEpisodes")? {
                    if max_eps > 0 {
                        std::cmp::min(count as i32, max_eps)
                    } else {
                        count as i32
                    }
                } else {
                    count as i32
                };
                
                Ok(final_count)
            }
        }
    }

    /// Convert chrono-tz timezone names to PostgreSQL-compatible timezone names
    /// This handles the mapping between frontend timezone selections and database queries
    fn map_timezone_for_postgres(user_timezone: &str) -> String {
        match user_timezone {
            // US timezone mappings
            "US/Alaska" => "America/Anchorage".to_string(),
            "US/Aleutian" => "America/Adak".to_string(),
            "US/Arizona" => "America/Phoenix".to_string(),
            "US/Central" => "America/Chicago".to_string(),
            "US/East-Indiana" => "America/Indiana/Indianapolis".to_string(),
            "US/Eastern" => "America/New_York".to_string(),
            "US/Hawaii" => "Pacific/Honolulu".to_string(),
            "US/Indiana-Starke" => "America/Indiana/Knox".to_string(),
            "US/Michigan" => "America/Detroit".to_string(),
            "US/Mountain" => "America/Denver".to_string(),
            "US/Pacific" => "America/Los_Angeles".to_string(),
            "US/Samoa" => "Pacific/Pago_Pago".to_string(),

            // Canada timezone mappings
            "Canada/Atlantic" => "America/Halifax".to_string(),
            "Canada/Central" => "America/Winnipeg".to_string(),
            "Canada/Eastern" => "America/Toronto".to_string(),
            "Canada/Mountain" => "America/Edmonton".to_string(),
            "Canada/Newfoundland" => "America/St_Johns".to_string(),
            "Canada/Pacific" => "America/Vancouver".to_string(),
            "Canada/Saskatchewan" => "America/Regina".to_string(),
            "Canada/Yukon" => "America/Whitehorse".to_string(),

            // Brazil timezone mappings
            "Brazil/Acre" => "America/Rio_Branco".to_string(),
            "Brazil/DeNoronha" => "America/Noronha".to_string(),
            "Brazil/East" => "America/Sao_Paulo".to_string(),
            "Brazil/West" => "America/Manaus".to_string(),

            // Chile timezone mappings
            "Chile/Continental" => "America/Santiago".to_string(),
            "Chile/EasterIsland" => "Pacific/Easter".to_string(),

            // Mexico timezone mappings
            "Mexico/BajaNorte" => "America/Tijuana".to_string(),
            "Mexico/BajaSur" => "America/Mazatlan".to_string(),
            "Mexico/General" => "America/Mexico_City".to_string(),

            // Common US legacy timezone abbreviations
            "EST" => "America/New_York".to_string(),
            "CST" => "America/Chicago".to_string(),
            "MST" => "America/Denver".to_string(),
            "PST" => "America/Los_Angeles".to_string(),
            "HST" => "Pacific/Honolulu".to_string(),
            "EST5EDT" => "America/New_York".to_string(),
            "CST6CDT" => "America/Chicago".to_string(),
            "MST7MDT" => "America/Denver".to_string(),
            "PST8PDT" => "America/Los_Angeles".to_string(),

            // European legacy mappings
            "CET" => "Europe/Paris".to_string(),
            "EET" => "Europe/Helsinki".to_string(),
            "WET" => "Europe/Lisbon".to_string(),
            "MET" => "Europe/Paris".to_string(),

            // Common international legacy mappings
            "GMT" => "UTC".to_string(),
            "GMT+0" => "UTC".to_string(),
            "GMT-0" => "UTC".to_string(),
            "GMT0" => "UTC".to_string(),
            "Greenwich" => "UTC".to_string(),
            "UCT" => "UTC".to_string(),
            "Universal" => "UTC".to_string(),
            "Zulu" => "UTC".to_string(),

            // Country/region legacy mappings
            "Cuba" => "America/Havana".to_string(),
            "Egypt" => "Africa/Cairo".to_string(),
            "Eire" => "Europe/Dublin".to_string(),
            "GB" => "Europe/London".to_string(),
            "GB-Eire" => "Europe/London".to_string(),
            "Hongkong" => "Asia/Hong_Kong".to_string(),
            "Iceland" => "Atlantic/Reykjavik".to_string(),
            "Iran" => "Asia/Tehran".to_string(),
            "Israel" => "Asia/Jerusalem".to_string(),
            "Jamaica" => "America/Jamaica".to_string(),
            "Japan" => "Asia/Tokyo".to_string(),
            "Kwajalein" => "Pacific/Kwajalein".to_string(),
            "Libya" => "Africa/Tripoli".to_string(),
            "NZ" => "Pacific/Auckland".to_string(),
            "NZ-CHAT" => "Pacific/Chatham".to_string(),
            "Navajo" => "America/Denver".to_string(),
            "PRC" => "Asia/Shanghai".to_string(),
            "Poland" => "Europe/Warsaw".to_string(),
            "Portugal" => "Europe/Lisbon".to_string(),
            "ROC" => "Asia/Taipei".to_string(),
            "ROK" => "Asia/Seoul".to_string(),
            "Singapore" => "Asia/Singapore".to_string(),
            "Turkey" => "Europe/Istanbul".to_string(),
            "W-SU" => "Europe/Moscow".to_string(),

            // If it's already a valid IANA timezone name or unknown, pass through
            _ => {
                // For unknown timezones, fall back to UTC to prevent errors
                if user_timezone.is_empty() {
                    "UTC".to_string()
                } else {
                    // Most chrono-tz names are already IANA compliant, so try the original first
                    user_timezone.to_string()
                }
            }
        }
    }

    // Get playlist episodes dynamically without using PlaylistContents table
    // ULTRA-PRECISE implementation covering ALL playlist options with timezone awareness
    pub async fn get_playlist_episodes_dynamic(
        &self, 
        playlist_id: i32, 
        user_id: i32
    ) -> AppResult<crate::models::PlaylistEpisodesResponse> {
        use tracing::{info, debug, warn};
        
        debug!("🎵 Getting dynamic playlist episodes for playlist {} user {}", playlist_id, user_id);
        
        match self {
            DatabasePool::Postgres(pool) => {
                // Get user timezone for proper date calculations
                let raw_user_timezone: String = sqlx::query_scalar(
                    r#"SELECT timezone FROM "Users" WHERE userid = $1"#
                ).bind(user_id).fetch_optional(pool).await?.unwrap_or_else(|| "UTC".to_string());
                let raw_timezone = if raw_user_timezone.is_empty() { "UTC".to_string() } else { raw_user_timezone };
                let user_timezone = Self::map_timezone_for_postgres(&raw_timezone);
                
                debug!("User {} timezone: {} -> {}", user_id, raw_timezone, user_timezone);
                
                // Get playlist configuration with ALL fields
                let playlist_row = sqlx::query(
                    r#"SELECT userid, name, description, minduration, maxduration, sortorder, 
                       includeunplayed, includepartiallyplayed, includeplayed, timefilterhours,
                       groupbypodcast, maxepisodes, playprogressmin, playprogressmax, podcastids,
                       issystemplaylist, created, iconname, episodecount
                       FROM "Playlists" WHERE playlistid = $1"#
                ).bind(playlist_id).fetch_optional(pool).await?;
                
                let playlist = playlist_row.ok_or_else(|| crate::error::AppError::not_found("Playlist not found"))?;
                
                // Check user permissions - users can only access their own playlists
                if playlist.try_get::<i32, _>("userid")? != user_id {
                    return Err(crate::error::AppError::forbidden("You can only access your own playlists"));
                }
                
                debug!("📋 Playlist '{}' config: min_dur={:?}, max_dur={:?}, sort={}, time_filter={:?}, progress_min={:?}, progress_max={:?}", 
                    playlist.try_get::<String, _>("name")?, playlist.try_get::<Option<i32>, _>("minduration")?, playlist.try_get::<Option<i32>, _>("maxduration")?, playlist.try_get::<String, _>("sortorder")?, 
                    playlist.try_get::<Option<i32>, _>("timefilterhours")?, playlist.try_get::<Option<f64>, _>("playprogressmin")?, playlist.try_get::<Option<f64>, _>("playprogressmax")?);
                
                // Build the comprehensive dynamic query
                let mut query_parts = Vec::new();
                let mut where_conditions = Vec::new();
                let mut bind_values: Vec<Box<dyn std::fmt::Debug + Send + Sync>> = Vec::new();
                
                // Base SELECT with all episode data needed for SavedEpisode model including podcastid
                query_parts.push(format!(r#"
                    SELECT DISTINCT
                        e.episodetitle,
                        p.podcastname,
                        TO_CHAR(e.episodepubdate AT TIME ZONE '{}', 'YYYY-MM-DD"T"HH24:MI:SS"Z"') as episodepubdate,
                        e.episodedescription,
                        COALESCE(e.episodeartwork, p.artworkurl) as episodeartwork,
                        e.episodeurl,
                        e.episodeduration,
                        COALESCE(h.listenduration, 0) as listenduration,
                        e.episodeid,
                        COALESCE(p.websiteurl, '') as websiteurl,
                        -- ULTRA-PRECISE completion logic: 90% threshold OR within 30 seconds of end
                        CASE WHEN 
                            h.listenduration IS NOT NULL AND (
                                h.listenduration >= e.episodeduration * 0.9 OR 
                                (e.episodeduration - h.listenduration) <= 30
                            ) 
                        THEN true ELSE false END as completed,
                        EXISTS(SELECT 1 FROM "SavedEpisodes" se WHERE se.episodeid = e.episodeid AND se.userid = {}) as saved,
                        EXISTS(SELECT 1 FROM "EpisodeQueue" eq WHERE eq.episodeid = e.episodeid AND eq.userid = {}) as queued,
                        EXISTS(SELECT 1 FROM "DownloadedEpisodes" de WHERE de.episodeid = e.episodeid AND de.userid = {}) as downloaded,
                        false as is_youtube,
                        p.podcastid,
                        -- Progress percentage for debugging
                        ROUND(((COALESCE(h.listenduration, 0)::float / NULLIF(e.episodeduration, 0)) * 100)::numeric, 2) as progress_percent
                    FROM "Episodes" e
                    JOIN "Podcasts" p ON e.podcastid = p.podcastid AND p.userid = {}
                    LEFT JOIN "UserEpisodeHistory" h ON e.episodeid = h.episodeid AND h.userid = {}"#, 
                    user_timezone, user_id, user_id, user_id, user_id, user_id
                ));
                
                // Base condition - always filter by user's podcasts
                where_conditions.push("p.userid = $1".to_string());
                
                // 1. DURATION FILTERS - exact duration matching
                if let Some(min_dur) = playlist.try_get::<Option<i32>, _>("minduration")? {
                    where_conditions.push(format!("e.episodeduration >= {}", min_dur));
                    debug!("🕒 Added min duration filter: {} seconds", min_dur);
                }
                if let Some(max_dur) = playlist.try_get::<Option<i32>, _>("maxduration")? {
                    where_conditions.push(format!("e.episodeduration <= {}", max_dur));
                    debug!("🕒 Added max duration filter: {} seconds", max_dur);
                }
                
                // 2. TIMEZONE-AWARE TIME FILTER - ULTRA-PRECISE datetime handling
                if let Some(hours) = playlist.try_get::<Option<i32>, _>("timefilterhours")? {
                    // Convert hours to user's timezone for precise "last X hours" calculation
                    where_conditions.push(format!(
                        "e.episodepubdate >= (NOW() AT TIME ZONE '{}' - INTERVAL '{} hours') AT TIME ZONE '{}' AT TIME ZONE 'UTC'", 
                        user_timezone, hours, user_timezone
                    ));
                    debug!("📅 Added timezone-aware time filter: last {} hours in timezone {}", hours, user_timezone);
                }
                
                // 3. PODCAST FILTER - handle PostgreSQL array of podcast IDs
                if let Some(podcast_ids) = playlist.try_get::<Option<Vec<i32>>, _>("podcastids")? {
                    if !podcast_ids.is_empty() && !podcast_ids.contains(&-1) {
                        let podcast_ids_str = podcast_ids.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(",");
                        where_conditions.push(format!("p.podcastid IN ({})", podcast_ids_str));
                        debug!("🎙️ Added PostgreSQL podcast filter: {:?}", podcast_ids);
                    } else {
                        debug!("🎙️ PostgreSQL podcast filter contains -1 or is empty, including all podcasts");
                    }
                }
                
                // 4. ULTRA-PRECISE PLAY STATE FILTERS
                let mut play_state_conditions = Vec::new();
                
                if playlist.try_get::<bool, _>("includeunplayed")? {
                    // UNPLAYED: No history record OR listen duration is 0 or NULL
                    play_state_conditions.push("(h.listenduration IS NULL OR h.listenduration = 0)".to_string());
                    debug!("▶️ Including UNPLAYED episodes");
                }
                
                if playlist.try_get::<bool, _>("includepartiallyplayed")? {
                    // PARTIALLY PLAYED: Has listen time > 0 but < 90% AND not within 30 seconds of end
                    play_state_conditions.push(
                        "(h.listenduration > 0 AND h.listenduration < e.episodeduration * 0.9 AND (e.episodeduration - h.listenduration) > 30)".to_string()
                    );
                    debug!("⏸️ Including PARTIALLY PLAYED episodes (>0% and <90%, not within 30s of end)");
                }
                
                if playlist.try_get::<bool, _>("includeplayed")? {
                    // PLAYED: Listen duration >= 90% OR within 30 seconds of end
                    play_state_conditions.push(
                        "(h.listenduration IS NOT NULL AND (h.listenduration >= e.episodeduration * 0.9 OR (e.episodeduration - h.listenduration) <= 30))".to_string()
                    );
                    debug!("✅ Including PLAYED episodes (>=90% or within 30s of end)");
                }
                
                if !play_state_conditions.is_empty() {
                    where_conditions.push(format!("({})", play_state_conditions.join(" OR ")));
                } else {
                    // No play states selected - return no results
                    where_conditions.push("FALSE".to_string());
                    warn!("⚠️ No play states selected for playlist '{}' - will return empty results", playlist.try_get::<String, _>("name")?);
                }
                
                // 5. ULTRA-PRECISE PROGRESS PERCENTAGE FILTERS
                if let Some(min_progress) = playlist.try_get::<Option<f64>, _>("playprogressmin")? {
                    let min_decimal = min_progress / 100.0;
                    where_conditions.push(format!(
                        "(COALESCE(h.listenduration, 0)::float / NULLIF(e.episodeduration, 0)) >= {}", 
                        min_decimal
                    ));
                    debug!("📊 Added min progress filter: {}% ({})", min_progress, min_decimal);
                }
                if let Some(max_progress) = playlist.try_get::<Option<f64>, _>("playprogressmax")? {
                    let max_decimal = max_progress / 100.0;
                    where_conditions.push(format!(
                        "(COALESCE(h.listenduration, 0)::float / NULLIF(e.episodeduration, 0)) <= {}", 
                        max_decimal
                    ));
                    debug!("📊 Added max progress filter: {}% ({})", max_progress, max_decimal);
                }
                
                // 6. ULTRA-PRECISE ORDERING with podcast grouping support
                let mut order_parts = Vec::new();
                
                if playlist.try_get::<bool, _>("groupbypodcast")? {
                    order_parts.push("p.podcastid".to_string());
                    debug!("📚 Grouping by podcast enabled");
                }
                
                let sort_clause = match playlist.try_get::<String, _>("sortorder")?.as_str() {
                    "date_asc" => "episodepubdate ASC",
                    "date_desc" => "episodepubdate DESC", 
                    "duration_asc" => "episodeduration ASC",
                    "duration_desc" => "episodeduration DESC",
                    "listen_progress" => "(COALESCE(h.listenduration, 0)::float / NULLIF(e.episodeduration, 0)) DESC",
                    "completion" => "(COALESCE(h.listenduration, 0)::float / NULLIF(e.episodeduration, 0)) DESC",
                    "random" => "RANDOM()",
                    "title_asc" => "episodetitle ASC",
                    "title_desc" => "episodetitle DESC",
                    "podcast_asc" => "podcastname ASC",
                    "podcast_desc" => "podcastname DESC",
                    _ => {
                        warn!("⚠️ Unknown sort order '{}', defaulting to date_desc", playlist.try_get::<String, _>("sortorder")?);
                        "episodepubdate DESC"
                    }
                };
                order_parts.push(sort_clause.to_string());
                debug!("🔄 Sort order: {}", sort_clause);
                
                // 7. BUILD FINAL QUERY
                let where_clause = if where_conditions.is_empty() {
                    String::new()
                } else {
                    format!(" WHERE {}", where_conditions.join(" AND "))
                };
                
                let order_clause = format!(" ORDER BY {}", order_parts.join(", "));
                
                // Apply MaxEpisodes limit if specified (playlist setting, not pagination)
                let limit_clause = if let Some(max_eps) = playlist.try_get::<Option<i32>, _>("maxepisodes")? {
                    if max_eps > 0 {
                        format!(" LIMIT {}", max_eps)
                    } else {
                        String::new()
                    }
                } else {
                    String::new()
                };
                
                let final_query = format!("{}{}{}{}", 
                    query_parts.join(" "), where_clause, order_clause, limit_clause);
                
                debug!("🔍 Final dynamic playlist query: {}", final_query);
                
                // Execute the main query
                let rows = sqlx::query(&final_query)
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;

                let mut episodes = Vec::new();
                for row in rows {
                    episodes.push(crate::models::SavedEpisode {
                        episodetitle: row.try_get("episodetitle")?,
                        podcastname: row.try_get("podcastname")?,
                        episodepubdate: row.try_get("episodepubdate")?,
                        episodedescription: row.try_get("episodedescription")?,
                        episodeartwork: row.try_get("episodeartwork")?,
                        episodeurl: row.try_get("episodeurl")?,
                        episodeduration: row.try_get("episodeduration")?,
                        listenduration: row.try_get("listenduration").ok(),
                        episodeid: row.try_get("episodeid")?,
                        websiteurl: row.try_get("websiteurl")?,
                        completed: row.try_get("completed")?,
                        saved: row.try_get("saved")?,
                        queued: row.try_get("queued")?,
                        downloaded: row.try_get("downloaded")?,
                        is_youtube: row.try_get("is_youtube")?,
                        podcastid: row.try_get("podcastid").ok(),
                    });
                }
                
                debug!("📝 Retrieved {} episodes from dynamic query", episodes.len());
                
                // Create playlist info from the playlist row we already have
                let playlist_info = crate::models::PlaylistInfo {
                    name: playlist.try_get::<String, _>("name")?,
                    description: playlist.try_get::<String, _>("description")?,
                    episode_count: episodes.len() as i32,
                    icon_name: playlist.try_get::<String, _>("iconname")?,
                };
                
                Ok(crate::models::PlaylistEpisodesResponse {
                    episodes,
                    playlist_info,
                })
            }
            DatabasePool::MySQL(pool) => {
                // FULL MySQL implementation with exact same logic but MySQL syntax
                let raw_user_timezone: String = sqlx::query_scalar("SELECT TimeZone FROM Users WHERE UserID = ?").bind(user_id)
                    .fetch_optional(pool).await?.unwrap_or_else(|| "UTC".to_string());
                let raw_timezone = if raw_user_timezone.is_empty() { "UTC".to_string() } else { raw_user_timezone };
                let user_timezone = Self::map_timezone_for_postgres(&raw_timezone);
                
                debug!("User {} timezone: {} -> {}", user_id, raw_timezone, user_timezone);
                
                let playlist_row = sqlx::query(
                    r#"SELECT UserID, Name, Description, MinDuration, MaxDuration, SortOrder,
                       IncludeUnplayed, IncludePartiallyPlayed, IncludePlayed, TimeFilterHours,
                       GroupByPodcast, MaxEpisodes, PlayProgressMin, PlayProgressMax, PodcastIDs,
                       IsSystemPlaylist, Created, IconName, EpisodeCount
                       FROM Playlists WHERE PlaylistID = ?"#
                ).bind(playlist_id).fetch_optional(pool).await?;
                
                let playlist = playlist_row.ok_or_else(|| crate::error::AppError::not_found("Playlist not found"))?;
                
                if playlist.try_get::<i32, _>("UserID")? != user_id {
                    return Err(crate::error::AppError::forbidden("You can only access your own playlists"));
                }
                
                debug!("📋 MySQL Playlist '{}' config loaded", playlist.try_get::<String, _>("Name")?);
                
                // MySQL version with adjusted syntax for timezone conversion and date handling
                let mut query_parts = Vec::new();
                let mut where_conditions = Vec::new();
                
                query_parts.push(format!(r#"
                    SELECT DISTINCT
                        e.EpisodeTitle as episodetitle,
                        p.PodcastName as podcastname,
                        DATE_FORMAT(CONVERT_TZ(e.EpisodePubDate, 'UTC', '{}'), '%Y-%m-%dT%H:%i:%sZ') as episodepubdate,
                        e.EpisodeDescription as episodedescription,
                        COALESCE(e.EpisodeArtwork, p.ArtworkURL) as episodeartwork,
                        e.EpisodeURL as episodeurl,
                        e.EpisodeDuration as episodeduration,
                        COALESCE(h.ListenDuration, 0) as listenduration,
                        e.EpisodeID as episodeid,
                        COALESCE(p.WebsiteURL, '') as websiteurl,
                        CASE WHEN 
                            h.ListenDuration IS NOT NULL AND (
                                h.ListenDuration >= e.EpisodeDuration * 0.9 OR 
                                (e.EpisodeDuration - h.ListenDuration) <= 30
                            ) 
                        THEN 1 ELSE 0 END as completed,
                        EXISTS(SELECT 1 FROM SavedEpisodes se WHERE se.EpisodeID = e.EpisodeID AND se.UserID = {}) as saved,
                        EXISTS(SELECT 1 FROM EpisodeQueue eq WHERE eq.EpisodeID = e.EpisodeID AND eq.UserID = {}) as queued,
                        EXISTS(SELECT 1 FROM DownloadedEpisodes de WHERE de.EpisodeID = e.EpisodeID AND de.UserID = {}) as downloaded,
                        0 as is_youtube,
                        p.PodcastID as podcastid,
                        ROUND((COALESCE(h.ListenDuration, 0) / NULLIF(e.EpisodeDuration, 0)) * 100, 2) as progress_percent
                    FROM Episodes e
                    JOIN Podcasts p ON e.PodcastID = p.PodcastID AND p.UserID = {}
                    LEFT JOIN UserEpisodeHistory h ON e.EpisodeID = h.EpisodeID AND h.UserID = {}"#, 
                    user_timezone, user_id, user_id, user_id, user_id, user_id
                ));
                
                where_conditions.push("p.UserID = ?".to_string());
                
                // Apply all the same filters with MySQL syntax
                if let Some(min_dur) = playlist.try_get::<Option<i32>, _>("MinDuration")? {
                    where_conditions.push(format!("e.EpisodeDuration >= {}", min_dur));
                }
                if let Some(max_dur) = playlist.try_get::<Option<i32>, _>("MaxDuration")? {
                    where_conditions.push(format!("e.EpisodeDuration <= {}", max_dur));
                }
                
                if let Some(hours) = playlist.try_get::<Option<i32>, _>("TimeFilterHours")? {
                    where_conditions.push(format!(
                        "e.EpisodePubDate >= DATE_SUB(CONVERT_TZ(NOW(), 'UTC', '{}'), INTERVAL {} HOUR)", 
                        user_timezone, hours
                    ));
                    debug!("📅 Added MySQL timezone-aware time filter: last {} hours in timezone {}", hours, user_timezone);
                }
                
                // 3. PODCAST FILTER - handle JSON array of podcast IDs (MySQL)
                if let Some(podcast_ids_json) = playlist.try_get::<Option<String>, _>("PodcastIDs")?.as_ref() {
                    if !podcast_ids_json.is_empty() && podcast_ids_json != "[]" && podcast_ids_json != "null" {
                        match serde_json::from_str::<Vec<i32>>(podcast_ids_json) {
                            Ok(podcast_ids) if !podcast_ids.is_empty() && !podcast_ids.contains(&-1) => {
                                let podcast_ids_str = podcast_ids.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(",");
                                where_conditions.push(format!("p.PodcastID IN ({})", podcast_ids_str));
                                debug!("🎙️ Added MySQL podcast filter: {:?}", podcast_ids);
                            }
                            Ok(_) => debug!("🎙️ MySQL podcast filter contains -1 or is empty, including all podcasts"),
                            Err(e) => warn!("⚠️ Failed to parse MySQL podcast IDs JSON '{}': {}", podcast_ids_json, e),
                        }
                    }
                }
                
                // 4. ULTRA-PRECISE PLAY STATE FILTERS (MySQL)
                let mut play_state_conditions = Vec::new();
                
                if playlist.try_get::<bool, _>("IncludeUnplayed")? {
                    play_state_conditions.push("(h.ListenDuration IS NULL OR h.ListenDuration = 0)".to_string());
                    debug!("▶️ Including UNPLAYED episodes (MySQL)");
                }
                
                if playlist.try_get::<bool, _>("IncludePartiallyPlayed")? {
                    play_state_conditions.push(
                        "(h.ListenDuration > 0 AND h.ListenDuration < e.EpisodeDuration * 0.9 AND (e.EpisodeDuration - h.ListenDuration) > 30)".to_string()
                    );
                    debug!("⏸️ Including PARTIALLY PLAYED episodes (MySQL)");
                }
                
                if playlist.try_get::<bool, _>("IncludePlayed")? {
                    play_state_conditions.push(
                        "(h.ListenDuration IS NOT NULL AND (h.ListenDuration >= e.EpisodeDuration * 0.9 OR (e.EpisodeDuration - h.ListenDuration) <= 30))".to_string()
                    );
                    debug!("✅ Including PLAYED episodes (MySQL)");
                }
                
                if !play_state_conditions.is_empty() {
                    where_conditions.push(format!("({})", play_state_conditions.join(" OR ")));
                } else {
                    where_conditions.push("FALSE".to_string());
                    warn!("⚠️ No play states selected for MySQL playlist '{}' - will return empty results", playlist.try_get::<String, _>("Name")?);
                }
                
                // 5. ULTRA-PRECISE PROGRESS PERCENTAGE FILTERS (MySQL)
                if let Some(min_progress) = playlist.try_get::<Option<f64>, _>("PlayProgressMin")? {
                    let min_decimal = min_progress / 100.0;
                    where_conditions.push(format!(
                        "(COALESCE(h.ListenDuration, 0) / NULLIF(e.EpisodeDuration, 0)) >= {}", 
                        min_decimal
                    ));
                    debug!("📊 Added MySQL min progress filter: {}% ({})", min_progress, min_decimal);
                }
                if let Some(max_progress) = playlist.try_get::<Option<f64>, _>("PlayProgressMax")? {
                    let max_decimal = max_progress / 100.0;
                    where_conditions.push(format!(
                        "(COALESCE(h.ListenDuration, 0) / NULLIF(e.EpisodeDuration, 0)) <= {}", 
                        max_decimal
                    ));
                    debug!("📊 Added MySQL max progress filter: {}% ({})", max_progress, max_decimal);
                }
                
                // 6. ULTRA-PRECISE ORDERING with podcast grouping support (MySQL)
                let mut order_parts = Vec::new();
                
                if playlist.try_get::<bool, _>("GroupByPodcast")? {
                    order_parts.push("podcastid".to_string());
                    debug!("📚 MySQL grouping by podcast enabled");
                }
                
                let sort_clause = match playlist.try_get::<String, _>("SortOrder")?.as_str() {
                    "date_asc" => "episodepubdate ASC",
                    "date_desc" => "episodepubdate DESC", 
                    "duration_asc" => "episodeduration ASC",
                    "duration_desc" => "episodeduration DESC",
                    "listen_progress" => "(COALESCE(h.ListenDuration, 0) / NULLIF(e.EpisodeDuration, 0)) DESC",
                    "completion" => "(COALESCE(h.ListenDuration, 0) / NULLIF(e.EpisodeDuration, 0)) DESC",
                    "random" => "RAND()",
                    "title_asc" => "episodetitle ASC",
                    "title_desc" => "episodetitle DESC",
                    "podcast_asc" => "podcastname ASC",
                    "podcast_desc" => "podcastname DESC",
                    _ => {
                        warn!("⚠️ Unknown MySQL sort order '{}', defaulting to date_desc", playlist.try_get::<String, _>("SortOrder")?);
                        "episodepubdate DESC"
                    }
                };
                order_parts.push(sort_clause.to_string());
                debug!("🔄 MySQL Sort order: {}", sort_clause);
                
                // 7. BUILD FINAL MYSQL QUERY
                let where_clause = if where_conditions.is_empty() {
                    String::new()
                } else {
                    format!(" WHERE {}", where_conditions.join(" AND "))
                };
                
                let order_clause = format!(" ORDER BY {}", order_parts.join(", "));
                
                // Apply MaxEpisodes limit if specified (MySQL)
                let limit_clause = if let Some(max_eps) = playlist.try_get::<Option<i32>, _>("MaxEpisodes")? {
                    if max_eps > 0 {
                        format!(" LIMIT {}", max_eps)
                    } else {
                        String::new()
                    }
                } else {
                    String::new()
                };
                
                let final_query = format!("{}{}{}{}", 
                    query_parts.join(" "), where_clause, order_clause, limit_clause);
                
                debug!("🔍 Final MySQL dynamic playlist query: {}", final_query);
                
                // Execute the main MySQL query
                let rows = sqlx::query(&final_query)
                    .bind(user_id)
                    .fetch_all(pool)
                    .await?;

                let mut episodes = Vec::new();
                for row in rows {
                    episodes.push(crate::models::SavedEpisode {
                        episodetitle: row.try_get("episodetitle")?,
                        podcastname: row.try_get("podcastname")?,
                        episodepubdate: row.try_get("episodepubdate")?,
                        episodedescription: row.try_get("episodedescription")?,
                        episodeartwork: row.try_get("episodeartwork")?,
                        episodeurl: row.try_get("episodeurl")?,
                        episodeduration: row.try_get("episodeduration")?,
                        listenduration: row.try_get("listenduration").ok(),
                        episodeid: row.try_get("episodeid")?,
                        websiteurl: row.try_get("websiteurl")?,
                        completed: row.try_get("completed")?,
                        saved: row.try_get("saved")?,
                        queued: row.try_get("queued")?,
                        downloaded: row.try_get("downloaded")?,
                        is_youtube: row.try_get("is_youtube")?,
                        podcastid: row.try_get("podcastid").ok(),
                    });
                }
                
                debug!("📝 Retrieved {} episodes from MySQL dynamic query", episodes.len());
                
                // Create playlist info from the MySQL playlist row we already have
                let playlist_info = crate::models::PlaylistInfo {
                    name: playlist.try_get::<String, _>("Name")?,
                    description: playlist.try_get::<String, _>("Description")?,
                    episode_count: episodes.len() as i32,
                    icon_name: playlist.try_get::<String, _>("IconName")?,
                };
                
                Ok(crate::models::PlaylistEpisodesResponse {
                    episodes,
                    playlist_info,
                })
            }
        }
    }
}

// Standalone create_playlist function that matches Python API
pub async fn create_playlist(pool: &DatabasePool, config: &Config, playlist_data: &crate::models::CreatePlaylistRequest) -> AppResult<i32> {
    pool.create_playlist(config, playlist_data).await
}

// Standalone delete_playlist function that matches Python API
pub async fn delete_playlist(pool: &DatabasePool, _config: &Config, playlist_data: &crate::models::DeletePlaylistRequest) -> AppResult<()> {
    pool.delete_playlist(playlist_data.user_id, playlist_data.playlist_id).await
}