tagdex 1.0.0

An mp3 tag indexer written in Rust.
Documentation
use crate::song::Song;
use itertools::Itertools;
use skim::{
  prelude::{SkimItemReader, SkimOptionsBuilder},
  Skim,
};
use std::io::Cursor;

// Get interactive user input as list of attributes.
fn get_choices(prompt: String, items: Vec<&String>) -> Vec<String> {
  let options = SkimOptionsBuilder::default()
    .height(String::from("100%"))
    .multi(true)
    .prompt(prompt)
    .build()
    .unwrap();

  let items = SkimItemReader::default().of_bufread(Cursor::new(items.iter().join("\n")));

  let result = Skim::run_with(&options, Some(items)).expect("No skim result.");

  match result.final_event {
    skim::prelude::Event::EvActAccept(_) => result
      .selected_items
      .iter()
      .map(|item| item.output().to_string())
      .collect(),
    _ => Vec::new(),
  }
}

// Filter by artist; this needs a special handler as checks require set intersection.
fn filter_artists(songs: &Vec<Song>) -> Vec<&str> {
  let artists: Vec<&String> = songs
    .iter()
    .map(|song| &song.artists)
    .flatten()
    .unique()
    .collect();

  let artists = get_choices(String::from("artist(s): "), artists);

  songs
    .iter()
    .filter(|song| song.artists.iter().any(|artist| artists.contains(artist)))
    .filter_map(|song| song.path.to_str())
    .collect()
}

// Filter by a string attribute retreived by the extractor.
fn filter_single<F>(songs: &Vec<Song>, extractor: F, prompt: String) -> Vec<&str>
where
  F: Fn(&Song) -> &String,
{
  let attrs: Vec<&String> = songs.iter().map(|song| extractor(song)).unique().collect();

  let attrs = get_choices(prompt, attrs);

  songs
    .iter()
    .filter(|song| attrs.contains(extractor(song)))
    .filter_map(|song| song.path.to_str())
    .collect()
}

// Filter by a predefined attribute.
pub fn filter<'a>(songs: &'a Vec<Song>, attr: &'a str) -> Option<Vec<&'a str>> {
  match attr {
    "title" => Some(filter_single(
      &songs,
      |song| &song.title,
      String::from("title: "),
    )),
    "album" => Some(filter_single(
      &songs,
      |song| &song.album,
      String::from("album: "),
    )),
    "artist" => Some(filter_artists(&songs)),
    "genre" => Some(filter_single(
      &songs,
      |song| &song.genre,
      String::from("genre: "),
    )),
    "year" => Some(filter_single(
      &songs,
      |song| &song.year,
      String::from("year: "),
    )),
    _ => None,
  }
}