use backend::{self, Arguments, ScanDir, Season, tokenizer, DRY_RUN};
use gdk::enums::key;
use gtk::prelude::*;
use gtk::{
self, Builder, Button, Entry, FileChooserDialog, ListStore,
SpinButton, TreeView, TreeViewColumn, Type, Window, WindowType
};
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
use tvdb;
#[inline]
fn parse_directory(directory: &str) -> String {
let mut output = String::from(directory);
if output.starts_with("file://") {
output = output.replace("file://", "");
output = output.replace("%20", " ");
output = output.replace("\n", "");
output = output.replace("\r", "");
}
output
}
pub fn interface() {
gtk::init().unwrap_or_else(|_| panic!("tv-renamer: failed to initialize GTK."));
let builder = Builder::new_from_string(include_str!("gtk_interface.glade"));
let window: Window = builder.get_object("main_window").unwrap();
let preview_button: Button = builder.get_object("preview_button").unwrap();
let rename_button: Button = builder.get_object("rename_button").unwrap();
let series_name_entry: Entry = builder.get_object("series_name_entry").unwrap();
let series_directory_entry: Entry = builder.get_object("series_directory_entry").unwrap();
let template_entry: Entry = builder.get_object("template_entry").unwrap();
let series_directory_button: Button = builder.get_object("series_directory_button").unwrap();
let episode_spin_button: SpinButton = builder.get_object("episode_spin_button").unwrap();
let season_spin_button: SpinButton = builder.get_object("season_spin_button").unwrap();
let preview_tree: TreeView = builder.get_object("preview_tree").unwrap();
let info_bar: gtk::InfoBar = builder.get_object("info_bar").unwrap();
let info_button: Button = builder.get_object("info_close").unwrap();
let notification_label: gtk::Label = builder.get_object("notification_label").unwrap();
let preview_list = ListStore::new(&[Type::String, Type::String]);
macro_rules! add_column {
($preview_tree:ident, $title:expr, $id:expr) => {{
let column = TreeViewColumn::new();
let renderer = gtk::CellRendererText::new();
column.set_title($title);
column.set_resizable(true);
column.pack_start(&renderer, true);
column.add_attribute(&renderer, "text", $id);
preview_tree.append_column(&column);
}}
}
add_column!(preview_tree, "Before", 0);
add_column!(preview_tree, "After", 1);
preview_tree.set_model(Some(&preview_list));
preview_tree.set_headers_visible(true);
macro_rules! rename_action {
($widget:ident, $dry_run:ident, $dialog:ident) => {{
let $widget = $widget.clone();
let season_spin_button = season_spin_button.clone();
let episode_spin_button = episode_spin_button.clone();
let series_entry = series_name_entry.clone();
let directory_entry = series_directory_entry.clone();
let preview_list = preview_list.clone();
let info_bar = info_bar.clone();
let notification_label = notification_label.clone();
let template_entry = template_entry.clone();
$widget.connect_clicked(move |_| {
if $dialog {
let dialog = FileChooserDialog::new (
Some("Choose Directory"),
Some(&Window::new(WindowType::Popup)),
gtk::FileChooserAction::SelectFolder,
);
dialog.add_button("Cancel", gtk::ResponseType::Cancel.into());
dialog.add_button("Select", gtk::ResponseType::Ok.into());
if dialog.run() == gtk::ResponseType::Ok.into() {
dialog.get_filename().map(|path| path.to_str().map(|text| directory_entry.set_text(text)));
}
dialog.destroy();
}
if let Some(directory) = directory_entry.get_text() {
let mut program = &mut Arguments {
flags: if $dry_run { DRY_RUN } else { 0 },
base_directory: parse_directory(&directory),
series_name: series_entry.get_text().unwrap_or_default(),
season_index: season_spin_button.get_value_as_int() as u8,
episode_index: episode_spin_button.get_value_as_int() as u16,
pad_length: 2,
template: tokenizer::tokenize_template(template_entry.get_text().unwrap().as_str())
};
if program.series_name.is_empty() {
program.series_name = String::from(Path::new(&program.base_directory)
.file_name().unwrap().to_str().unwrap());
series_entry.set_text(program.series_name.as_str());
}
if !program.base_directory.is_empty() {
rename_series(&program, &preview_list, &info_bar, ¬ification_label);
}
}
});
}}
}
rename_action!(preview_button, true, false);
rename_action!(series_directory_button, true, true);
rename_action!(rename_button, false, false);
{ let info_bar = info_bar.clone();
info_button.connect_clicked(move |_| {
info_bar.hide();
});
}
window.set_wmclass ("tv-renamer", "Tv-renamer");
window.show_all();
info_bar.hide();
window.connect_delete_event(|_, _| {
gtk::main_quit();
Inhibit(false)
});
window.connect_key_press_event(move |_, key| {
if let key::Escape = key.get_keyval() { gtk::main_quit() }
gtk::Inhibit(false)
});
gtk::main();
}
fn rename_series(args: &Arguments, preview_list: &ListStore, info_bar: >k::InfoBar, notification_label: >k::Label) {
preview_list.clear();
match backend::scan_directory(&args.base_directory, args.season_index) {
Ok(ScanDir::Episodes(season)) => {
match rename_season(&season, args.episode_index, args, preview_list) {
Ok(_) => {
if args.flags & DRY_RUN != 0 { return }
info_bar.set_message_type(gtk::MessageType::Info);
notification_label.set_text("Rename Success");
},
Err(why) => match_rename_error(info_bar, notification_label, why, args)
}
},
Ok(ScanDir::Seasons(seasons)) => {
for season in seasons {
match rename_season(&season, 1, args, preview_list) {
Ok(_) => {
if args.flags & DRY_RUN != 0 { return }
info_bar.set_message_type(gtk::MessageType::Info);
notification_label.set_text("Rename Success");
},
Err(why) => {
match_rename_error(info_bar, notification_label, why, args);
break
}
}
}
},
Err(why) => {
info_bar.set_message_type(gtk::MessageType::Error);
notification_label.set_text(why.description());
}
}
info_bar.show();
}
fn match_rename_error(info_bar: >k::InfoBar, notification_label: >k::Label, why: RenameErr, args: &Arguments) {
info_bar.set_message_type(gtk::MessageType::Error);
let message = match why {
RenameErr::RenameFailed(source, target) => format!("Could not rename {:?} to {:?}", source, target),
RenameErr::TargetExists(path) => format!("{:?} already exists", path),
RenameErr::EpisodeDoesNotExist(episode) => format!("Episode {} could not be found on TheTVDB", episode),
RenameErr::SeriesLookupFailed => format!("{} could not be found on TheTVDB", &args.series_name)
};
notification_label.set_text(message.as_str());
}
enum RenameErr {
TargetExists(PathBuf),
RenameFailed(PathBuf, PathBuf),
EpisodeDoesNotExist(u16),
SeriesLookupFailed
}
fn rename_season(season: &Season, episode_no: u16, arguments: &Arguments, preview_list: &ListStore)
-> Result<(), RenameErr>
{
let mut episode_no = episode_no;
let api = tvdb::Tvdb::new("0629B785CE550C8D");
let series_id = api.search(&arguments.series_name, "en").map_err(|_| RenameErr::SeriesLookupFailed)?[0].seriesid;
for source in &season.episodes {
let target = backend::collect_target(source, season.season_no, episode_no, arguments, &api, series_id)
.map_err(|_| RenameErr::EpisodeDoesNotExist(episode_no))?;
if target.exists() { return Err(RenameErr::TargetExists(source.clone())); }
update_preview(preview_list, source, &target);
if arguments.flags & DRY_RUN == 0 {
fs::rename(&source, &target).map_err(|_| RenameErr::RenameFailed(source.clone(), target))?;
}
episode_no += 1;
}
Ok(()) }
#[inline]
fn update_preview(preview_list: &ListStore, source: &Path, target: &Path) {
let src = source.components().last().unwrap().as_os_str().to_str().unwrap().to_string();
let trg = target.components().last().unwrap().as_os_str().to_str().unwrap().to_string();
preview_list.insert_with_values(None, &[0, 1], &[&src, &trg]);
}