From 64ab045a961cbf3ca616737fb8aa5a52a076f9e2 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 10 Oct 2026 03:12:52 -0500 Subject: [PATCH] Initial commit --- .gitignore | 3 ++ Cargo.toml | 10 ++++++ src/main.rs | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4090561 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/test_data +Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e464520 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "link-manager" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.6.1", features = ["derive"] } +itertools = "0.15.0" +regex = "1.13.1" +terminal_size = "0.4.4" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7a4d0ef --- /dev/null +++ b/src/main.rs @@ -0,0 +1,94 @@ +use std::{io::BufRead, path::PathBuf}; +use terminal_size; + +use regex; +use clap::Parser; + +fn main() { + let cli = Cli::parse(); + + let source_folder = std::fs::read_dir(std::path::absolute(cli.src).unwrap()).unwrap(); + let target_path = std::path::absolute(cli.dst).unwrap(); + + let episode_matching = regex::Regex::new(r"(?i) s((?:eason\s)?\d+).*?E?((?:pisode)?\d+).*\.(\w+)").unwrap(); + let mut found_episodes: Vec = Vec::new(); + + for f in source_folder { + let file = f.unwrap().path(); + let filename = file.file_name().unwrap(); + let captures = episode_matching.captures(filename.to_str().unwrap()); + match captures { + Some(c) => { + found_episodes.push(EpisodeID { + path: file.clone(), + filename: file.file_name().unwrap().to_string_lossy().to_string(), + season: c.get(1).unwrap().as_str().parse().unwrap(), + episode: c.get(2).unwrap().as_str().parse().unwrap(), + filetype: c.get(3).unwrap().as_str().to_string(), + }); + }, + None => continue, + }; + } + found_episodes.sort_by(|x, y| x.episode.cmp(&y.episode)); + + let max_len = found_episodes.iter().max_by(|x, y| { + x.filename.len().cmp(&y.filename.len()) + }).unwrap().filename.len(); + let term_width = terminal_size::terminal_size().unwrap().0.0; + + let max_filename_cols = (term_width as usize / (max_len + 4)).clamp(1, 5); + let filename_rows = (found_episodes.len()/max_filename_cols).clamp(1, usize::MAX); + + let mut arranged:Vec> = vec![Vec::new();filename_rows]; + + for (i, f) in found_episodes.iter().enumerate() { + arranged[i%filename_rows].push(format!("{:size$}", f.filename, size = max_len+4)); + } + for i in arranged.iter() { + for j in i { + print!("{j}"); + } + println!(""); + } + + println!("Found {} episodes.", found_episodes.len()); + println!("Create links at {}? [y/N]", target_path.to_string_lossy()); + + loop { + let mut line = String::new(); + let _read = std::io::stdin() + .lock() + .read_line(&mut line).unwrap(); + + match line.as_str().trim_end() { + "y"|"Y" => break, + "n"|"N" => return, + _ => println!("Not recognized please type \"y\" for yes or \"n\" for no") + } + } + + for i in found_episodes { + let mut target = target_path.clone(); + target.push(format!("S{:02}E{:02}.{}", i.season, i.episode, i.filetype)); + std::os::unix::fs::symlink(i.path, target).expect("Unable to create symlink"); + } +} + +#[derive(Parser)] +#[command(version, about, long_about=None)] +struct Cli { + #[arg(short,long)] + src: String, + #[arg(short,long)] + dst: String, +} + +#[derive(Debug)] +struct EpisodeID { + path: PathBuf, + filename: String, + season: u32, + episode: u32, + filetype: String, +} \ No newline at end of file