framesmith-cli 0.1.0

CLI tool for controlling Samsung Frame TVs over the local network
use anyhow::{Result, bail};

use crate::cli::{
    ArtCommand, FilterCommand, MatteCommand, ModeCommand, OnOff, SlideshowCategoryValue,
    SlideshowCommand,
};
use crate::executor::Executor;
use crate::ipc::{Request, Response};

pub async fn run(exec: &Executor, sub: &ArtCommand, json: bool) -> Result<()> {
    let request = match sub {
        ArtCommand::List => Request::ArtList,
        ArtCommand::ShowInfo { content_id } => Request::ArtShowInfo {
            content_id: content_id.clone(),
        },
        ArtCommand::Select { content_id } => Request::ArtSelect {
            content_id: content_id.clone(),
        },
        ArtCommand::Upload {
            file,
            select,
            replace_selected,
            canvas,
        } => Request::ArtUpload {
            file_path: file.clone(),
            select: *select,
            replace_selected: *replace_selected,
            canvas_color: canvas.clone(),
        },
        ArtCommand::Delete { content_ids } => Request::ArtDelete {
            content_ids: content_ids.clone(),
        },
        ArtCommand::Thumbnail { content_id, o } => Request::ArtThumbnail {
            content_id: content_id.clone(),
            output_path: o.clone(),
        },
        ArtCommand::Favorite { content_id, state } => Request::ArtFavorite {
            content_id: content_id.clone(),
            on: matches!(state, OnOff::On),
        },
        ArtCommand::Matte { sub } => match sub {
            MatteCommand::List => Request::MatteList,
            MatteCommand::Set {
                content_id,
                matte_id,
                portrait,
            } => Request::MatteSet {
                content_id: content_id.clone(),
                matte_id: matte_id.clone(),
                portrait: portrait.clone(),
            },
        },
        ArtCommand::Filter { sub } => match sub {
            FilterCommand::List => Request::FilterList,
            FilterCommand::Set {
                content_id,
                filter_id,
            } => Request::FilterSet {
                content_id: content_id.clone(),
                filter_id: filter_id.clone(),
            },
        },
        ArtCommand::Mode { sub } => match sub {
            ModeCommand::On => Request::ModeOn,
            ModeCommand::Off => Request::ModeOff,
            ModeCommand::Status => Request::ModeStatus,
        },
        ArtCommand::Slideshow { sub } => match sub {
            SlideshowCommand::On {
                duration,
                shuffle,
                category,
            } => Request::SlideshowOn {
                duration_secs: *duration,
                shuffle: *shuffle,
                category: category.as_ref().map(|c| match c {
                    SlideshowCategoryValue::MyPictures => "my-pictures".to_string(),
                    SlideshowCategoryValue::Favorites => "favorites".to_string(),
                }),
            },
            SlideshowCommand::Off => Request::SlideshowOff,
            SlideshowCommand::Status => Request::SlideshowStatus,
            SlideshowCommand::Configure {
                duration,
                shuffle,
                category,
            } => Request::SlideshowConfigure {
                duration_secs: *duration,
                shuffle: shuffle.as_ref().map(|s| matches!(s, OnOff::On)),
                category: category.as_ref().map(|c| match c {
                    SlideshowCategoryValue::MyPictures => "my-pictures".to_string(),
                    SlideshowCategoryValue::Favorites => "favorites".to_string(),
                }),
            },
        },
    };

    let response = exec.send_request(request).await?;
    render(sub, &response, json)
}

