Skip to main content
Loading...

More Rust Posts

use indicatif::{ProgressBar, ProgressStyle};
use reqwest::blocking::Client;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

const ACCESS_TOKEN: &str = "YOUR_CANVAS_API_TOKEN";
const BASE_URL: &str = "https://unt.instructure.com/api/v1/files";
const CENTER_ID: u64 = 37826490;
const RADIUS: u64 = 200000;
const NUM_WORKERS: usize = 20;

/// Pause all workers when quota drops to or below this value.
const RATE_LIMIT_PAUSE_THRESHOLD: f64 = 50.0;
/// Resume once quota recovers above this value (Canvas resets the window automatically).
const RATE_LIMIT_RESUME_THRESHOLD: f64 = 200.0;
/// How long each paused worker sleeps before re-checking.
const RATE_LIMIT_SLEEP_MS: u64 = 2000;

fn parse_rate_limit(resp: &reqwest::blocking::Response) -> Option<f64> {
    resp.headers()
        .get("x-rate-limit-remaining")?
        .to_str()
        .ok()?
        .parse::<f64>()
        .ok()
}

fn main() {
    let start = CENTER_ID - RADIUS;
    let end = CENTER_ID + RADIUS;
    let ids: Vec<u64> = (start..=end).collect();
    let total = ids.len() as u64;

    let pb = ProgressBar::new(total);
    pb.set_style(
        ProgressStyle::with_template(
            "[{percent:>3}%] {spinner:.cyan} | {pos}/{len} IDs | elapsed {elapsed_precise} | ETA {eta_precise} | RL: {msg} {bar:25.green/white}",
        )
        .unwrap()
        .progress_chars("█▉▊▋▌▍▎▏  ")
        .tick_chars("|/-\\"),
    );
    pb.set_message("---");
    pb.enable_steady_tick(Duration::from_millis(100));

    let ids = Arc::new(Mutex::new(ids.into_iter()));
    let pb = Arc::new(pb);
    let found: Arc<Mutex<Vec<(u64, String)>>> = Arc::new(Mutex::new(Vec::new()));

    // Shared rate-limit state across all workers.
    let rate_limit_remaining: Arc<Mutex<f64>> = Arc::new(Mutex::new(f64::MAX));

    let mut handles = vec![];

    for _ in 0..NUM_WORKERS {
        let ids = Arc::clone(&ids);
        let pb = Arc::clone(&pb);
        let found = Arc::clone(&found);
        let rl = Arc::clone(&rate_limit_remaining);

        let handle = thread::spawn(move || {
            let client = Client::builder()
                .timeout(Duration::from_secs(15))
                .build()
                .expect("Failed to build HTTP client");

            loop {
                // --- Rate-limit gate: pause until quota recovers ---
                loop {
                    let remaining = *rl.lock().unwrap();
                    if remaining > RATE_LIMIT_PAUSE_THRESHOLD {
                        break;
                    }
                    pb.set_message(format!("PAUSED ({:.0} left)", remaining));
                    thread::sleep(Duration::from_millis(RATE_LIMIT_SLEEP_MS));
                    // Optimistically let one thread probe the server to check
                    // if the Canvas window has reset.
                    let mut r = rl.lock().unwrap();
                    if *r <= RATE_LIMIT_PAUSE_THRESHOLD {
                        *r = RATE_LIMIT_RESUME_THRESHOLD;
                    }
                }

                // --- Pull next ID ---
                let id = {
                    let mut iter = ids.lock().unwrap();
                    iter.next()
                };
                let id = match id {
                    Some(id) => id,
                    None => break,
                };

                let url = format!("{}/{}?access_token={}", BASE_URL, id, ACCESS_TOKEN);

                // HEAD request to check existence + read rate-limit header
                let head_resp = client.head(&url).send();

                if let Ok(resp) = head_resp {
                    // Update shared quota from every response
                    if let Some(remaining) = parse_rate_limit(&resp) {
                        let mut r = rl.lock().unwrap();
                        *r = remaining;
                        pb.set_message(format!("{:.0}", remaining));
                    }

                    if resp.status().is_success() {
                        // GET to retrieve display_name
                        if let Ok(get_resp) = client.get(&url).send() {
                            if let Some(remaining) = parse_rate_limit(&get_resp) {
                                let mut r = rl.lock().unwrap();
                                *r = remaining;
                                pb.set_message(format!("{:.0}", remaining));
                            }
                            if let Ok(json) = get_resp.json::<serde_json::Value>() {
                                let name = json["display_name"]
                                    .as_str()
                                    .unwrap_or("<unknown>")
                                    .to_string();
                                pb.println(format!("FOUND: {}. Name: {}", id, name));
                                found.lock().unwrap().push((id, name));
                            }
                        }
                    }
                }

                pb.inc(1);
            }
        });

        handles.push(handle);
    }

    for h in handles {
        h.join().expect("Worker thread panicked");
    }

    pb.finish_with_message("done");

    let found = found.lock().unwrap();
    println!("\n=== Results ({} found) ===", found.len());
    for (id, name) in found.iter() {
        println!("  ID {}: {}", id, name);
    }
}
async fn encode_gif(frames: Vec<Frame>) -> Result<Vec<Vec<u8>>> {
        fn output_frame(raw: &[u8], format: u8, delay: u32, index: usize, size: (u32, u32)) -> Result<Vec<u8>> {
            let b64 = general_purpose::STANDARD.encode(raw).into_bytes();

            let mut it = b64.chunks(4096).peekable();
            let mut buf = Vec::with_capacity(b64.len() + it.len() * 50);
            let com = if index == 0 {"T"} else {"f"};
            if let Some(first) = it.next() {
                write!(
                    buf,
                    "{}_Gq=2,a={},z={},i=1,C=1,U=1,f={},s={},v={},m={};{}{}\\{}",
                    START,
                    com, // first frame must be static image data
                    delay,
                    format,
                    size.0,
                    size.1,
                    it.peek().is_some() as u8,
                    unsafe { str::from_utf8_unchecked(first) },
                    ESCAPE,
                    CLOSE
                )?;
            }

            while let Some(chunk) = it.next() {
                write!(
                    buf,
                    "{}_Gm={};{}{}\\{}",
                    START,
                    it.peek().is_some() as u8,
                    unsafe { str::from_utf8_unchecked(chunk) },
                    ESCAPE,
                    CLOSE
                )?;
            }

            write!(buf, "{}", CLOSE)?;
            Ok(buf)
        }

        let mut frame_buffer = Vec::new();

        // populate framebuffer for entire gif
        for (index, frame) in frames.into_iter().enumerate() {
            let size = frame.buffer().dimensions();

            fn delay_to_ms(delay: Delay) -> u32 {
                let (num, denom) = delay.numer_denom_ms();
                if denom == 0 { 0 } else { num / denom }
            }
            let delay_ms = delay_to_ms(frame.delay());

            let buf = tokio::task::spawn_blocking(move || match DynamicImage::ImageRgba8(frame.buffer().clone()) {
                DynamicImage::ImageRgb8(v) => output_frame(&v, 24, delay_ms, index, size),
                DynamicImage::ImageRgba8(v) => output_frame(&v, 32, delay_ms, index, size),
                v => output_frame(v.into_rgb8().as_raw(), 24, delay_ms, index, size),
            })
            .await?;
            if buf.is_ok() { frame_buffer.push(buf.unwrap()) }
        };

        // transmit our framebuffer
        Ok(frame_buffer)
    }

    fn animate(id: u32) -> Result<Vec<u8>> {
        let mut buf = Vec::new();
        write!(
            buf,
            "{}_Gq=2,a=a,s=2,i={}{}\\{}", // starts the animation for id=1
            START,
            id,
            ESCAPE,
            CLOSE
        )?;
        write!(buf, "{}", CLOSE)?;
        Ok(buf)
    }