ferium 4.7.1

Fast CLI program for managing Minecraft mods and modpacks from Modrinth, CurseForge, and Github Releases
#![allow(clippy::unwrap_used)]

use crate::TICK;
use anyhow::{Context, Result};
use colored::Colorize;
use ferinth::{
    structures::{project::Project, user::TeamMember},
    Ferinth,
};
use furse::{structures::mod_structs::Mod, Furse};
use futures::{stream::FuturesUnordered, StreamExt as _};
use itertools::{izip, Itertools};
use libium::config::structs::{ModIdentifier, Profile};
use octocrab::{
    models::{repos::Release, Repository},
    OctocrabBuilder,
};

enum Metadata {
    CF(Mod),
    MD(Project, Vec<TeamMember>),
    GH(Repository, Vec<Release>),
}
impl Metadata {
    fn name(&self) -> &str {
        match self {
            Metadata::CF(p) => &p.name,
            Metadata::MD(p, _) => &p.title,
            Metadata::GH(p, _) => &p.name,
        }
    }

    fn id(&self) -> ModIdentifier {
        match self {
            Metadata::CF(p) => ModIdentifier::CurseForgeProject(p.id),
            Metadata::MD(p, _) => ModIdentifier::ModrinthProject(p.id.clone()),
            Metadata::GH(p, _) => {
                ModIdentifier::GitHubRepository((p.owner.clone().unwrap().login, p.name.clone()))
            }
        }
    }
}

pub async fn verbose(md: Ferinth, cf: Furse, profile: &mut Profile, markdown: bool) -> Result<()> {
    if !markdown {
        eprint!("Querying metadata... ");
    }

    let mut tasks = FuturesUnordered::new();
    let mut mr_ids = Vec::new();
    let mut cf_ids = Vec::new();
    for mod_ in &profile.mods {
        match mod_.identifier.clone() {
            ModIdentifier::CurseForgeProject(project_id) => cf_ids.push(project_id),
            ModIdentifier::ModrinthProject(project_id) => mr_ids.push(project_id),
            ModIdentifier::GitHubRepository((owner, repo)) => {
                tasks.push(async {
                    Ok::<_, anyhow::Error>((
                        OctocrabBuilder::new()
                            .build()?
                            .repos(&owner, &repo)
                            .get()
                            .await?,
                        OctocrabBuilder::new()
                            .build()?
                            .repos(owner, repo)
                            .releases()
                            .list()
                            .send()
                            .await?,
                    ))
                });
            }
        }
    }

    let mr_projects = md
        .get_multiple_projects(&mr_ids.iter().map(|s| &**s).collect::<Vec<_>>())
        .await?;
    let mr_teams_members = md
        .list_multiple_teams_members(
            &mr_projects
                .iter()
                .map(|p| &p.team as &str)
                .collect::<Vec<_>>(),
        )
        .await?;

    let cf_projects = if cf_ids.is_empty() {
        Vec::new()
    } else {
        cf.get_mods(cf_ids).await?
    };

    let mut metadata = Vec::new();
    for (project, members) in izip!(mr_projects, mr_teams_members) {
        metadata.push(Metadata::MD(project, members));
    }
    for project in cf_projects {
        metadata.push(Metadata::CF(project));
    }
    while let Some(res) = tasks.next().await {
        let (repo, releases) = res?;
        metadata.push(Metadata::GH(repo, releases.items));
    }
    metadata.sort_unstable_by_key(|e| e.name().to_lowercase());

    if !markdown {
        println!("{}", &*TICK);
    }

    for project in &metadata {
        profile
            .mods
            .iter_mut()
            .find(|mod_| mod_.identifier == project.id())
            .context("Could not find expected mod")?
            .name = project.name().to_string();

        if markdown {
            match project {
                Metadata::CF(p) => curseforge_md(p),
                Metadata::MD(p, t) => modrinth_md(p, t),
                Metadata::GH(p, _) => github_md(p),
            }
        } else {
            match project {
                Metadata::CF(p) => curseforge(p),
                Metadata::MD(p, t) => modrinth(p, t),
                Metadata::GH(p, r) => github(p, r),
            }
        }
    }

    Ok(())
}

pub fn curseforge(project: &Mod) {
    println!(
        "
{}
  {}\n
  Link:         {}
  Source:       {}
  Project ID:   {}
  Open Source:  {}
  Downloads:    {}
  Authors:      {}
  Categories:   {}",
        project.name.bold(),
        project.summary.trim().italic(),
        project.links.website_url.to_string().blue().underline(),
        "CurseForge Mod".dimmed(),
        project.id.to_string().dimmed(),
        project
            .links
            .source_url
            .as_ref()
            .map_or("No".red(), |url| format!(
                "Yes ({})",
                url.to_string().blue().underline()
            )
            .green()),
        project.download_count.to_string().yellow(),
        project
            .authors
            .iter()
            .map(|author| &author.name)
            .format(", ")
            .to_string()
            .cyan(),
        project
            .categories
            .iter()
            .map(|category| &category.name)
            .format(", ")
            .to_string()
            .magenta(),
    );
}

