framesmith 0.1.0

A Rust library for controlling Samsung Frame TVs over the local network
Documentation
//! Manage artwork on a Samsung Frame TV.
//!
//! Usage:
//!   cargo run --example manage_art -- <TV_IP> <COMMAND> [ARGS...]
//!
//! Commands:
//!   list                       List all uploaded images
//!   select <ID>                Display an image by its content ID
//!   delete <ID> [ID...]        Delete one or more images
//!   favorite <ID>              Mark an image as a favorite
//!   unfavorite <ID>            Remove an image from favorites
//!   mattes                     List available mattes
//!   set-matte <ID> <MATTE_ID>  Apply a matte to an image
//!   filters                    List available photo filters
//!   set-filter <ID> <FILTER>   Apply a photo filter to an image
//!   thumbnail <ID> <OUT_PATH>  Download an image thumbnail
//!
//! Examples:
//!   cargo run --example manage_art -- 192.168.1.100 list
//!   cargo run --example manage_art -- 192.168.1.100 select MY_F0001
//!   cargo run --example manage_art -- 192.168.1.100 delete MY_F0001 MY_F0002
//!   cargo run --example manage_art -- 192.168.1.100 set-matte MY_F0001 flexible_dx_moderate
//!   cargo run --example manage_art -- 192.168.1.100 thumbnail MY_F0001 thumb.jpg

use std::net::IpAddr;

use framesmith::{FileAuthTokenStore, FrameTv};

#[tokio::main]
async fn main() -> Result<(), framesmith::Error> {
    let args: Vec<String> = std::env::args().collect();
    if args.len() < 3 {
        eprintln!("Usage: {} <TV_IP> <COMMAND> [ARGS...]", args[0]);
        eprintln!("Run with --help for available commands");
        std::process::exit(1);
    }

    let ip: IpAddr = args[1].parse().expect("invalid IP address");
    let command = args[2].as_str();

    let token_path = std::env::temp_dir().join("framesmith_token.txt");
    let tv = FrameTv::connection_builder(ip)
        .auth_token_store(FileAuthTokenStore::new(&token_path))
        .connect()
        .await?;

    match command {
        "list" => {
            let images = tv.get_uploaded_art_images().await?;
            if images.is_empty() {
                println!("No uploaded images.");
                return Ok(());
            }

            let selected = tv.get_selected_art_image().await.ok();
            let selected_id = selected.as_ref().map(|s| s.id());

            println!("{:<16} {:>5} x {:<5} {:<8}", "ID", "W", "H", "MATTE");
            println!("{}", "-".repeat(60));
            for img in &images {
                let marker = if selected_id == Some(img.id()) {
                    " <-- displaying"
                } else {
                    ""
                };
                let matte = img.matte_id().unwrap_or("-");
                println!(
                    "{:<16} {:>5} x {:<5} {:<8}{}",
                    img.id(),
                    img.width(),
                    img.height(),
                    matte,
                    marker,
                );
            }
            println!("\n{} image(s) total", images.len());
        }

        "select" => {
            let id = require_arg(&args, 3, "content ID");
            tv.select_art_image_by_id(&id).await?;
            println!("Now displaying: {id}");
        }

        "delete" => {
            if args.len() < 4 {
                eprintln!("Usage: delete <ID> [ID...]");
                std::process::exit(1);
            }
            let ids: Vec<&str> = args[3..].iter().map(|s| s.as_str()).collect();
            tv.delete_uploaded_art_images(&ids).await?;
            println!("Deleted {} image(s)", ids.len());
        }

        "favorite" => {
            let id = require_arg(&args, 3, "content ID");
            tv.set_art_image_favorite(&id, true).await?;
            println!("Marked {id} as favorite");
        }

        "unfavorite" => {
            let id = require_arg(&args, 3, "content ID");
            tv.set_art_image_favorite(&id, false).await?;
            println!("Removed {id} from favorites");
        }

        "mattes" => {
            let mattes = tv.get_available_mattes().await?;
            println!("{:<30} TYPE", "ID");
            println!("{}", "-".repeat(50));
            for matte in &mattes {
                println!("{:<30} {}", matte.id(), matte.matte_type());
            }
        }

        "set-matte" => {
            let id = require_arg(&args, 3, "content ID");
            let matte_id = require_arg(&args, 4, "matte ID");
            tv.set_art_image_matte(&id, &matte_id, None).await?;
            println!("Applied matte '{matte_id}' to {id}");
        }

        "filters" => {
            let filters = tv.get_available_photo_filters().await?;
            println!("{:<20} NAME", "ID");
            println!("{}", "-".repeat(40));
            for filter in &filters {
                println!("{:<20} {}", filter.id(), filter.name());
            }
        }

        "set-filter" => {
            let id = require_arg(&args, 3, "content ID");
            let filter_id = require_arg(&args, 4, "filter ID");
            tv.set_art_image_photo_filter(&id, &filter_id).await?;
            println!("Applied filter '{filter_id}' to {id}");
        }

        "thumbnail" => {
            let id = require_arg(&args, 3, "content ID");
            let out_path = require_arg(&args, 4, "output path");
            let thumbnail = tv.get_art_image_thumbnail(&id).await?;
            std::fs::write(&out_path, thumbnail.as_bytes())?;
            println!(
                "Saved thumbnail to {out_path} ({} bytes)",
                thumbnail.as_bytes().len()
            );
        }

        other => {
            eprintln!("Unknown command: {other}");
            eprintln!(
                "Available: list, select, delete, favorite, unfavorite, mattes, set-matte, filters, set-filter, thumbnail"
            );
            std::process::exit(1);
        }
    }

    Ok(())
}

fn require_arg(args: &[String], index: usize, name: &str) -> String {
    args.get(index).cloned().unwrap_or_else(|| {
        eprintln!("Missing required argument: <{name}>");
        std::process::exit(1);
    })
}