Save it like /{placeID}/{versionNum}_{hash}.rbxl 
Instead of /{placeID}/{versionNum}/{hash}.rbxl 

use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::time::Duration;

use flate2::read::GzDecoder;
use futures::stream::{self, StreamExt};
use reqwest::Client;
use tokio::fs::{create_dir_all, File};
use tokio::io::AsyncWriteExt;
use tracing::{error, info};

#[tokio::main]
async fn main() {
    // Initialize the tracing subscriber for logging
    tracing_subscriber::fmt::init();

    // Define the base directory where index.txt files are located (reading directory)
    let read_dir = Path::new(r"C:\Users\Lukas\Downloads\lkjhgfd2");

    // Define the base directory where downloads will be saved (download directory)
    let download_dir = Path::new(r"C:\Users\Lukas\Downloads\lkjhgfd3");

    // Ensure the download directory exists
    if let Err(e) = fs::create_dir_all(&download_dir) {
        error!(
            "Failed to create download directory {}: {}",
            download_dir.display(),
            e
        );
        return;
    }

    // List of placeIDs
    let place_ids = vec![
        "10789565990",
        "11181971058",
        "11181982436",
        "11182034319",
        "11182037286",
        "11263735303",
        "11263873956",
        "12647442701",
        "33564694",
        "4568576845",
        "4737314314",
        "4748963513",
        "4956810680",
        "4998492434",
        "5000291183",
        "5063342777",
        "5229799451",
        "5400427674",
        "5465723678",
        "5914914682",
        "5972110179",
        "6087359275",
        "6701464352",
        "6767882272",
        "6768240609",
        "6783491168",
        "7162161171",
        "7730658758",
        "9048939285",
    ];

    // Initialize HTTP client with timeout and other configurations
    let client = Client::builder()
        .timeout(Duration::from_secs(30))
        .build()
        .expect("Failed to build HTTP client");

    // Process each placeID concurrently with a limit on concurrency
    let concurrency_limit = 5; // Adjust based on your network capability
    stream::iter(place_ids)
        .for_each_concurrent(concurrency_limit, |place_id| {
            let client = client.clone();
            let read_dir = read_dir.to_path_buf();
            let download_dir = download_dir.to_path_buf();
            async move {
                if let Err(e) = process_place_id(&client, &read_dir, &download_dir, place_id).await {
                    error!("Error processing placeID {}: {}", place_id, e);
                }
            }
        })
        .await;

    info!("All downloads completed.");
}

/// Processes a single placeID:
/// - Reads index.txt from the reading directory
/// - Parses entries
/// - Downloads assets to the download directory
async fn process_place_id(
    client: &Client,
    read_dir: &PathBuf,
    download_dir: &PathBuf,
    place_id: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    info!("Processing placeID: {}", place_id);

    // Path to index.txt in the reading directory
    let index_path = read_dir.join(place_id).join("index.txt");

    // Read index.txt content
    let index_content = match fs::read_to_string(&index_path) {
        Ok(content) => content,
        Err(e) => {
            error!("Failed to read {}: {}", index_path.display(), e);
            return Err(Box::new(e));
        }
    };

    // Parse index.txt lines into a vector of (version, url) tuples
    let assets: Vec<(String, String)> = index_content
        .lines()
        .filter_map(|line| {
            let parts: Vec<&str> = line.split('|').map(|s| s.trim()).collect();
            if parts.len() == 2 {
                Some((parts[0].to_string(), parts[1].to_string()))
            } else {
                None
            }
        })
        .collect();

    info!("---------------------");
    info!("Found {} assets for placeID {}", assets.len(), place_id);
    info!("---------------------");

    // Define concurrency for asset downloads per placeID
    let asset_concurrency = 10; // Adjust as needed

    // Process asset downloads concurrently
    stream::iter(assets)
        .for_each_concurrent(asset_concurrency, |(version, url)| {
            let client = client.clone();
            let download_dir = download_dir.clone();
            let place_id = place_id.to_string();
            async move {
                if let Err(e) =
                    download_asset(&client, &download_dir, &place_id, &version, &url).await
                {
                    error!(
                        "Failed to download asset {} for placeID {}: {}",
                        url, place_id, e
                    );
                }
            }
        })
        .await;

    info!("Completed placeID: {}", place_id);
    Ok(())
}

/// Downloads a single asset, decompresses it if gzipped, and saves it to the designated path
async fn download_asset(
    client: &Client,
    download_dir: &PathBuf,
    place_id: &str,
    version: &str,
    url: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    // Extract asset filename from URL
    let original_filename = match url.split('/').last() {
        Some(name) => name,
        None => {
            return Err("Failed to extract asset filename from URL".into());
        }
    };

    // Remove any query parameters or fragments from the filename if present
    let original_filename = original_filename.split('?').next().unwrap_or(original_filename);
    let original_filename = original_filename.split('#').next().unwrap_or(original_filename);

    // Define the target directory and file path in the download directory
    let target_dir = download_dir.join(place_id).join(version);
    let asset_filename = format!("{}.rbxl", original_filename);
    let target_path = target_dir.join(&asset_filename);

    // Create the target directory if it doesn't exist
    create_dir_all(&target_dir).await?;

    // Check if the file already exists to avoid re-downloading
    if target_path.exists() {
        info!("Asset already exists: {:?}", target_path);
        return Ok(());
    }

    // Make the HTTP GET request
    let response = client.get(url).send().await?;

    if !response.status().is_success() {
        return Err(format!("Failed to download {}: HTTP {}", url, response.status()).into());
    }

    // Determine if the content is gzipped
    let is_gzipped = response
        .headers()
        .get(reqwest::header::CONTENT_ENCODING)
        .and_then(|v| v.to_str().ok())
        .map_or(false, |encoding| encoding.contains("gzip"));

    // Collect the response bytes
    let bytes = response.bytes().await?;

    // Check magic numbers if header is absent
    let is_gzipped = is_gzipped || bytes.starts_with(&[0x1f, 0x8b]);

    // Decompress if gzipped
    let data: Vec<u8> = if is_gzipped {
        info!("Decompressing gzipped asset: {}", url);
        let mut d = GzDecoder::new(&bytes[..]);
        let mut decompressed = Vec::new();
        d.read_to_end(&mut decompressed)?;
        decompressed
    } else {
        bytes.to_vec()
    };

    // Write the (decompressed) data to the file
    let mut file = File::create(&target_path).await?;
    file.write_all(&data).await?;

    info!("Downloaded and saved: {:?}", target_path);
    Ok(())
}