/* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ use crate::cli::Action; use anyhow::anyhow; use dialoguer::{theme::ColorfulTheme, Confirm, FuzzySelect, Input}; use jellybase::SECRETS; use jellycommon::TraktKind; use jellyimport::trakt::Trakt; use log::warn; use std::{ fmt::Display, path::{Path, PathBuf}, }; use tokio::{ fs::{rename, OpenOptions}, io::AsyncWriteExt, }; pub async fn add(action: Action) -> anyhow::Result<()> { match action { Action::Add { media } => { let theme = ColorfulTheme::default(); let search_kinds = if media.is_dir() { &[TraktKind::Show, TraktKind::Season] } else { &[TraktKind::Movie, TraktKind::Episode] }; let (trakt_object, trakt_kind) = loop { let name: String = Input::with_theme(&theme) .with_prompt("Search by title") .default(path_to_query(&media)) .interact_text() .unwrap(); let trakt = Trakt::new( SECRETS .api .trakt .as_ref() .ok_or(anyhow!("no trakt api key configured"))?, ); let results = trakt.search(search_kinds, &name, false).await?; if results.is_empty() { warn!("no search results"); continue; } let correct = FuzzySelect::with_theme(&theme) .items(&results) .default(0) .with_prompt("Metadata Source") .interact_opt() .unwrap(); if let Some(o) = correct { break (results[o].inner.inner().to_owned(), results[o].r#type); } }; if media.is_dir() { let flagspath = media.join("flags"); let flag = format!( "trakt={}:{}\n", match trakt_kind { TraktKind::Movie => "movie", TraktKind::Show => "show", TraktKind::Season => "season", TraktKind::Episode => "episode", _ => unreachable!(), }, trakt_object.ids.trakt.unwrap() ); if Confirm::with_theme(&theme) .with_prompt(format!("Append {flag:?} to {flagspath:?}?")) .default(true) .interact() .unwrap() { OpenOptions::new() .append(true) .write(true) .create(true) .open(flagspath) .await? .write_all(flag.as_bytes()) .await?; } } else { let ext = media .extension() .map(|e| format!(".{}", e.to_string_lossy())) .unwrap_or("mkv".to_string()); let stem = media.file_name().unwrap().to_string_lossy().to_string(); let stem = stem.split_once(".").unwrap_or((stem.as_str(), "")).0; let mut newpath = media.parent().unwrap().join(format!( "{stem}.trakt-{}{ext}", trakt_object.ids.trakt.unwrap() )); let mut n = 1; while newpath.exists() { newpath = media .parent() .unwrap() .join(format!("{stem}.alt-{n}{ext}",)); n += 1; } if Confirm::with_theme(&theme) .with_prompt(format!("Rename {media:?} -> {newpath:?}?")) .default(true) .interact() .unwrap() { rename(media, newpath).await?; } } Ok(()) } _ => unreachable!(), } } // fn validate_id(s: &String) -> anyhow::Result<()> { // if &make_id(s) == s { // Ok(()) // } else { // bail!("invalid id") // } // } // fn make_id(s: &str) -> String { // let mut out = String::new(); // for s in s.chars() { // match s { // 'a'..='z' | '0'..='9' => out.push(s), // 'A'..='Z' => out.push(s.to_ascii_lowercase()), // '-' | ' ' | '_' | ':' => out.push('-'), // _ => (), // } // } // out // } fn path_to_query(path: &Path) -> String { let stem = path.file_name().unwrap().to_string_lossy().to_string(); let stem = stem.split_once(".").unwrap_or((stem.as_str(), "")).0; stem.replace("-", " ") } pub struct PathDisplay(PathBuf); impl Display for PathDisplay { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("/")?; f.write_str(self.0.to_str().unwrap()) } }