fn render(sub: &ArtCommand, response: &Response, json: bool) -> Result<()> {
    let data = match response {
        Response::Ok { data } => data,
        Response::Error { message } => bail!("{message}"),
        Response::TvDisconnected { message } => bail!("{message}"),
    };

    match sub {
        ArtCommand::List => {
            if json {
                println!("{}", serde_json::to_string_pretty(data)?);
            } else {
                let empty = vec![];
                let items = data.as_array().unwrap_or(&empty);
                if items.is_empty() {
                    println!("No uploaded art images.");
                } else {
                    let id_w = items
                        .iter()
                        .filter_map(|i| i.get("content_id").and_then(|v| v.as_str()))
                        .map(|s| s.len())
                        .max()
                        .unwrap_or(10)
                        .max(10);
                    println!("{:<id_w$}  {:>5} x {:<5}  MATTE", "CONTENT ID", "W", "H");
                    for img in items {
                        let id = img.get("content_id").and_then(|v| v.as_str()).unwrap_or("");
                        let w = img.get("width").and_then(|v| v.as_u64()).unwrap_or(0);
                        let h = img.get("height").and_then(|v| v.as_u64()).unwrap_or(0);
                        let matte = img.get("matte_id").and_then(|v| v.as_str()).unwrap_or("-");
                        println!("{id:<id_w$}  {w:>5} x {h:<5}  {matte}");
                    }
                }
            }
        }

        ArtCommand::ShowInfo { .. } => {
            if json {
                println!("{}", serde_json::to_string_pretty(data)?);
            } else {
                let id = data
                    .get("content_id")
                    .and_then(|v| v.as_str())
                    .unwrap_or("");
                let cat = data.get("category").and_then(|v| v.as_str()).unwrap_or("");
                let w = data.get("width").and_then(|v| v.as_u64()).unwrap_or(0);
                let h = data.get("height").and_then(|v| v.as_u64()).unwrap_or(0);
                println!("Content ID: {id}");
                println!("Category:   {cat}");
                println!("Size:       {w}x{h}");
                if let Some(matte) = data.get("matte_id").and_then(|v| v.as_str()) {
                    println!("Matte:      {matte}");
                }
                if let Some(portrait) = data.get("portrait_matte_id").and_then(|v| v.as_str()) {
                    println!("Portrait matte: {portrait}");
                }
            }
        }

        ArtCommand::Select { content_id } => {
            if json {
                println!("{}", serde_json::to_string_pretty(data)?);
            } else {
                println!("Selected {content_id}.");
            }
        }

        ArtCommand::Upload {
            select,
            replace_selected,
            ..
        } => {
            if json {
                println!("{}", serde_json::to_string_pretty(data)?);
            } else {
                let id = data
                    .get("content_id")
                    .and_then(|v| v.as_str())
                    .unwrap_or("");
                println!("Uploaded: {id}");
                if *select || *replace_selected {
                    println!("Selected for display.");
                }
                if data
                    .get("replaced")
                    .and_then(|v| v.as_bool())
                    .unwrap_or(false)
                {
                    println!("Deleted previous selection.");
                }
            }
        }

        ArtCommand::Delete { content_ids } => {
            if json {
                println!("{}", serde_json::to_string_pretty(data)?);
            } else {
                println!("Deleted {} image(s).", content_ids.len());
            }
        }

        ArtCommand::Thumbnail { .. } => {
            if json {
                println!("{}", serde_json::to_string_pretty(data)?);
            } else {
                let path = data.get("path").and_then(|v| v.as_str()).unwrap_or("");
                let size = data.get("size").and_then(|v| v.as_u64()).unwrap_or(0);
                println!("Thumbnail saved to {path} ({size} bytes).");
            }
        }

        ArtCommand::Favorite { content_id, state } => {
            if json {
                println!("{}", serde_json::to_string_pretty(data)?);
            } else {
                let word = match state {
                    OnOff::On => "set",
                    OnOff::Off => "removed",
                };
                println!("Favorite {word} on {content_id}.");
            }
        }

        ArtCommand::Matte { sub } => match sub {
            MatteCommand::List => {
                if json {
                    println!("{}", serde_json::to_string_pretty(data)?);
                } else {
                    let empty = vec![];
                    let items = data.as_array().unwrap_or(&empty);
                    if items.is_empty() {
                        println!("No mattes available.");
                    } else {
                        let id_w = items
                            .iter()
                            .filter_map(|m| m.get("matte_id").and_then(|v| v.as_str()))
                            .map(|s| s.len())
                            .max()
                            .unwrap_or(8)
                            .max(8);
                        println!("{:<id_w$}  TYPE", "MATTE ID");
                        for m in items {
                            let id = m.get("matte_id").and_then(|v| v.as_str()).unwrap_or("");
                            let mt = m.get("matte_type").and_then(|v| v.as_str()).unwrap_or("");
                            println!("{id:<id_w$}  {mt}");
                        }
                    }
                }
            }
            MatteCommand::Set { content_id, .. } => {
                if json {
                    println!("{}", serde_json::to_string_pretty(data)?);
                } else {
                    println!("Matte set on {content_id}.");
                }
            }
        },

        ArtCommand::Filter { sub } => match sub {
            FilterCommand::List => {
                if json {
                    println!("{}", serde_json::to_string_pretty(data)?);
                } else {
                    let empty = vec![];
                    let items = data.as_array().unwrap_or(&empty);
                    if items.is_empty() {
                        println!("No photo filters available.");
                    } else {
                        let id_w = items
                            .iter()
                            .filter_map(|f| f.get("filter_id").and_then(|v| v.as_str()))
                            .map(|s| s.len())
                            .max()
                            .unwrap_or(9)
                            .max(9);
                        println!("{:<id_w$}  NAME", "FILTER ID");
                        for f in items {
                            let id = f.get("filter_id").and_then(|v| v.as_str()).unwrap_or("");
                            let name = f.get("filter_name").and_then(|v| v.as_str()).unwrap_or("");
                            println!("{id:<id_w$}  {name}");
                        }
                    }
                }
            }
            FilterCommand::Set { content_id, .. } => {
                if json {
                    println!("{}", serde_json::to_string_pretty(data)?);
                } else {
                    println!("Filter set on {content_id}.");
                }
            }
        },

        ArtCommand::Mode { sub } => match sub {
            ModeCommand::On => {
                if json {
                    println!("{}", serde_json::to_string_pretty(data)?);
                } else {
                    println!("Art mode enabled.");
                }
            }
            ModeCommand::Off => {
                if json {
                    println!("{}", serde_json::to_string_pretty(data)?);
                } else {
                    println!("Art mode disabled.");
                }
            }
            ModeCommand::Status => {
                if json {
                    println!("{}", serde_json::to_string_pretty(data)?);
                } else {
                    let enabled = data
                        .get("art_mode")
                        .and_then(|v| v.as_bool())
                        .unwrap_or(false);
                    println!("Art mode: {}", if enabled { "on" } else { "off" });
                }
            }
        },

        ArtCommand::Slideshow { sub } => match sub {
            SlideshowCommand::On { .. } => {
                if json {
                    println!("{}", serde_json::to_string_pretty(data)?);
                } else {
                    println!("Slideshow enabled.");
                }
            }
            SlideshowCommand::Off => {
                if json {
                    println!("{}", serde_json::to_string_pretty(data)?);
                } else {
                    println!("Slideshow disabled.");
                }
            }
            SlideshowCommand::Status => {
                if json {
                    println!("{}", serde_json::to_string_pretty(data)?);
                } else {
                    let enabled = data
                        .get("enabled")
                        .and_then(|v| v.as_bool())
                        .unwrap_or(false);
                    let dur = data
                        .get("duration_secs")
                        .and_then(|v| v.as_u64())
                        .unwrap_or(0);
                    let shuffle = data
                        .get("shuffle")
                        .and_then(|v| v.as_bool())
                        .unwrap_or(false);
                    let cat = data
                        .get("category")
                        .and_then(|v| v.as_str())
                        .unwrap_or("unknown");
                    println!("Slideshow:  {}", if enabled { "on" } else { "off" });
                    println!("Duration:   {dur}s");
                    println!("Shuffle:    {}", if shuffle { "on" } else { "off" });
                    println!("Category:   {cat}");
                }
            }
            SlideshowCommand::Configure { .. } => {
                if json {
                    println!("{}", serde_json::to_string_pretty(data)?);
                } else {
                    println!("Slideshow configuration updated.");
                }
            }
        },
    }

    Ok(())
}