pub fn modrinth(project: &Project, team_members: &[TeamMember]) {
    println!(
        "
{}
  {}\n
  Link:         {}
  Source:       {}
  Project ID:   {}
  Open Source:  {}
  Downloads:    {}
  Authors:      {}
  Categories:   {}
  License:      {}{}",
        project.title.bold(),
        project.description.italic(),
        format!("https://modrinth.com/mod/{}", project.slug)
            .blue()
            .underline(),
        "Modrinth Mod".dimmed(),
        project.id.dimmed(),
        project.source_url.as_ref().map_or("No".red(), |url| {
            format!("Yes ({})", url.to_string().blue().underline()).green()
        }),
        project.downloads.to_string().yellow(),
        team_members
            .iter()
            .map(|member| &member.user.username)
            .format(", ")
            .to_string()
            .cyan(),
        project.categories.iter().format(", ").to_string().magenta(),
        {
            if project.license.name.is_empty() {
                "Custom"
            } else {
                &project.license.name
            }
        },
        project.license.url.as_ref().map_or(String::new(), |url| {
            format!(" ({})", url.to_string().blue().underline())
        }),
    );
}

pub fn github(repo: &Repository, releases: &[Release]) {
    // Calculate number of downloads
    let mut downloads = 0;
    for release in releases {
        for asset in &release.assets {
            downloads += asset.download_count;
        }
    }

    println!(
        "
{}{}\n
  Link:         {}
  Source:       {}
  Identifier:   {}
  Open Source:  {}
  Downloads:    {}
  Authors:      {}
  Topics:       {}
  License:      {}",
        &repo.name.bold(),
        repo.description
            .as_ref()
            .map_or(String::new(), |description| {
                format!("\n  {description}")
            })
            .italic(),
        repo.html_url
            .as_ref()
            .unwrap()
            .to_string()
            .blue()
            .underline(),
        "GitHub Repository".dimmed(),
        repo.full_name.as_ref().unwrap().dimmed(),
        "Yes".green(),
        downloads.to_string().yellow(),
        repo.owner.as_ref().unwrap().login.cyan(),
        repo.topics.as_ref().map_or("".into(), |topics| topics
            .iter()
            .format(", ")
            .to_string()
            .magenta()),
        repo.license
            .as_ref()
            .map_or("None".into(), |license| format!(
                "{}{}",
                license.name,
                license.html_url.as_ref().map_or(String::new(), |url| {
                    format!(" ({})", url.to_string().blue().underline())
                })
            )),
    );
}

pub fn curseforge_md(project: &Mod) {
    println!(
        "
**[{}]({})**  
_{}_

|             |                 |
|-------------|-----------------|
| Source      | CurseForge `{}` |
| Open Source | {}              |
| Authors     | {}              |
| Categories  | {}              |",
        project.name.trim(),
        project.links.website_url,
        project.summary.trim(),
        project.id,
        project
            .links
            .source_url
            .as_ref()
            .map_or("No".into(), |url| format!("[Yes]({url})")),
        project
            .authors
            .iter()
            .map(|author| format!("[{}]({})", author.name, author.url))
            .format(", "),
        project
            .categories
            .iter()
            .map(|category| &category.name)
            .format(", "),
    );
}

pub fn modrinth_md(project: &Project, team_members: &[TeamMember]) {
    println!(
        "
**[{}](https://modrinth.com/mod/{})**  
_{}_

|             |               |
|-------------|---------------|
| Source      | Modrinth `{}` |
| Open Source | {}            |
| Author      | {}            |
| Categories  | {}            |",
        project.title.trim(),
        project.id,
        project.description.trim(),
        project.id,
        project
            .source_url
            .as_ref()
            .map_or("No".into(), |url| { format!("[Yes]({url})") }),
        team_members
            .iter()
            .map(|member| format!(
                "[{}](https://modrinth.com/user/{})",
                member.user.username, member.user.id
            ))
            .format(", "),
        project.categories.iter().format(", "),
    );
}

pub fn github_md(repo: &Repository) {
    println!(
        "
**[{}]({})**{}

|             |             |
|-------------|-------------|
| Source      | GitHub `{}` |
| Open Source | Yes         |
| Owner       | [{}]({})    |{}",
        repo.name,
        repo.html_url.as_ref().unwrap(),
        repo.description
            .as_ref()
            .map_or(String::new(), |description| {
                format!("  \n_{}_", description.trim())
            }),
        repo.full_name.as_ref().unwrap(),
        repo.owner.as_ref().unwrap().login,
        repo.owner.as_ref().unwrap().html_url,
        repo.topics.as_ref().map_or(String::new(), |topics| format!(
            "\n| Topics | {} |",
            topics.iter().format(", ")
        )),
    );
}