leetcode-tui-rs 0.6.1

Leetcode terminal UI. Helps you browse leetcode stats and manage your leetcode from terminal.
use api::{GQLLeetcodeRequest, QuestionRequest};
use color_eyre::Result;
use kdam::BarExt;
use leetcode_core as api;
use leetcode_tui_core::emit;
use leetcode_tui_db::DbQuestion;
use std::time::Duration;

fn should_update_db(runs_inside_tui: bool) -> Result<bool> {
    let first_time_start = DbQuestion::get_total_questions()? == 0 && !runs_inside_tui;
    if first_time_start || runs_inside_tui {
        return Ok(true);
    } else {
        return Ok(false);
    }
}

async fn sync_via_rest_api(runs_inside_tui: bool) -> Result<Vec<DbQuestion>> {
    log::info!("Attempting to sync via REST API /api/problems/all");
    let response = api::fetch_all_problems_rest().await?;

    let total = response.problems.len();
    log::info!("REST API returned {} problems", total);

    let mut db_questions = Vec::with_capacity(total);
    let mut skipped = 0;

    for problem in &response.problems {
        if problem.stat.hide {
            skipped += 1;
            continue;
        }

        match DbQuestion::try_from_rest_problem(problem) {
            Ok(q) => db_questions.push(q),
            Err(e) => {
                log::warn!(
                    "Failed to convert problem {} ({}): {}",
                    problem.stat.frontend_question_id,
                    problem.stat.title,
                    e
                );
            }
        }
    }

    if skipped > 0 {
        log::info!("Skipped {} hidden problems", skipped);
    }

    if runs_inside_tui {
        emit!(ProgressUpdate(
            "Syncing db...".into(),
            db_questions.len() as u32,
            db_questions.len() as u32
        ));
    }

    Ok(db_questions)
}

async fn sync_via_graphql(runs_inside_tui: bool) -> Result<Vec<DbQuestion>> {
    log::info!("Falling back to GraphQL API for sync");
    let query = api::QuestionRequest::default();
    let query_response = query.send().await?;
    let total_questions = query_response.get_total_questions();

    let chunk_size = 50;
    let total_pages = (total_questions + chunk_size - 1) / chunk_size;

    let mut all_questions = Vec::with_capacity(total_questions as usize);
    let mut failed_pages = 0;

    let mut cli_progress_bar = kdam::tqdm!(total = total_questions as usize);

    for page in 0..total_pages {
        let skip = page * chunk_size;
        let take = chunk_size.min(total_questions - skip);

        match QuestionRequest::new(take, skip).send().await {
            Ok(resp) => {
                let questions = resp.get_questions();
                let db_questions: Vec<DbQuestion> = questions
                    .into_iter()
                    .filter_map(|q| match q.try_into() {
                        Ok(q) => Some(q),
                        Err(e) => {
                            log::warn!("Failed to convert question: {}", e);
                            None
                        }
                    })
                    .collect();

                all_questions.extend(db_questions);
            }
            Err(e) => {
                log::warn!("Failed to fetch page {} (skip={}): {}", page, skip, e);
                failed_pages += 1;
            }
        }

        if runs_inside_tui {
            emit!(ProgressUpdate(
                "Syncing db...".into(),
                all_questions.len() as u32,
                total_questions as u32
            ));
        } else {
            cli_progress_bar.update(take as usize)?;
        }

        if page < total_pages - 1 {
            tokio::time::sleep(Duration::from_millis(200)).await;
        }
    }

    if failed_pages > 0 {
        log::warn!("{} pages failed to fetch", failed_pages);
    }

    Ok(all_questions)
}

pub async fn update_database_questions(runs_inside_tui: bool) -> Result<()> {
    if !should_update_db(runs_inside_tui)? {
        return Ok(());
    }

    let all_questions = match sync_via_rest_api(runs_inside_tui).await {
        Ok(questions) => {
            log::info!(
                "Successfully synced {} questions via REST API",
                questions.len()
            );
            questions
        }
        Err(e) => {
            log::warn!("REST API sync failed: {}. Falling back to GraphQL.", e);
            sync_via_graphql(runs_inside_tui).await?
        }
    };

    if all_questions.is_empty() {
        log::warn!("No questions were fetched. Database will not be updated.");
        return Ok(());
    }

    DbQuestion::save_multiple_to_db(all_questions);
    Ok(())
}