#!/usr/bin/env perl
#########################################################################
# acxi - audio conversion program
#########################################################################

# Copyright (c) 2010-2024 - Harald Hope - smxi.org 
# Home page: https://codeberg.org/smxi/acxi
# Forum support: https://techpatterns.com/forums/about1491.html
# Download url: https://smxi.org/acxi
#
# Based on flac2ogg.pl
# Copyright (c) 2004 - Jason L. Buberel - jason@buberel.org
# Copyright (c) 2007 - Evan Boggs - etboggs@indiana.edu
# Previous Home page (gone now): 
#   http://www.buberel.org/linux/batch-flac-to-ogg-converter.php
#
# Modified: 2018-12-05 - Cleaned up code, refactored
# Modified: 2011-07-26 - Harald Hope - Added patch for $ in file names; 
#   changed verbosity levels to fit future 3 release, got rid of 
#   $B_SILENT and $b_quiet
# Modified: 2011-03-23 - Odd Eivind Ebbesen - www.oddware.net - 
#   <oddebb at gmail dot com>
#   Added functionality for Flac conversion to MP3, preserving tags.
#########################################################################
# This program is free software; you can redistribute it and/or modify 
# it under the terms of the GNU General Public License as published by 
# the Free Software Foundation; either version 3 of the License, or 
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but 
# WITHOUT ANY WARRANTY; without even the implied warranty of 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
# General Public License for more details.
#
# Get the full text of the GPL here: http://www.gnu.org/licenses/gpl.txt
#########################################################################
# Given a source directory tree of original lossless music files 
# (flac, wav, etc), this program will recreate (or add to) a new 
# directory tree of ogg/mp3 files by recursively encoding only new 
# source files to destination types.
# The source and destination directories can be hard-coded using the 
# $SOURCE_DIRECTORY and $DESTINATION_DIRECTORY variables, or passed on 
# the command line, or can be set in the configuration file (recommended).  
#
# See USER MODIFIABLE VALUES for configuration information.

#########################################################################
### NO USER CHANGES IN THIS SECTION ###
#########################################################################
use strict;
use warnings;
# use diagnostics;
use 5.010;
use feature 'state';
use Getopt::Long qw(GetOptions);
Getopt::Long::Configure ('bundling', 'no_ignore_case', 
'no_getopt_compat', 'no_auto_abbrev','pass_through');
use File::Basename;
use File::stat;
use File::Find;
use File::Copy qw(copy);
use File::Glob qw(:bsd_glob);
use File::Path qw(rmtree make_path);
use Cwd qw(getcwd);
use POSIX qw(strftime);
use Data::Dumper;
$Data::Dumper::Sortkeys = 1;
# use File::Copy::Recursive qw(fcopy rcopy dircopy fmove rmove dirmove);
# use feature 'unicode_strings';
# use open qw(:std :utf8); # don't use
# only use for debugging, some distros do not ship with core modules.
# use Data::Dumper qw(Dumper);
## if can't find any other way to get rid of lost+found errors, enable this:
# no warnings 'File::Find';

#### -------------------------------------------------------------------
#### PROGRAM GLOBALS - DO NOT TOUCH THESE!
#### -------------------------------------------------------------------

## SELF INFO ##
my $self_name='acxi';
my $self_version='3.6.02';
my $self_patch='0';
my $self_date='2024-04-08';

## GLOBALS ##
## Booleans
my ($b_dest_changed,$b_force,$b_fork,$b_quiet,$b_test);
my ($b_check_dest,$b_check_out) = (1,1);
my $b_win = ($^O =~ /win/i) ? 1 : 0; # detect if $^O returns windows 

## Undefined Scalars
my ($print_line_heavy,$print_line_large,$print_line_small);
my ($codec,$extension,$exclude_append,$image_embed,$list_type,$start_dir);

## Defined Scalars
my ($quality,$autotag_multi,$silent_flac,$silent_ffmpeg,$silent_lame,
$silent_opus) = (7,'','','','','');
# default: recurse infinite for File::Find; start tag list track numbers at 1
my ($padding,$recurse,$start) = ('',-1,1);

## Data Storage Arrays
my (@excludes,@excludes_stripped,@extension_list,@found_list,@source_glob,@tags);

## Execution/Debug Switches
my (@dbg,%run);

## CONSTANTS ##
my $path_separator = ($b_win) ? '\\' : '/';
my $line_heavy = '===========================================================================';
my $line_result = ':::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::';
my $line_small = '-----------------------------------------------------------------';
my $line_large = '---------------------------------------------------------------------------';

#########################################################################
### USER MODIFIABLE VALUES ###
#########################################################################
# User config file at:
# Global: /etc/acxi.conf
# User override files checked in the following order (first found used):
# $XDG_CONFIG_HOME/acxi.conf, $HOME/.config/acxi.conf, $HOME/.acxi.conf
#
# Set values like this:
# Do not use the $ preceding the variable name, or the semicolon or 
# single/double quote marks in the config file. Use this syntax for 
# config files:
# SOURCE_DIRECTORY=/home/fred/music/flac
# DESTINATION_DIRECTORY=/home/fred/music/ogg
#
# It's highly recommended to create a config file so you don't have to
# update the values below every time acxi updates.
#
# Anything in configs or in this section will be overridden if you use
# a startup argument which replaces that item's value.

#### -------------------------------------------------------------------
#### CUSTOM CONFIGURATION FILE LOCATION
#### -------------------------------------------------------------------

# NOTE: only use this if you are running Windows, or any OS without $HOME
# or $XDG_CONFIG_HOME environmental variables. acxi will look for file:
# acxi.conf inside that directory. 
my $CONFIG_DIRECTORY='';

#### -------------------------------------------------------------------
#### APPLICATION PATHS
#### -------------------------------------------------------------------

my $COMMAND_FLAC = '/usr/bin/flac'; 
my $COMMAND_FFMPEG = '/usr/bin/ffmpeg';
my $COMMAND_FFPROBE = '/usr/bin/ffprobe';
my $COMMAND_LAME = '/usr/bin/lame';
my $COMMAND_OGG = '/usr/bin/oggenc';
my $COMMAND_OPUS = '/usr/bin/opusenc';
# If you are not generating checksums, you do not need this.
my $COMMAND_MD5 = '/usr/bin/md5sum';
# metaflac is required for flac to mp3, to copy over the ID3 tags, or
# for ffp checksum generator.
# If you are not doing either, you don't need this.
my $COMMAND_METAFLAC = '/usr/bin/metaflac';

#### -------------------------------------------------------------------
#### ASSIGN DIRECTORY PATHS
#### -------------------------------------------------------------------

# Options: -s/--source path -d/--destination path
# $SOURCE_DIRECTORY is the original, working, like flac, wav, etc
# $DESTINATION_DIRECTORY is the processed, ie, ogg, mp3
# CHANGE TO FIT YOUR SYSTEM - do not end in /
# IMPORTANT: DESTINATION_DIRECTORY cannot be equal to SOURCE_DIRECTORY
my $SOURCE_DIRECTORY = '/path/to/source/directory';
my $DESTINATION_DIRECTORY = '/path/to/your/output/directory';

#### -------------------------------------------------------------------
#### EXCLUDES
#### -------------------------------------------------------------------

# this is the unique part of the exclude file name, not including the 
# .txt. All exclude file names used must contain this string value.
my $EXCLUDE_BASE = 'acxi-exclude';

# Option: --exclude 
# Takes a ^^ separated list of key words which will match terms in 
# your source directory you do not want synced or copied over to 
# destination. Separate items with ^^. Will match the entire path 
# so be aware. Note you can exclude directory names like artwork too.
# Sample: $EXCLUDE = 'millivanilli^^bonjovi^^artwork';
# Can also use a file with one exclude item per line. File name 
# must include the 'acxi-excludes' in it or it won't be used.
# you can use various excluldes to create various collections.
# Sample: $EXCLUDE = '/home/me/music/excludes/acxi-excludes-phone.txt';
my $EXCLUDE = '';

#### -------------------------------------------------------------------
#### INPUT/OUTPUT
#### -------------------------------------------------------------------

# Options: -i/--input type ; -o/--output type
# The following are NOT case sensitive,ie flac/FLAC, txt/TXT will be 
# found. INPUT_TYPE and OUTPUT_TYPE will be forced to lower case 
# internally. Output change from ogg to '' 3.6.00. Xiph.org recommends opus.
my $INPUT_TYPE = 'flac';
# opus, ogg, mp3, m4a, etc
my $OUTPUT_TYPE = '';

# Option: -q/--quality number
# for flac: n can be 0-8. 0 produces largest file, is fastest, anything over 4
# is probably pointless since compression gain is very little vs time required.
# For mp3: n can be 0-9 (variable bit rate), 0 is largest file / highest quality
# For ogg: n can be between -1 and 10. Fractions allowed. 10 is the largest file 
# size / highest quality. 
# For opus: n can be 6-256. 256 is the largest file size / highest quality / 
# maximum bitrate. For flac 0-8 [0 biggest size, fastest], set to 4 because
# there is almost no size decrease with flac 5-8 but big cpu time increase.
my $QUALITY_AAC = 160;
my $QUALITY_FLAC = 4;
my $QUALITY_MP3 = 3;
my $QUALITY_OGG = 7;
my $QUALITY_OPUS = 144; # 128 claimed to be stereo transparent, 160 very good

# Frauenhofer codec considered best, but ffmeg native 'aac' will usually be 
# there.
my $CODEC_AAC = 'libfdk_aac';

# Option: -c/--copy and -a/--append (to append extension types to existing list.
## NOTE: if you want to override $COPY_TYPES in your config files, you
# must use this syntax:
# COPY_TYPES=doc,docx,bmp,png,doc,docx,jpg,jpeg,tif
# Add or remove types to copy over to ogg directories, do not include
# the input/output audio file types, only extra data types like txt.
# If you want no copying done, simply change to: $COPY_TYPES = 'none';
my $COPY_TYPES = 'gif,jpg,jpeg,png,txt';

# Option: --dither - changes default dither type for > 16 bit to 16 bit 
# resamplings. Only change if you know why. 
# https://ffmpeg.org/ffmpeg-resampler.html
# Values: rectangular; triangular; triangular_hp [with high pass];
# lipshitz [noise shaping]; shibata [noise shaping]; 
# low_shibata [low noise shaping]; high_shibata [high noise shaping];
# f_weighted [noise shaping]; modified_e_weighted [noise shaping];
# improved_e_weighted [noise shaping];
my $DITHER = 'shibata';

#### -------------------------------------------------------------------
#### ANALYZE
#### -------------------------------------------------------------------

# These are used for --analyze/-Z possible file error detection.
# If file values are less than this, prints alert message. To disable, simply
# set both to 0.
my $Z_MIN_SIZE = 1000; # lossless only, in KiB
my $Z_MIN_TIME = 10; # in seconds

#### -------------------------------------------------------------------
#### AUTO TAGGING
#### -------------------------------------------------------------------

# This is the file that contains the syntax found in auto.tag file. But
# you can call it something else if you prefer.
my $AUTOTAG_FILE = 'auto.tag';
# For -S/-M/--prefill/--infofix can be overridden with config or --info-file []
my $INFO_FILE = 'info.txt';
# For -Xf, the rating shown in Quality: /[rating] item.
my $INFO_RATING = 4;
# Uses --tag syntax: TAG%:value^^TAG%:value, remember to clear when done!
my $PREFILL_TAG = '';
# This is for --taglist, writes to this file per directory
my $TAGLIST_FILE = 'taglist.tag';

#### -------------------------------------------------------------------
#### CHECKSUM DATA
#### -------------------------------------------------------------------

# These are the file names MINUS the .ffp / .md5 extensions.
my $FFP_FILE = 'fingerprints';
my $MD5_FILE = 'md5sums';

#### -------------------------------------------------------------------
#### VERBOSITY LEVELS
#### -------------------------------------------------------------------

# Options: --quiet/--debug/-v, --verbosity 0-4
# You can turn these to always on either here or in config file by setting to 
# desired verbosity level here directly, or in config file. 
# 0 = [quiet/silent] - no output at all. File issue if something shows output.
# 1 = default - single line per operation. This is the default, so you don't
#     need to change it.
# 2 = more verbose, but without the actual conversion data from codecs
# 3 = adds codec conversion information.
# 4 = [debug] - adds specific debugger information. Certain debugger output may
# not work in Windows, but you rarely if ever need to see this level.
# NOTE: with FORK > 1, conversion debugging output can be out of order.
my $VERBOSITY = 1;

#### -------------------------------------------------------------------
#### ADVANCED
#### -------------------------------------------------------------------

# Options: --nlink --no-nlink
# Only change to 0 if you encounter file tree failures. This is for File::Find
# values:  0 - use nlink; 1 - don't use nlink [default, only change if you know
# why.
my $DONT_USE_NLINK = 1;

# Option: -F/--fork 0-xx
## number of forks/threads to use. 0 is default, and will not use forking
# note that debugging output gets strange with forking, so debug with fork = 0
# FORK = 1 results in slower times than using no forking so avoid that. 
my $FORK = 0;

#### -------------------------------------------------------------------
#### SELF UPDATER
#### -------------------------------------------------------------------

# Only for maintainers, set to 0 in config file to disable.
my $ALLOW_UPDATES = 1;

# this will be used to update the program and man page. You must have 
# write permissions to the file locations. Linux or BSD only.
# do not end paths with /
my $COMMAND_CURL = '/usr/bin/curl';
my $MAN_DIRECTORY = '/usr/local/share/man/man1';
my $SELF_DIRECTORY = '/usr/local/bin';

#########################################################################
### END USER MODIFIABLE VALUES ###
#########################################################################

########################################################################
#### STARTUP
########################################################################

sub main {
	UserConfigs::set();
	OptionsHandler::get();
	set_display_data();
	SelfUpdater::update() if $run{'update'};
	Validation::run();
	set_basic_data();
	Aggregate::run() if $run{'aggregate'};
	AutoTag::create() if $run{'autotag-create'};
	AutoTag::run_tagger() if $run{'tagger'};
	Checksums::process() if ($run{'checksum'} || $run{'checksum-verify'});
	CleanCollection::process() if $run{'clean'};
	InfoFix::process() if $run{'infofix'};
	TagList::process() if $run{'taglist'};
	SyncCollection::process() if $run{'sync'};
	print_completion_message();
}

#########################################################################
### CLEAN/SYNC MUSIC DIRECTORIES ###-
#########################################################################

#### -------------------------------------------------------------------
#### AGGREGATE
#### -------------------------------------------------------------------

## Aggregate
{
package Aggregate;

sub run {
	say $line_large;
	say 'Starting file aggregation from: ' . main::sourcer($SOURCE_DIRECTORY);
	say ' to: ' . main::sourcer($DESTINATION_DIRECTORY);
	my @files = split /,/, $run{'ag-file'};
	# my $abs_path = Cwd::abs_path($SOURCE_DIRECTORY);
	# chdir Cwd::getcwd(); # $SOURCE_DIRECTORY;
	chdir $start_dir;
	foreach (@files){
		aggregate($_);
	}
	say $line_small;
	say 'Completed file aggregation.';
	say $line_large;
	exit;
}

sub aggregate {
	($extension) = @_;
	my ($path,@files);
	my ($dir,$file,$file_working,$name);
	$list_type = 'file';
	@found_list = ();
	File::Find::find(\&main::wanted, @source_glob);
	say $line_small;
	say "Aggregating file type $extension...";
	say $line_small;
	foreach $file (sort { "\L$a" cmp "\L$b" } @found_list){
		$b_dest_changed = 1;
		$file_working = $file;
		$file_working =~ s|^\Q$SOURCE_DIRECTORY$path_separator\E||;
		$dir = File::Basename::dirname($file_working);
		$name = File::Basename::basename($file_working);
		if ($run{'aggregate-file'}){
			$path = $DESTINATION_DIRECTORY . $path_separator . $dir . '.' . $name;
		}
		else {
			$path = $DESTINATION_DIRECTORY . $path_separator . $dir;
			if (! -d $path){
				File::Path::make_path($path) or die qq("Arg... can't make: $path\n") if !$b_test;
			}
			$path .= $path_separator . $name;
			say 'path: ', $path if $dbg[4];
		}
		say $file_working;
		say $file if $dbg[4];
		File::Copy::copy($file, $path) if !$b_test;
		push @files, $file;
		$path = '';
	}
	if (!@found_list){
		say "No $extension type files found.";
	}
	say $line_small;
	say Data::Dumper::Dumper \@files if $dbg[6];
	say "Finished copying over $extension type files to " . main::sourcer("$DESTINATION_DIRECTORY");
}
}

#### -------------------------------------------------------------------
#### ANALYZE
#### -------------------------------------------------------------------

## Analyze
{
package Analyze;
my ($avg_kbs,$total_size,$total_size_raw,$total_time) = (0,0,0,0);
my ($source_info) = ('');
my ($type,$ref,%alerts,@files,@files_info,%results);

sub run {
	($type,$ref) = @_;
	my $dir = Cwd::getcwd; # resolves sym links
	# the caller has looped into directory with info file and input files
	if ($type eq 'info'){
		@files = main::globber('*.' . $INPUT_TYPE);
	}
	# the caller is generating the flac list and passing it in here
	else {
		@files = @$ref;
	}
	#	print Data::Dumper::Dumper \@files;
	if (!@files){
		main::error_handler('analyze',"No $INPUT_TYPE files found for quality test!\n",1);
	}
	else {
		undef(%alerts);
		undef(%results);
		undef(@files_info);
		($avg_kbs,$total_size,$total_size_raw,$total_time) = (0,0,0,0);
		($source_info) = ('');
		process();
		if ($type eq 'info'){
			info_quality() if $source_info; # updates INFO_FILE page contents array ref
		}
		else {
			generate_output(); # print output to screen
		}
	}
}

sub process {
	# If we hit error condition, we want to exit for -Xq, but not for -Z
	my $b_exit = ($type eq 'info') ? 1: 0;
	foreach my $source_file (@files){
		say "Getting metaflac for file: $source_file" if $dbg[4];
		my $info;
		my $escaped = main::escape_item("$source_file");
		# metaflac is MUCH faster than ffprobe. at least 25-30x faster!!
		if (lc($INPUT_TYPE eq 'flac' && !$run{'ffprobe'})){
			$info = process_metaflac($source_file,$escaped,$b_exit);
		}
		else {
			$info = process_ffprobe($source_file,$escaped,$b_exit);
		}
		# We WANT to see 0 data results.
		# if ($info->{'size'} && $info->{'duration'}){
		if ($type eq 'data' || !$source_info){
			my $source_data = $info->{'bps'} . '/' . sprintf("%.1f", $info->{'sample-rate'}/1000);
			$source_info = uc($INPUT_TYPE) . ": $source_data";
			$source_info .= " ($info->{'channels'} channels)";
			if ($type eq 'data'){
				$source_data .= '/' . $info->{'channels'} . 'ch';
				if (!grep {$_ eq $source_data} @files_info){
					push(@files_info,$source_data);
				}
			}
		}
		#}
		if ($type eq 'data'){
			my ($raw_time,$raw_size) = ($info->{'duration'},$info->{'size'});
			my $time_print = 'time: ' . main::print_time($info->{'duration'});
			my $size_print = 'size: '. main::print_size($info->{'size'}/1024);
			my $kbs_print = 'kbs: '. $info->{'kbs'};
			if ($VERBOSITY > 2){
				$time_print .= " ($raw_time seconds)"; 
				$size_print .= " ($raw_size B)";
			}
			$results{$source_file} = [$source_info,$time_print,$size_print,$kbs_print];
			if ($Z_MIN_TIME && $raw_time < $Z_MIN_TIME){
				my $msg = "track time (". sprintf("%0.2f",$raw_time);
				$msg .= "s) less than ${Z_MIN_TIME}s";
				push(@{$alerts{$source_file}},$msg); 
			}
			if ($Z_MIN_SIZE && ($raw_size/1024) < $Z_MIN_SIZE && 
			$INPUT_TYPE !~ /^(aac|m4a|mp3|ogg|opus)$/){
				my $msg = "track size (" . main::print_size($raw_size/1024);
				$msg .= ") less than $Z_MIN_SIZE KiB";
				push(@{$alerts{$source_file}},$msg); 
			}
			# only metaflac stores the hash
			if ($VERBOSITY > 1 && lc($INPUT_TYPE) eq 'flac'){
				$info->{'md5sum'} ||= 'N/A';
				my $ffp_print = 'ffp: ' . $info->{'md5sum'};
				splice(@{$results{$source_file}},1,0,$ffp_print);
			}
		}
	}
	say "Result: $source_info" if $dbg[6];
	$avg_kbs = sprintf("%.0f",$total_size*8/$total_time/1000);
	# back to KiB
	$total_size_raw = $total_size;
	$total_size = $total_size/1024;
}

sub process_ffprobe {
	my ($source_file,$escaped,$b_exit) = @_;
	my (%outcome,@source);
	my ($size,$duration,$channels,$sample_rate,$bps,$kbs,$md5sum) = (0,0,0,0,0,0,'');
	@source = qx($COMMAND_FFPROBE -v quiet -print_format flat -show_format -show_streams "$escaped");
	if (scalar @source < 20){
		main::error_handler('analyze',
		"ffprobe failed to get valid results from file:\n '$source_file'",$b_exit);
	}
	else {
		foreach (@source){
			$_ =~ s/[\r\n]|"//g;
			my @split = split(/\s*=\s*/,$_);
			$outcome{$split[0]} = (!defined $split[1] || $split[1] eq 'N/A') ? undef : $split[1];
		}
		say 'ffprobe @source: ', Data::Dumper::Dumper \@source if $dbg[1];
		if (!$outcome{'format.size'}){
			main::error_handler('analyze',
			"ffprobe: source size 0:\n '$source_file'",$b_exit) if $b_exit;
		}
		elsif (!$outcome{'format.duration'}){
			main::error_handler('analyze',
			"ffprobe: source duration 0:\n '$source_file'",$b_exit) if $b_exit;
		}
		if (defined $outcome{'format.size'}){
			$size = $outcome{'format.size'};
			$total_size += $size;
		}
		if (defined $outcome{'format.duration'}){
			$duration = $outcome{'format.duration'};
			$total_time += $duration;
		}
		if (defined $outcome{'streams.stream.0.sample_fmt'}){
			$outcome{'streams.stream.0.sample_fmt'} =~ s/^s//;
			$bps = $outcome{'streams.stream.0.sample_fmt'};
		}
		if (defined $outcome{'streams.stream.0.sample_rate'}){
			$sample_rate = $outcome{'streams.stream.0.sample_rate'};
		}
		if (defined $outcome{'streams.stream.0.channels'}){
			$channels = $outcome{'streams.stream.0.channels'};
		}
		if (defined $outcome{'format.bit_rate'}){
			$kbs = $outcome{'format.bit_rate'};
			$kbs = sprintf("%.0f",$kbs/1000);
		}
	}
	return {
	'size' => $size,
	'duration' => $duration,
	'channels' => $channels,
	'sample-rate' => $sample_rate,
	'bps' => $bps,
	'kbs' => $kbs,
	'md5sum' => $md5sum
	};
}

sub process_metaflac {
	my ($source_file,$escaped,$b_exit) = @_;
	my (@real);
	my ($size,$duration,$kbs) = (0,0,0);
	my @source = qx($COMMAND_METAFLAC --show-channels --show-sample-rate --show-bps --show-total-samples --show-md5sum "$escaped");
	if (scalar @source != 5){
		main::error_handler('analyze',
		"metaflac: failed to get 5 items from file:\n '$source_file'",$b_exit);
	}
	else {
		foreach (@source){
			$_ =~ s/[\r\n]//g;
			push(@real,$_);
		}
		say $escaped . ' :: ', Data::Dumper::Dumper \@source if $dbg[1];
		# the file will have a size even if it's not valid
		# stat returns size in bytes. 
		my @stat = stat($source_file);
		# If has embedded image, will change values slightly, that's life.
		$size = $stat[7];
		$total_size += $size;
		# Don't show errors here for -Z, only -Xq
		if (!$real[1]){
			main::error_handler('analyze',
			"metaflac: source sample rate 0:\n '$source_file'",$b_exit) if $b_exit;
		}
		elsif (!$real[3]){
			main::error_handler('analyze',
			"metaflac: source total samples count 0:\n '$source_file'",$b_exit) if $b_exit;
		}
		else {
			$duration = ($real[3]/$real[1]);
			$total_time += $duration;
			# in kilobits per second, not bytes
			$kbs = $size/$duration/1000*8;
			$kbs = sprintf("%.0f",$kbs);
		}
	}
	# say "$stat[7] $stat[11] $stat[12]";
	# say "s: $size kbs: $kbs";
	# say "duration: $duration seconds";
	return {
	'size' => $size,
	'duration' => $duration,
	'channels' => $real[0],
	'sample-rate' => $real[1],
	'bps' => $real[2],
	'kbs' => $kbs,
	'md5sum' => $real[4]
	};
}

# this is inserted into $INFO_FILE content array reference
# Q: is it better to just pop this at end of info file?
sub info_quality {
	my $b_head = 1;
	my (@data,$verify);
	$verify = Checksums::verify_flacs() if $run{'infofix-verify'};
	for (my $i=0;$i < scalar @$ref; $i++){
		$b_head = 0 if $b_head && $i > 1 && $ref->[$i] =~ /^\s*$/;
		# put the block under the top band info header block
		if (!$b_head){
			push(@data,
			'',
			$source_info,
			"Quality: /$INFO_RATING ()",
			'Time: ' . main::print_time($total_time),
			'Size: ' . main::print_size($total_size),
			'Average kb/s: ' . $avg_kbs,
			'Tracks: ' . scalar @files,
			);
			if ($run{'infofix-verify'} && $verify){
				if ($VERBOSITY > 1 || $verify->[1] || $verify->[2]){
					push(@data,'FLAC FFPs: ', 
					split(/\n/,$verify->[0]),
					split(/\n/,$verify->[1]),
					split(/\n/,$verify->[2])
					);
				}
				else {
					push(@data,'FLAC FFPs: ' . $verify->[0]);
				}
			}
			push(@data,
			'',
			);
			splice(@$ref,$i,0,@data);
			last;
		}
	}
}

sub generate_output {
	my $time = main::print_time($total_time);
	my $size = main::print_size($total_size);
	if ($VERBOSITY > 2){
		$time .= " ($total_time seconds)"; 
		$size .= " ($total_size_raw B)"; 
	}
	say '  Generating ' . lc($INPUT_TYPE) . ' data:';
	$time = '  Time: ' . $time;
	$size = '  Size: ' . $size;
	$avg_kbs = '  Average kb/s: ' . $avg_kbs;
	my $track_count = '  Tracks: ' . scalar @files;
	my $source_data = '  Info: ' . join(', ',sort @files_info);
	if ($VERBOSITY > 0){
		foreach my $key (sort keys %results){
			say "   File: $key:";
			if ($VERBOSITY > 1){
				say '    ', join("\n    ",@{$results{$key}});
			}
			else {
				say '    ', join(' ',@{$results{$key}});
			}
		}
	}
	if (%alerts){
		foreach my $key (sort keys %alerts){
			say "  ALERT: File: $key:";
			say '   ', join("\n   ",@{$alerts{$key}});
		}
	}
	say $source_data if defined $source_data;
	say $time if defined $time;
	say $size if defined $size;
	say $avg_kbs if defined $avg_kbs;
	say $track_count if defined $track_count;
}
}

#### -------------------------------------------------------------------
#### AUTO-TAGGING
#### -------------------------------------------------------------------

## AutoTag 
{
package AutoTag;

sub run_tagger {
	say $line_large;
	if ($run{'image-embed'}){
		say 'Starting image embedding in: ' . main::sourcer($SOURCE_DIRECTORY);
		image_embedder();
		say $line_small;
		say 'Completed image embedding.';
	}
	if ($run{'tag-update'}){
		say 'Starting tag update in: ' . main::sourcer($SOURCE_DIRECTORY);
		tag_updater();
		say $line_small;
		say 'Completed tag update.';
	}
	elsif ($run{'autotagger'}){
		say 'Starting auto-tagging in: ' . main::sourcer($SOURCE_DIRECTORY);
		get_tag_files();
		say $line_small;
		say 'Completed auto-tagging.';
	}
	say $line_large;
}

sub create {
	say $line_large;
	say "Creating $AUTOTAG_FILE in: " . main::sourcer("$SOURCE_DIRECTORY");
	make_tag_file();
	say $line_small;
	if (!$b_test){
		say 'Completed file creation.';
	}
	else {
		say 'End Test Output.';
	}
	say $line_large;
}

sub get_tag_files {
	my ($item,$print_file,$result,$working_dir);
	$list_type = 'file';
	$extension = $AUTOTAG_FILE;
	@found_list = ();
	File::Find::find(\&main::wanted, @source_glob);
	foreach $item (sort { "\L$a" cmp "\L$b" } @found_list){
		$print_file = $working_dir = $item;
		$print_file =~ s|^\Q$SOURCE_DIRECTORY$path_separator\E|| if $VERBOSITY < 3;
		# $working_dir =~ s/[^\/]+$//;
		$working_dir = File::Basename::dirname($item);
		chdir "$start_dir";
		chdir "$working_dir";
		if ($dbg[4]){
			say Cwd::getcwd();
			system 'pwd';
		}
		say "Processing: $print_file";
		process_tags();
		$b_dest_changed = 1;
	}
	if (!@found_list){
		say "No $AUTOTAG_FILE files found.";
	}
}

sub process_tags {
	my @tags = main::reader($AUTOTAG_FILE);
	say 'Raw tags: ', Data::Dumper::Dumper \@tags if $dbg[2];
	push(@tags,'--END--') if @tags;
	my ($args,$autotag_unique,$error_message,$holder);
	my (@image_args,@images,@main_tags,@temp,@track_tags,@unique);
	my ($b_image_check,$b_tagblock,$b_tracks);
	my ($i,$cmd,$comments,$tag) = (0,'','','');
	state %set;
	# handle corner case multi line tag content, and populate unique if found
	foreach my $line (@tags){
		next if $line =~ /^\s*#/;
		if ($line =~ /^\s*([^:%]+)\s*%:\s*(.*)?\s*$/ || $line eq '--END--'){
			if ($temp[$i]){
				# get rid of > 2 linebreaks in content 
				$temp[$i]->[1] =~ s/[\n]{3,}/\n\n/; 
				# get rid of any newlines or spaces at end of value
				$temp[$i]->[1] =~ s/[\s\r\n]+$//; 
				$i++;
			}
			last if $line eq '--END--';
			# Handle occurances of per file tags, plus --unique override
			if ($1 && $1 eq 'UNIQUE'){
				if (!$run{'autotag-unique'} && $2){
					$autotag_unique = $2;
				}
			}
			else {
				$temp[$i] = [$1,$2];
			}
		}
		else {
			$line =~ s/^\s+|\s+$//g;
			# this will handle freak blank first line issues
			$temp[$i]->[1] .= "\n" . $line if defined $temp[$i];
		}
	}
	$autotag_unique = $run{'autotag-unique'} if $run{'autotag-unique'};
	if ($autotag_unique){
		if ($autotag_unique eq 'TAGBLOCK'){
			$b_tagblock = 1;
		}
		else {
			@unique = split(/\s*,\s*/,uc($autotag_unique));
		}
	}
	@tags = @temp;
	my @tt = qw(ACCURATERIPDISCID ACCURATERIPRESULT ACOUSTID_ID 
	ACOUSTID_FINGERPRINT ISRC MUSICBRAINZ_RELEASETRACKID MUSICBRAINZ_RELEASETRACKID 
	MUSICBRAINZ_TRACKID MUSICBRAINZ_WORKID PART REPLAYGAIN_TRACK_GAIN 
	REPLAYGAIN_TRACK_PEAK SUBTITLE TITLE TITLESORT TRACKNUMBER VERSION WORK);
	push (@tt,@unique) if @unique;
	my $track_pattern = join('|',@tt);
	$track_pattern = qr/^($track_pattern)$/;
	say 'Processed raw tags: ', Data::Dumper::Dumper \@tags if $dbg[3];
	foreach my $working (@tags){
		say Data::Dumper::Dumper $working if $dbg[20];
		# note: TRACKNUMBER%:0 would trip this, so test explicitly
		next if !defined $working->[1] || $working->[1] eq '';
		say "@$working" if $dbg[20];
		# $working->[1] =~ s/"/\\\"/g;
		# $working->[1] =~ s/\$/\\\$/g;
		$working->[1] = main::escape_item("$working->[1]");
		if ($working->[0] =~ $track_pattern){
			$tag = qq(--set-tag="$working->[0]"="$working->[1]" );
			push(@track_tags,$tag);
		}
		elsif ($working->[0] ne 'FILE'){
			# delete all previous items for key, to avoid multi tagging
			if ($b_tracks && (!$set{$working->[0]} || $working->[1] eq 'UNSET')){
				if ($working->[0] eq 'IMAGE'){
					@images = (); # reset the images completely
				}
				else {
					@main_tags = grep {!/^--set-tag="$working->[0]"/} @main_tags;
				}
				next if $working->[1] eq 'UNSET';
			}
			# then set the key flag again
			$set{$working->[0]} = 1;
			if ($working->[0] eq 'IMAGE'){
				$tag = qq($working->[1]);
				push(@images,$tag);
			}
			else {
				$tag = qq(--set-tag="$working->[0]"="$working->[1]");
				push(@main_tags,$tag);
			}
		}
		elsif ($working->[0] eq 'FILE'){
			## NOTE: we have to unescape $ in paths for -e tests, but leave it escaped
			## for qq cmd strings. Why? Shell expands it, perl does not.
			my $temp = $working->[1];
			$temp = main::unescape_item("$temp");
			print " Tagging \"$temp\"...";
			main::error_handler('file-missing',"Missing file: $temp",1) if ! -e "$temp";
			# note: must be set here, not in qx to avoid quote errors
			# single quotes, in case contains $ symbol
			say File::stat::stat("$working->[1]")->size if $dbg[21];
			$cmd = qq($COMMAND_METAFLAC --remove-all-tags $padding "$working->[1]");
			say "\nremove tags: ", $cmd if $dbg[5];
			if (!$b_test){
				qx($cmd);
				if ($? > 0){
					$error_message = "$COMMAND_METAFLAC returned error: $?";
					main::error_handler('application-error', $error_message,1);
				}
			}
			if ($run{'image-remove'}){
				image_remover("$working->[1]");
			}
			say File::stat::stat("$working->[1]")->size if $dbg[21];
			# disable for now, the test for type isn't working, metaflac is making types
			# 3 regardless of actual type.
			@image_args = image_handler("$working->[1]",@images) if @images;
			@track_tags = (@main_tags,@image_args,@track_tags);
			$args = join ' ', @track_tags;
			@track_tags = ();
			@image_args = ();
			@main_tags = () if $b_tagblock;
			# reset all detected @main items
			foreach (keys %set){
				$set{$_} = 0;
			}
			$cmd = qq($COMMAND_METAFLAC $args "$working->[1]");
			say "\nadd tags cmd: ", $cmd if $dbg[5];
			$b_tracks = 1;
			if (!$b_test){
				qx($cmd);
				if ($? > 0){
					$error_message = "$COMMAND_METAFLAC returned error: $?";
					main::error_handler('application-error', $error_message,1);
				}
				else {
					say '  File tagged';
				}
			}
			else {
				say '  Test mode, no tag';
			}
			say File::stat::stat("$working->[1]")->size if $dbg[21];
		}
	}
}

sub tag_updater {
	my ($args,$cmd,$error_message,$file,$print_file,
	$result,%tag_data,@working,$working_dir);
	my ($print_padding,$print_tags,$remove,$tag) = ('','','','');
	$print_padding = ' and padding' if $padding;
	foreach (@tags){
		@working = split /%:/, $_;
		$tag .= qq(--set-tag="$working[0]"="$working[1]" ) if uc($working[1]) ne 'UNSET';
		$remove .= qq(--remove-tag="$working[0]" );
		$print_tags .= qq($working[0] )
	}
	$list_type = 'file';
	$extension = $INPUT_TYPE;
	@found_list = ();
	File::Find::find(\&main::wanted, @source_glob);
	chdir "$start_dir";
	chdir "$SOURCE_DIRECTORY";
	say "Updating tags:$print_tags... ";
	foreach $file (sort { "\L$a" cmp "\L$b" } @found_list){
		$print_file = $working_dir = $file;
		$print_file =~ s|^\Q$SOURCE_DIRECTORY$path_separator\E|| if $VERBOSITY < 3;
		print "Processing: $print_file...\n Tags$print_padding ";
		$file = main::escape_item("$file");
		$file =~ s|^\Q$SOURCE_DIRECTORY$path_separator\E||; 
		$cmd = qq($COMMAND_METAFLAC $remove "$file");
		say "tag updater remove: $SOURCE_DIRECTORY\n cmd: $cmd" if $dbg[5];
		if (!$b_test){
			qx($cmd);
			if ($? > 0){
				$error_message = "$COMMAND_METAFLAC $padding returned error: $?";
				main::error_handler('application-error', $error_message,1);
			}
			print 'removed... ';
		}
		else {
			print 'remove: skip... ';
		}
		$cmd = qq($COMMAND_METAFLAC $tag "$file");
		say 'tag updater add cmd: ', $cmd if $dbg[5];
		if (!$b_test){
			# if the only action is to UNSET the tag, $tag will be empty
			if ($tag){
				qx($cmd);
				if ($? > 0){
					$error_message = "$COMMAND_METAFLAC returned error: $?";
					main::error_handler('application-error', $error_message,1);
				}
			}
			say 'Tags updated.';
		}
		else {
			say 'Tags update: skip';
		}
	}
	if (!@found_list){
		say "No $INPUT_TYPE files found.";
	}
}

sub make_tag_file {
	my ($file,@files);
	chdir "$start_dir";
	#chdir $SOURCE_DIRECTORY;
	my $source_dir = $SOURCE_DIRECTORY;
	# $source_dir =~ s|([\(\)\$])|\\$1|g;
	$source_dir =~ s|^\Q$source_dir$path_separator\E||;
	say "source1:  $source_dir" if $dbg[4];
	print "Checking for pre-existing $AUTOTAG_FILE... ";
	if ($b_test){
		say "Running test mode.";
	}
	elsif (-e $source_dir . $path_separator . $AUTOTAG_FILE){
		main::error_handler('file-exists', "File $AUTOTAG_FILE already exists in:\n$source_dir",1);
	}
	else {
		say "none found. Proceeding.";
	}
	if ($dbg[4]){
		say 'cwd: ', Cwd::getcwd();
		system 'pwd';
		say "start1: $start_dir"
	}
	print "Creating $AUTOTAG_FILE file... ";
	$extension = $INPUT_TYPE;
	$list_type = 'file';
	@found_list = ();
	File::Find::find(\&main::wanted, @source_glob);
	say '' if $VERBOSITY > 2;
	foreach $file (sort { "\L$a" cmp "\L$b" } @found_list){
		$b_dest_changed = 1;
		$file =~ s|^\Q$SOURCE_DIRECTORY$path_separator\E||;
		say $file if $VERBOSITY > 2;
		push(@files, $file);
	}
	if (@files){
		@files = sort @files; # can be a problem, if track number comes last!
		say "Processing.";
		populate_tag_file(\@files);
	}
	else {
		say "No files found to process.";
	}
}

sub prefill_data {
	my ($b_fields,$b_performer,$b_performer_run,$b_tracks_end,$b_tracks_start,
	$field_name,$field_value);
	# no blank lines in band/location/date block, stop after first blank line.
	my ($b_top_block,$info) = (1,{});
	my $data = InfoFix::open_info_file('prefill');
	my $counter = 0;
	# we only want values where something exists, found french syntax, other 
	# languages will be added if people want them.
	my $fields = join('|',qw(Album Album[\s_-]?Artist Album[\s_-]?Sort Artiste?s? 
	Artist\s*\/\s*Band Band CDDB CDDB[\s_-]?ID City Club 
	Composer Composed[\s_-]?by Conducted[\s_-]?by Conductor Country Cover Date 
	Ensemble Etree Etree[\s_-]?ID Event Festival Genre Image 
	Label Lieu Location Media Mastered[\s_-]?by Masterer Mixed[\s_-]?by Mixer 
	Opus Orquestr?a Producer Publisher
	Recorded[\s_-]?by Remastered[\s_-]?by Remasterer Remixed[\s_-]?by Remixer 
	SHNID Source[\s_-]?Media State Symphony Taper Taped[\s_-]?by Type
	Venue Ville Year)
	);
	if ($run{'autotag-unique'}){
		$info->{'unique'} = uc($run{'autotag-unique'});
	}
	foreach my $line (@$data){
		$line = main::trimmer($line);
		$counter++;
		$b_top_block = 0 if !$line && $counter > 1;
		($field_name,$field_value) = ('','');
		if (($b_top_block || $b_fields) && $line =~ /^($fields)\s*:\s*(.+)$/i){
			$b_fields = 1;
			$field_name = $1;
			$field_value = $2;
		}
		if ($b_top_block && !$b_fields){
			if ($counter == 1){
				$info->{'artist'} = $line;
			}
			elsif ($counter < 7 && $line !~ /^(19|20)[0-9]{2}-/){
				$line =~ s/,$//;
				if ($info->{'location'}){
					if ($info->{'location'} !~ /\Q$line\E/i){
						$info->{'location'} .= ', ' . $line ;
					}
				}
				else {
					$info->{'location'} = $line;
				}
			}
			elsif ($counter < 7 && 
			$line =~ /^(((19|20)[0-9]{2})-[01][0-9]-[0-3][0-9])(.*)?/){
				$info->{'date-print'} = $1;
				$info->{'date-print'} .= $4 if $4;
				$info->{'date'} = $1;
				$info->{'year'} = $2;
			}
		}
		if ($b_fields && $field_name){
			if ($field_name =~ /^(Album)$/i){
				$info->{'album'} = $field_value;
			}
			elsif ($field_name =~ /^(Album[\s_-]?Artist)$/i){
				$info->{'albumartist'} = $field_value;
			}
			elsif ($field_name =~ /^(Album[\s_-]?Sort)$/i){
				$info->{'albumsort'} = $field_value;
			}
			elsif ($field_name =~ /^(Artiste?s?|(Artiste?s?\s*\/\s*)?Band)$/i){
				$info->{'artist'} = $field_value;
			}
			elsif ($field_name =~ /^(CDDB([\s_-]?ID)?)$/i){
				$info->{'cddb'} = $field_value;
			}
			elsif ($field_name =~ /^(Compos(er|ed[\s_-]?by))$/i){
				$info->{'composer'} = $field_value;
			}
			elsif ($field_name =~ /^(Conduct(ed[\s_-]?by|or))$/i){
				$info->{'conductor'} = $field_value;
			}
			# full date ISO strings plus extra
			elsif ($field_name =~ /^(Date)$/i &&
			$field_value =~ /^(((19|20)\d{2})-[01]\d-[0-3]\d)(.*)?/){
				$info->{'date-print'} = $1;
				$info->{'date-print'} .= $4 if $4;
				$info->{'date'} = $1;
				$info->{'year'} = $2;
			}
			# accept YYYY for date/year too
			elsif ($field_name =~ /^(Date)$/i && $field_value =~ /^((19|20)\d{2})$/){
				$info->{'date'} = $1;
				$info->{'year'} = $1;
			}
			elsif ($field_name =~ /^(Ensemble|Orquestr?a|Symphony)$/i){
				$info->{'ensemble'} = $field_value;
			}
			elsif ($field_name =~ /^(Etree([\s_-]?db))?$/i){
				$info->{'etree'} = $field_value;
			}
			elsif ($field_name =~ /^(Genre)$/i){
				$info->{'genre'} = $field_value;
			}
			elsif ($field_name =~ /^(Cover|Image)$/i){
				$info->{'image'} = $field_value;
			}
			elsif ($field_name =~ /^(City|Country|Location|State|Ville)$/i){
				if ($info->{'location'}){
					if ($info->{'location'} !~ /\Q$field_value\E/i){
						$info->{'location'} .= ', ' . $field_value;
					}
				}
				else {
					$info->{'location'} = $field_value;
				}
			}
			elsif ($field_name =~ /^((Master|Mix)(er|ed[\s_-]?by))$/i){
				$info->{'mixer'} = $field_value;
			}
			elsif ($field_name =~ /^(Producer|Taper|(Record|Tap)ed[\s_-]?by)$/i){
				$info->{'producer'} = $field_value;
			}
			elsif ($field_name =~ /^(Publisher|Label)$/i){
				$info->{'publisher'} = $field_value;
			}
			elsif ($field_name =~ /^(Re(mix|master)(er|ed[\s_-]?by))$/i){
				$info->{'remixer'} = $field_value;
			}
			elsif ($field_name =~ /^(SHNID)$/i){
				$info->{'shnid'} = $field_value;
			}
			elsif ($field_name =~ /^((Source[\s_-]?)?Media)$/i){
				$info->{'sourcemedia'} = $field_value;
			}
			elsif ($field_name =~ /^(Type)$/i){
				$info->{'type'} = $field_value;
			}
			# an event/festival can be at a club/venue
			elsif ($field_name =~ /^(Club|Event|Festival|Lieu|Venue)$/i){
				if ($info->{'venue'}){
				 $info->{'venue'}.= ', ' . $field_value;
				}
				else {
					$info->{'venue'} = $field_value;
				}
			}
			elsif ($field_name =~ /^(Year)$/i){
				$info->{'year'} = $field_value;
			}
		}
		if (!$b_top_block){
			# see InfoFix::run_fixes as well if changing the terminator regex
			if (!$b_tracks_end && $b_tracks_start && 
			$line =~ /^\s*[:^<>]{1,}?(E[ST]L?|END([\s_-]?(SETLIST|TRACKS))?)?[:^<>]{1,}\s*$/i){
				$b_tracks_end = 1;
			}
			# Start building tracks, note: on other lines can start with numbers like this!
			# but with terminator not issue after tracklist, but can be before.
			# Corner cases: (2), ;, %,*; (4:45) *, just dump the trailing items.  
			# Not needed to add back onto track title, tags don't need those anyway usually.
			if (($b_fields || $counter > 4) && !$b_tracks_end && 
			$line =~ /^([1-6]-\d{1,2}|\d{1,2})\.\s(.*?)[\s;%\*]*(\s+[\[\(\{][\d:\.\s]+[\]\)\}]\s*)*[\s;%\*]*$/){
				push(@{$info->{'tracks'}},$2);
				$b_tracks_start = 1;
			}
			# Start building performers
			elsif (!$b_performer_run && ($b_fields || $counter > 4) && 
			$line =~ /^[\s*_=-]*(Band|(Band\s)?Line[\s_-]?up|(Band\s)?Members|Performers|Personnel)[^:]*:/i){
				$b_performer = 1;
				next;
			}
			if ($b_performer){
				if ($line){
					push(@{$info->{'performers'}},$line);
				}
				# first white space after performer list
				else {
					$b_performer = 0;
					# in case performer line trigger happens later we don't want to add 
					# those items to performer! 
					$b_performer_run = 1;
				}
			}
		}
	}
	say Data::Dumper::Dumper $info if $dbg[6];
	return $info;
}

sub populate_tag_file {
	my ($files) = @_;
	my ($info,@multi,@tags);
	my ($b_first,$j,$holder,$starter) = (1,0,'','');
	my $counter=$start;
	my $b_prefill_tag = ($run{'prefill-tag'} && %{$run{'prefill-tag'}}) ? 1 : 0;
	$info = prefill_data() if $run{'prefill'};
	my @collection = (
	'# The default behavior for all non per track specific tags is for the tag to to',
	'# be applied to all following tags unless that value is replaced by a new value,',
	'# or if the value UNSET is set for that tag following the last file to use it',
	'# in a block of tags.',
	'#',
	'# To disable this behavior, supply comma separated tag names to be used per file,',
	'# and unset after each file is tagged. This allows you for instance to set a',
	'# COMPOSER for a single track. Do not use for any tag that might apply to more',
	'# than one consecutive file. Use TAGNAME%:UNSET at end of block in that case.',
	'UNIQUE',
	'## RECORDING NAME/CREATORS ##',
	'ALBUM','# Name to sort under','ALBUMSORT',
	'ARTIST','# Multi-Artist only','ALBUMARTIST',
	'# Symphony, Orquestra. Usually group playing non-original music.','ENSEMBLE',
	'# Generally classical fields','COMPOSER','CONDUCTOR','OPUS',
	'# Band members, etc.', 'PERFORMER','PERFORMER','PERFORMER','PERFORMER',,
	'PERFORMER','PERFORMER','PERFORMER','PERFORMER','PERFORMER','PERFORMER',
	'## RECORDING COMMENTS. Note: COMMENT preferred over DESCRIPTION ##',
	'COMMENT','COMMENT','COMMENT','COMMENT','COMMENT',
	 '## DISPLAY IMAGES ##',
	 '# Please see sample auto.tag file for more information on IMAGE',
	 '# Path to image file from where auto.tag file is located. Will be',
	 '# set as default type 3, cover image. ',
	 '# Example: IMAGE%:images/cover.jpg', 
	 '# To remove all embedded images:',
	 '# metaflac --remove --block-type=PICTURE,PADDING --dont-use-padding *.flac',
	 'IMAGE',
	'## RECORDING INFO ##',
	# note: VENUE and LABEL better handled by xiph spec: LOCATION and ORGANIZATION
	# Include YEAR to make conversion to mp3 tagging cleaner
	'GENRE','RATING',
	'# Use ISO format: YYYY-MM-DD','DATE','# YYYY','YEAR','LOCATION','VENUE',
	'MIXER','REMIXER',
	'# Person responsible, either taper, funder, etc.','PRODUCER','PUBLISHER',
	'# e.g. record label, taper group.','ORGANIZATION',
	'# Database type IDs', 'CDDB','ETREE','SHNID',
	'## TECHNICAL INFORMATION ##',
	'ENCODING',
	'# Useful for tapers etc','SOURCE','SOURCE','SOURCE','SOURCE',
	'# Media recording taken from, eg, cassette, tape, dat, vinyl',
	'SOURCEMEDIA',
	);
	my @discs = ('DISCNUMBER','DISCSUBTITLE','TRACKTOTAL');
	my @disc = ('','## DISC INFO ##',
	'# Leave DISCNUMBER, DISCTOTAL empty if 1 disc set',
	'DISCTOTAL',
	'# Do not leave TRACKTOTAL empty, this is per disc track totals for players',
	@discs,'',
	'## TRACK INFO ##', 
	'# Use of any of above tags between track blocks will switch to the new value.',
	'# To make the value empty use: UNSET as the field value.',
	'# TRACKNUMBER and FILE required, title not known?: suggest TITLE%:Unknown ');
	my @track = ('','TRACKNUMBER','TITLE','VERSION','PART','FILE');
	splice(@track, 1, 0, 'ARTIST') if $run{'multiartist'};
	@collection = map {$_ .= '%:' if /^[A-Z]/; $_;} @collection;
	# ALBUM, ALBUMARTIST, TYPE handled explicitly below
	my @prefills = qw(ALBUMSORT ARTIST CDDB COMPOSER CONDUCTOR DATE ENSEMBLE 
	ETREE GENRE IMAGE LOCATION MIXER OPUS PRODUCER PUBLISHER REMIXER 
	SHNID SOURCEMEDIA VENUE YEAR);
	@prefills = map {$_ .= '%:'; $_;} @prefills;
	if ($info && %$info && @collection){
		my @working;
		foreach my $item (@collection){
			# these first ones require custom handling
			if ($item eq 'ALBUM%:'){
				if ($b_prefill_tag && $run{'prefill-tag'}->{'ALBUM'}){
					$item .= $run{'prefill-tag'}->{'ALBUM'};
				}
				elsif ($info->{'album'}){
					$item .= $info->{'album'};
				}
				# we'll construct the album name out of the bits
				elsif ($info->{'date-print'} || $info->{'location'} || $info->{'venue'}){
					$item .= $info->{'date-print'} . ', ' if $info->{'date-print'};
					if ($info->{'venue'} && 
					(!$info->{'location'} || $info->{'location'} !~ /\Q$info->{'venue'}\E/i)){
						$item .= $info->{'venue'} . ', ';
					}
					$item .= $info->{'location'} if $info->{'location'};
					$item =~ s/,\s*$//;
				}
				if ($info->{'type'}){
					$item .= ' - ' . $info->{'type'};
				}
				if ($info->{'etree'}){
					$item .= ' (etree ' . $info->{'etree'} . ')';
				}
				elsif ($info->{'shnid'}){
					$item .= ' (shnid ' . $info->{'shnid'} . ')';
				}
			}
			elsif ($item eq 'ALBUMARTIST%:'){
				if ($b_prefill_tag && $run{'prefill-tag'}->{'ALBUMARTIST'}){
					$item .= $run{'prefill-tag'}->{'ALBUMARTIST'};
				}
				elsif ($info->{'artist'} || $info->{'albumartist'}){
					$item .= ($info->{'albumartist'}) ? $info->{'albumartist'} : $info->{'artist'};
				}
			}
			elsif ($item eq 'PERFORMER%:'){
				if ($info->{'performers'} && @{$info->{'performers'}}){
					$item .= shift @{$info->{'performers'}};
				}
			}
			elsif ($item eq 'UNIQUE%:'){
				if ($info->{'unique'}){
					$item .= $info->{'unique'};
				}
			}
			# and these are generic prefills
			elsif (grep {$_ eq $item} @prefills){
				my $field = $item;
				$field =~ s/%:$//;
				my ($lc_field,$uc_field) = (lc($field),uc($field));
				if ($b_prefill_tag && $run{'prefill-tag'}->{$uc_field}){
					$item .= $run{'prefill-tag'}->{$uc_field};
				}
				elsif ($info->{$lc_field}){
					$item .= $info->{$lc_field};
				}
			}
			push(@working,$item);
		}
		@collection = @working if @working;
	}
	say Data::Dumper::Dumper \@collection if $dbg[6];
	@disc = map {$_ .= '%:' if /^[A-Z]/; $_;} @disc;
	@discs = map {$_ .= '%:' if /^[A-Z]/; $_;} @discs;
	if ($run{'autotag-single'} && @$files){
		@disc = map {$_ .= create_track_total(scalar (@$files)) if $_ eq 'TRACKTOTAL%:'; $_;} @disc;
	}
	if ($run{'autotag-multi'} && @$files){
		$autotag_multi =~ s/%/[1-9]/g;
		$autotag_multi =~ s/@/[A-Z]/g;
		# say "$autotag_multi";
		foreach (@$files){
			if (/^($autotag_multi)/i){
				$starter = $1;
				# $counter++;
			}
			else {
				main::error_handler('autotag-multi',
				 "$autotag_multi filename start pattern not found in file:\n$_!!",1);
			}
			if ($holder ne $starter){
				$j++;
				$holder = $starter;
				$multi[$j] = 1;
				# say "$starter $j";
			}
			else {
				$multi[$j]++;
			}
		}
		shift @multi;# get rid of first element
		($counter,$j,$holder,$starter) = ($start,0,'','');
		# this handles the first block of DISC info
		if (scalar @multi > 1){
			# say 'm:', $multi[0], ' sm: ', scalar @multi;
			@disc = map {$_ .= scalar @multi if $_ eq 'DISCTOTAL%:'; $_;} @disc;
			@disc = map {$_ .= create_track_total($multi[0]) if $_ eq 'TRACKTOTAL%:'; $_;} @disc;
			@disc = map {$_ .= 1 if $_ eq 'DISCNUMBER%:'; $_;} @disc;
		}
	}
	say Data::Dumper::Dumper \@multi if $dbg[7];
	@tags = (@collection,@disc);
	@track = map {$_ .= '%:' if /^[A-Z]/; $_;} @track;
	my (@disc_working,@info_working);
	if ($info->{'tracks'}){
		if (scalar(@{$info->{'tracks'}}) == scalar(@$files)){
			say "File/$INFO_FILE track counts match: " . scalar(@$files);
			@info_working = @{$info->{'tracks'}};
		}
		else {
			main::error_handler('track-counts','Prefill track count mismatch: Files: ' . 
			scalar(@$files) . '; Info Tracks: ' . scalar(@{$info->{'tracks'}}),1);
		}
	}
	my ($k,$ma_split) = (0,'');
	if ($run{'multiartist'}){
		$k = 1;
		$ma_split = $run{'multiartist'}->[1];
	}
	foreach (@$files){
		my @track_working = @track;
		if ($run{'autotag-multi'} && scalar @multi > 1){
			if (/^($autotag_multi)/i){
				$starter = $1;
			}
			if ($holder ne $starter){
				$holder = $starter;
				# say "$starter";
				if (!$b_first){
					$counter = $start;
					$j++;
					@disc_working = @discs;
					# and this handles all subsequent new disc/set block
					$disc_working[0] .= $j+1; # disc number
					$disc_working[2] .= create_track_total($multi[$j]); # track total
					# then we add in the tracks for the previous disk/set, and start
					# the next block
					push(@tags,'',@disc_working);
				}
				$b_first = 0;
			}
		}
		# the track number
		if ($run{'autotag-single'} || $run{'autotag-multi'}){
			$track_working[$k+1] = $track[$k+1] . $counter;
		}
		# the file name
		$track_working[$k+5] = $track[$k+5] . $_;
		if (@info_working){
			my $title = shift(@info_working);
			if (!$run{'multiartist'}){
				$track_working[$k+2] = $track[$k+2] . $title;
			}
			else {
				my ($one,$two) = split(/\s+\Q$ma_split\E\s+/,$title,2);
				if (!defined $one || !defined $two){
					main::error_handler('multiartist-split','Multiartist splits: ' .
					'split pattern \'' . $ma_split . '\' not found in: ' . $title,1);
				}
				($one,$two) = ($run{'multiartist'}->[0] eq 'at') ? ($one,$two):($two,$one);
				$track_working[$k+0] = $track[$k+0] . $one; # ARTIST%:[band]
				$track_working[$k+2] = $track[$k+2] . $two; # TITLE%: [title]
			}
		}
		if (!$run{'no-replaygain'}){
			my $replaygain = get_replaygain(main::escape_item("$_"));
			# we want the replaygain data right before FILE
			splice(@track_working, 5, 0, @$replaygain) if @$replaygain;
		}
		push(@tags,@track_working);
		# this allows for 0 track numbers
		$counter++;
	}
	if ($b_test){
		say "Test mode $AUTOTAG_FILE data:\n$line_result\n";
		say join("\n", @tags),"\n";
	}
	else {
		main::writer($AUTOTAG_FILE,\@tags);
	}
}

sub image_embedder {
	my ($args,$cmd,$error_message,@image_args,$item,$print_file,
	$result,$working_dir);
	$list_type = 'file';
	$extension = $INPUT_TYPE;
	@found_list = ();
	File::Find::find(\&main::wanted, @source_glob);
	chdir "$start_dir";
	chdir "$SOURCE_DIRECTORY";
	if ($image_embed && ! -e $image_embed){
		$error_message = "Embed image $image_embed could not be located!\n";
		$error_message .= "Source directory: $SOURCE_DIRECTORY\n";
		$error_message .= "Please make sure your embed image file name or path is correct.\n";
		main::error_handler('file-missing', $error_message,1);
	}
	foreach $item (sort { "\L$a" cmp "\L$b" } @found_list){
		$print_file = $working_dir = $item;
		$print_file =~ s|^\Q$SOURCE_DIRECTORY$path_separator\E|| if $VERBOSITY < 3;
		print "Processing: $print_file...";
		$item = main::escape_item("$item");
		if ($run{'image-remove'}){
			image_remover("$item");
		}
		if ($image_embed){
			@image_args = image_handler("$item",("$image_embed"));
			$args = join ' ', @image_args;
			$cmd = qq($COMMAND_METAFLAC $args "$item");
			say "\nimage embedder", $cmd if $dbg[5];
			if (!$b_test){
				qx($cmd);
				if ($? > 0){
					$error_message = "$COMMAND_METAFLAC returned error: $?";
					main::error_handler('application-error', $error_message,1);
				}
				say 'Image embedded';
			}
			else {
				say 'Test mode only: image embed';
			}
		}
		else {
			say 'Image and padding data removed';
		}
		$b_dest_changed = 1;
	}
	if (!@found_list){
		say "No $INPUT_TYPE files found.";
	}
}

sub image_remover {
	my ($file) = @_;
	my ($cmd,$error_message) = ('','');
	print "Removing images... ";
	$cmd = qq($COMMAND_METAFLAC --remove --block-type=PICTURE,PADDING --dont-use-padding "$file");
	if (!$b_test){
		qx($cmd);
		if ($? > 0){
			$error_message = "$COMMAND_METAFLAC returned error: $?";
			main::error_handler('application-error', $error_message,1);
		}
	}
	say "\nimage remover cmd: ", $cmd if $dbg[5];
}

# note: metaflac is ignoring type integer and assigning 3, not sure why
sub image_handler {
	my ($file,@images) = @_;
	my ($size,@args,@result,$working);
	my $cmd = qq($COMMAND_METAFLAC --list --block-type=PICTURE "$file");
	@result = qx($cmd);
	say "\nimage handler cmd: ", $cmd if $dbg[5]; 
	@result = grep {/type:.*\(PICTURE\)/} @result if @result;
	# filename: type: 6 (PICTURE) 3||||images/cover.jpg
	# --import-picture-from="3||||front.jpg"
	foreach (@images){
		$working = (split /\|/, $_)[-1];
		if (@result){
			say "\n  Skipping $working (pre-existing image data)... ";
		}
		else {
			if (-e $working){
				$size = sprintf("%.1f", File::stat::stat("$working")->size / 1024);
				push @args, qq(--import-picture-from="$_");
				say "\n  Embedding $working ($size KiB)... ";
			}
			else {
				say "\n  File $working not found... ";
			}
		}
	}
	return @args;
}

# The idea here is that the track total will show the full number including
# removed start tracks. Not a common situation, but does happen
sub create_track_total {
	my $working = $_[0];
	# note: 0 start, track number to 14, 15 tracks, would show 14/15, so make shwo 14/14
	my ($total) = (0);
	if ($start == 0){
		$total = $working - 1;
	}
	# the normal, simple scalar of the found files. Default behavior
	elsif ($start == 1){
		$total = $working;
	}
	# ex: 15 original tracks, first 2 removed, start, file numbering == 03
	# 13 actual tracks, start 3, subtract 1.
	else {
		$total = $working + $start - 1;
	}
	say "ctt: $start $total" if $dbg[8];
	return $total;
}

# --preserve-modtime --remove-all-tags --remove-tag
sub get_replaygain {
	my ($input_file) = @_;
	my @tags;
	my $replaygain = main::get_flac_tags('replaygain',$input_file);
	foreach (keys %$replaygain){
		push(@tags, $_ . '%:' . $replaygain->{$_}) if $replaygain->{$_};
	}
	return \@tags;
}
}

#### -------------------------------------------------------------------
#### CHECKSUMS
#### -------------------------------------------------------------------

## Checksums 
{
package Checksums;
my ($print_src,%raw_ffps);

sub process {
	say $line_large;
	my $type = 'checksum processing';
	if ($run{'analyze'}){
		$type = lc($INPUT_TYPE) . ' analysis';
	}
	elsif ($run{'duplicates'}){
		$type = 'ffp duplicate tests';
	}
	elsif ($run{'checksum-ffps'}){
		$type = 'ffp collection';
	}
	say "Starting $type in: " . main::sourcer("$SOURCE_DIRECTORY");
	process_directories();
	say $line_small;
	say "Completed $type.";
	say $line_large;
}

sub process_directories {
	my ($result);
	my $qualify = '';
	if ($run{'no-ffp'}){
		$qualify .= " (Skipping ffp processing)";
	}
	if ($run{'no-md5'} && !$run{'duplicates'} && !$run{'checksum-ffps'}){
		$qualify .= " (Skipping md5 processing)";
	}
	$list_type = 'dir';
	say $line_small;
	say "Checking directories...$qualify";
	@found_list = ();
	chdir "$start_dir";
	File::Find::find(\&main::wanted, @source_glob);
	say 'start dir: ', $start_dir if $dbg[4];
	my $file_type = lc($INPUT_TYPE) . ',' . uc($INPUT_TYPE) . ',';
	$file_type .= ucfirst(lc($INPUT_TYPE)); # flac,FLAC,Flac
	foreach my $item (sort { "\L$a" cmp "\L$b" } @found_list){
		my $b_valid;
		$print_src = $item;
		$print_src =~ s|^\Q$SOURCE_DIRECTORY$path_separator\E|| if $VERBOSITY < 3;
		# if ($VERBOSITY > 2){
		#	say Cwd::getcwd();
		#	system 'pwd';
		# }
		chdir "$start_dir";
		chdir "$item";
		if ($dbg[4]){
			say 'srcdir: ', $SOURCE_DIRECTORY;
			say 'item: ', $item;
			say 'cwd: ', Cwd::getcwd();
			system 'pwd';
		}
		say $line_small;
		say " Processing: $print_src";
		my @files = main::globber("*.{$file_type}");
		say "\n", Data::Dumper::Dumper \@files if $dbg[6];
		if (grep -f, @files){
			if (!$run{'analyze'}){
				delete_checksums() if $run{'checksum-delete'};
				generate_checksums() if $run{'checksum'};
				$b_dest_changed = 1;
			}
			else {
				Analyze::run('data',\@files);
			}
		}
		else {
			say "  No $INPUT_TYPE files";
		}
		if ($run{'checksum-verify'}){
			verify_checksums(\@files);
		}
	}
	if ($run{'duplicates'}){
		check_ffp_duplicates();
	}
}

sub generate_checksums {
	if (!$run{'duplicates'} && !$run{'checksum-ffps'}){
		say "  Generating checksums:";
		print "   File: $FFP_FILE.ffp: ";
	}
	else {
		say "  Collecting ffps...";
	}
	if ($dbg[4]){
		say 'cwd: ', Cwd::getcwd();
		system('pwd');
	}
	if (!$run{'no-ffp'}){
		generate_ffp() if !$run{'no-ffp'};
	}
	else {
		say 'skipped';
	}
	if (!$run{'no-md5'} && !$run{'duplicates'}){
		print "   File: $MD5_FILE.md5: ";
		generate_md5();
	}
}

sub generate_ffp {
	my ($error_message,@output);
	# system("$COMMAND_METAFLAC --show-md5sum *.flac > $FFP_FILE.ffp");
	# explicit --with-filename required in cases of 1 flac in directory
	@output = qx($COMMAND_METAFLAC --with-filename --show-md5sum *.flac);
	if ($? == -1 || $? > 0){
		$error_message = "$COMMAND_METAFLAC returned error: $?";
		main::error_handler('application-error', $error_message,1);
	}
	else {
		chomp (@output);
		if ($run{'duplicates'}){
			$raw_ffps{$print_src} = \@output;
		}
		elsif (!$b_test && !$run{'checksum-ffps'}){
			main::writer("$FFP_FILE.ffp",\@output);
			say 'created for ' . scalar(@output) . ' files';
		}
		else {
			say '' if !$run{'duplicates'} && !$run{'checksum-ffps'};
			say "   Files: " . scalar(@output), "\n    ", join("\n    ", @output);
		}
	}
}

sub generate_md5 {
	my ($error_message,@output);
	# system("$COMMAND_MD5 *.* > $MD5_FILE.md5");
	@output = qx($COMMAND_MD5 -b *.*);
	if ($? == -1 || $? > 0){
		$error_message = "$COMMAND_MD5 returned error: $?";
		main::error_handler('application-error', $error_message,1);
	}
	else {
		chomp (@output = grep {$_ !~ m^($AUTOTAG_FILE|\.ffp|\.md5)^} @output);
		if (!$b_test){
			main::writer("$MD5_FILE.md5",\@output);
			say 'created for ' . scalar(@output) . ' files';
		}
		else {
			say "\n   Files: " . scalar(@output), "\n    ", join("\n    ", @output);
		}
	}
}

sub verify_checksums {
	my ($files) = @_;
	verify_flacs() if !$run{'no-ffp'} && grep -f, @$files;
	verify_md5() if !$run{'no-md5'};
}

# NOTE: something is wrong with how flac outputs to stderr and
# creates invisible text strings, maybe unicode related
sub verify_flacs {
	my ($b_app_error,$error_message,$err_nu,$output,@errors);
	my ($bad,$err,$msg) = ('','','');
	print "  Checking $INPUT_TYPE file ffps (be patient): ";
	say '' if $run{'infofix-verify'};
	# note, flac sends output to stderr
	# NOTE: $_ =~ s/([^\x20-\x7E])/sprintf '\x{%02x}', ord $1/eg;
	# shows the weird hidden text this command creates
	say 'cwd: ', Cwd::getcwd() if $dbg[4];
	if (!(grep -f, main::globber("*.$INPUT_TYPE"))){
		say "No $INPUT_TYPE files found.";
		return;
	}
	my @results = qx($COMMAND_FLAC -wt *.flac 2>&1);
	$err_nu = $?;
	chomp @results;
	if ($dbg[21]){
		say "\@result count: ", scalar @results, "\n  ", join("\n  ",@results);
	}
	@results = grep {
		!/^\s*$/ && 
		!/(Xiph\.org|^flac\s[0-9]|this is free soft|for details)/ig} @results;
	@results = map {
		$_ =~ s/\s+$//;
		$_ =~ s/(testing.*complete|\x{08})//gi;
		$_ =~ s/[^[:print:]]+//g;
		$_;
	} @results;
	# 	foreach (@results){
	# 		$_ =~ s/([^\x20-\x7E])/sprintf '\x{%02x}', ord $1/eg;
	# 		say "##$_##";
	# 	}
	if ($err_nu > 0){
		$b_app_error = 1;
		$error_message = "$COMMAND_FLAC returned error: $?";
		print '  ' if !$run{'infofix-verify'};
		main::error_handler('application-error', $error_message,0);
		if ($VERBOSITY > 1){
			$output = "   Error Result:\n    " . join("\n    ",@results);
			if (!$run{'infofix-verify'}){
				say $output if $VERBOSITY > 1;
			}
			else {
				$err = $output;
			}
		}
	}
	$err_nu = 0;
	# @results = grep {/\.$INPUT_TYPE:\s/i} @results;
	# track.flac: testing, 33% completetesting, 65% completetesting, 98% completeok
	my $index = 0;
	my ($b_start,@temp);
	foreach (@results){
		# say length($_);
		# $_ =~ s/\s+$//; # moved to map
		# say "${_}::";
		# next if /^\s*$/; # moved to grep
		$b_start = 1 if !$b_start && $_ =~ /\.$INPUT_TYPE:/;
		next if !$b_start;
		#$_ =~ s/(testing.*complete|\x{08})//gi; # moved to map
		#$_ =~ s/[^[:print:]]+//g; # moved to map
		# say length($_);
		# say $_;
		# the poor design of flac output is hard to put into words
		# not only do they use those backspace hidden things, but
		# sometimes they decide to put the ok/failed/warning on its own line!!!
		# .flac: WARNING, cannot check MD5 signature since it was unset in the STREAMINFO
		# ok - or more odd, say it's ok after giving a warning
		if (/\.$INPUT_TYPE:/i){
			$index = scalar @temp;
			push(@temp, $_);
		}
		else {
			$_ =~ s/^\s+|\s+$//g;
			my $append = (lc($_) eq 'ok') ? " $_" : " :: $_" ;
			$temp[$index] .= $append if scalar @temp > 0;
		}
	}
	@results = @temp;
	# say scalar @results, "\n", "@results";
	# NOTE: cases found where ok and rest of string prints on next line!!
	@errors = grep {!/\.$INPUT_TYPE:\s+ok$/i} @results;
	@results = grep {/\.$INPUT_TYPE:\s+ok$/i} @results;
	# say scalar @results, "\n", "@results";
	# say scalar @errors, "\n", "@errors";
	# say "errors:\n", join "\n", @errors;
	if (@errors){
		$output = "   Files with errors:\n    " . join("\n    ", @errors);
		if (!$run{'infofix-verify'}){
			say '' if !$b_app_error;
			say $output;
		}
		else {
			$bad = $output;
		}
	}
	if (@results){
		if ($VERBOSITY > 1 || $b_app_error){
			$output = "   Verified files:\n    " . join("\n    ", @results);
			if (!$run{'infofix-verify'}){
				say '' if !$b_app_error && !@errors;
				say $output;
			}
			else {
				$msg = $output;
			}
		}
		else {
			$output = "Verified";
			if (!$run{'infofix-verify'}){
				say $output;
			}
			else {
				$msg = $output;
			}
		}
	}
	else {
		$output = "No verified $INPUT_TYPE files";
		if (!$run{'infofix-verify'}){
			say $output;
		}
		else {
			$msg = $output;
		}
	}
	return [$msg,$bad,$err] if $run{'infofix-verify'};
}

sub verify_md5 {
	my (@data,@errors,@results,@working);
	my ($b_bad,$b_break,$cmd,$hash,$hash2,$error_message,$msg,$result);
	my @checksums = main::globber("*.{md5,md5.txt}");
	foreach my $file (@checksums){
		print "  Checking md5 checksums in: $file: ";
		@data = main::reader($file,'strip');
		# say "data\n", join ("\n", @data);
		$b_break = 0;
		foreach my $track (@data){
			# say "track: $track";
			($hash,$hash2) = ('','');
			$track =~ /^([a-f0-9]+)([\s\*]+)(.*)$/;
			if ($1 && $3){
				$hash = $1;
				$track = $3;
				# convert to local paths, windows vs nix
				$track =~ s^[/|\\]^$path_separator^g;
				# say 't: ', $track;
				if ($VERBOSITY > 1){
					say '' if !$b_break;
					print "   Checking: $track: ";
					$b_break = 1;
				}
				if (-e $track){
					my $escaped = main::escape_item("$track");
					$cmd = qq($COMMAND_MD5 "$escaped");
					$result = qx($cmd);
					$hash2 = (split /\s/, $result)[0];
				}
				else {
					$hash2 = 'missing';
				}
				if ($hash2){
					if ($hash eq $hash2){
						say "Matched" if $VERBOSITY > 1;
					}
					else {
						say '' if !$b_break;
						print "   $track: " if $VERBOSITY < 2;
						$msg = ($hash2 ne 'missing') ? "FAILED" : "FAILED: File Not Found";
						say $msg;
						$b_bad = 1;
						$b_break = 1;
					}
					if ($VERBOSITY > 2){
						say "    MD5 Test: $hash Actual: $hash2";
					}
				}
			}
		}
		say "All Files Matched" if !$b_bad && $VERBOSITY < 2;
	}
}

sub delete_checksums {
	my $error_message = '';
	# note: some checksum generators tack on a .txt to the filename.
	# seen case like: "verbose stuff md5.txt
	my $glob = '*{.cfp,.ffp,.md5,.st5,index.html{.1,.2,.3,},md5ok,';
	$glob .= '{md5{ok,},md5sum{s,},ffp{s,},robots,sbeok,';
	$glob .= 'shn{len,tool}{_len,_length,},st5}.txt{.1,.2,.3,}}';
	my @checksums = main::globber($glob);
	# say Data::Dumper::Dumper @checksums;
	foreach my $file (@checksums){
		print "   Deleting $file: ";
		if ($b_test){
			say "test deletion";
		}
		elsif (unlink($file)){
			say "deleted";
		}
		else {
			main::error_handler('checksum-delete', "Failed to delete: $file");
		}
	}
}

sub check_ffp_duplicates {
	# say Data::Dumper::Dumper \%raw_ffps;
	my (%dupes,%orig,@ffps);
	my ($count,$i,$pad) = (0,0,2);
	say $line_small;
	say "Starting duplicate verification...";
	foreach my $dir (sort keys %raw_ffps){
		# say 'd1: ', $dir;
		foreach my $item (@{$raw_ffps{$dir}}){
			my @track = split(/:/,$item);
			if (!grep {$_ eq $track[-1]} @ffps){
				push(@ffps, $track[-1]);
			}
			else {
				if ($VERBOSITY > 2){
					say " Duplicate found in:\n  $dir:";
					say "   ffp:  $track[-1]";
					say "   file: $track[0]";
				}
				push(@{$dupes{$dir}},$item);
			}
		}
	}
	say $line_small;
	if (%dupes){
		$count = keys %dupes;
		$pad = get_pad($count);
		say "The following $count directories had ffp duplicates:";
		foreach my $dir (sort keys %dupes){
			$i++;
			say ' ', sprintf("%0${pad}d: ", $i), $dir;
			# if ($VERBOSITY > 1){
				say "  dupe: ", join("\n  dupe: ", sort @{$dupes{$dir}});
			# }
		}
		say $line_small;
		say "These were first found in:";
		foreach my $dir (sort keys %dupes){
			 #say 'd2: ', $dir;
			foreach my $item (@{$dupes{$dir}}){
				my $ffp = (split(/:/,$item))[-1];
				FFP:
				foreach my $dir1 (sort keys %raw_ffps){
					FFP1:
					foreach my $item1 (@{$raw_ffps{$dir1}}){
						# check dirs and items to allow for dupes in same directory
						next FFP1 if $dir eq $dir1 && $item eq $item1;
						my $ffp1 = (split(/:/,$item1))[-1];
						if ($ffp eq $ffp1){
							if ($VERBOSITY > 2){
								say " $dir1";
								say "  item: $item1";
							}
							if (!grep {$_ eq $item1} @{$orig{$dir1}}){
								push(@{$orig{$dir1}},$item1);
							}
							next FFP;
						}
					}
				}
			}
		}
		if (%orig){
			$i = 0;
			$count = keys %orig;
			$pad = get_pad($count);
			foreach my $dir (sort keys %orig){
				$i++;
				say ' ', sprintf("%0${pad}d: ", $i), $dir;
				# if ($VERBOSITY > 1){
					say "  orig: ", join("\n  orig: ", sort @{$orig{$dir}});
				# }
			}
		}
	}
	else {
		say "No ffp duplicates found!! Great!!";
	}
}

sub get_pad {
	my $count = $_[0];
	my $pad = 2;
	if ($count < 10){
		$pad = 1;}
	elsif ($count < 100){
		$pad = 2;}
	elsif ($count < 1000){
		$pad = 3;}
	elsif ($count < 10000){
		$pad = 4;}
	return $pad;
}
}

#### -------------------------------------------------------------------
#### CLEANING
#### -------------------------------------------------------------------

## CleanCollection
{
package CleanCollection;

sub process {
	say $line_large;
	say "Starting cleanup of: " . main::sourcer("$DESTINATION_DIRECTORY");
	process_type('directory');
	process_type('file');
	say $line_small;
	say 'Completed cleanup checks.';
	say $line_large;
	exit if !$run{'clean-sync'};
}

sub process_type {
	my ($type) = @_;
	my ($b_deleted,$item,$result);
	$list_type = ($type eq 'directory') ? 'dir-clean': 'file-clean';
	say $line_small;
	say "Checking $type removal...";
	@found_list = ();
	File::Find::find(\&main::wanted, $DESTINATION_DIRECTORY);
	if (@found_list && confirm_deletion($type)){
		foreach $item (sort { "\L$a" cmp "\L$b" } @found_list){
			$result = 'UNSET';
			say "Deleting $type: $item";
			$result = File::Path::rmtree("$item") if !$b_test;
			$b_deleted = 1;
			$b_dest_changed = 1;
			say "Delete $type result: $result" if $VERBOSITY > 1;
		}
	}
	main::print_not_found("$type-cleaned") if !$b_deleted;
}

sub confirm_deletion {
	my ($type) = @_;
	my ($b_confirm,$response) = (0,'');
	say $line_small;
	say join("\n", @found_list);
	say $line_small;
	say "The preceding $type items will be deleted. Deletions cannot";
	say "be restored! BE AWARE!!";
	say "Please type 'delete' + 'enter' to remove them, or hit 'enter' to skip.";
	say "If you are unsure, hit 'enter' to see the file list." if $type eq 'directory';
	chomp($response = <STDIN>);
	if (lc($response) eq 'delete'){
		say "Are you SURE you want to delete these items?";
		say "Type 'yes' + 'enter' to confirm, or hit 'enter' to skip.";
		chomp($response = <STDIN>);
		return 1 if lc($response) eq 'yes';
	}
	say "Skipping deletion for this $type group.";
	return 0;
}
}

#### -------------------------------------------------------------------
#### INFO / TAGLIST PROCESSING
#### -------------------------------------------------------------------

## InfoFix
{
package InfoFix;

sub process {
	my (@files,$item,$print_src,$result);
	$list_type = 'dir';
	say $line_small;
	say "Checking directories for: $INFO_FILE";
	@found_list = ();
	chdir "$start_dir";
	File::Find::find(\&main::wanted, @source_glob);
	say 'start dir: ', $start_dir if $dbg[4];
	Encode::Guess->set_suspects(qw(ascii utf8 cp1252)) if $run{'infofix-character'};
	foreach $item (sort { "\L$a" cmp "\L$b" } @found_list){
		my $b_valid;
		$print_src = $item;
		$print_src =~ s|^\Q$SOURCE_DIRECTORY$path_separator\E|| if $VERBOSITY < 3;
		chdir "$start_dir";
		chdir "$item";
		if ($dbg[4]){
			say 'srcdir: ', $SOURCE_DIRECTORY;
			say 'item: ', $item;
			say 'cwd: ', Cwd::getcwd();
			system 'pwd';
		}
		say " Processing: $print_src";
		if (-r "$INFO_FILE"){
			# say $line_small;
			say "  Working on file: $INFO_FILE";
			run_fixes($item);
		}
		else {
			say "  No $INFO_FILE found.";
			say $line_small;
		}
	}
	if (!@found_list){
		say "No directories found.";
	}
}

sub run_fixes {
	my ($file) = @_;
	my ($disk_id,$newlines,$number) = ('',0,1);
	my ($b_autonumber,$b_tracks_end,@prepped);
	my $data = open_info_file('info-fixes');
	if ($run{'infofix-character'}){
		my $encoding = get_encoding($data);
		cp1252_fix($data) if ($encoding && $encoding eq 'cp1252');
	}
	date_fix($data) if $run{'infofix-date'};
	# Puts it after top data block, but could also put at end
	Analyze::run('info',$data) if $run{'infofix-quality'};
	# say Data::Dumper::Dumper \@data;
	# we want these to be set to uppercase again after autoformat runs
	my $upper = 'Aud|Cdr?|Sd?b|Ips|Eac|Ak|Az|Ca|Co|Ct|Dc|De|Dk|Fl|Flac|Hd|Hi|Il|';
	$upper .= 'Ma|Md|Mkw|Mo|Nc|Nh|Nj|Nm|Nyc?|Pa|Ri|Sbd?|Sf|Shn|Tn|Usa|Ut|Wa|';
	$upper .= 'Wav|Wi|Va|Vt';
	my $line_nu = 0;
	foreach my $row (@$data){
		$line_nu++;
		if ($run{'infofix-lower'}){
			# set to lower, and make upper first character of each line.
			$row = ucfirst(lc($row));
			# make each item following period upper case first
			$row =~ s/(\.\s+)(\w)/$1\u\L$2/g;
			# replace known standard 'words' with UC variants
			$row =~ s/\b($upper)\b/\U$1/gi;
		}
		if ($run{'infofix-upper'}){
			foreach my $style (qw(highlight)){
				$row = Text::Autoformat::autoformat($row, {case => $style, left=>1, right=>1000});
			}
			# set back to upper common abbreviations, states, etc
			# don't do Or or Me or Oh!
			$row =~ s/\b($upper)\b/\U$1/g;
			$row =~ s/\bCD[\s-]?([1-6])/Disc $1/g;
			$row =~ s/\bSB\b/SBD/g;
		}
		$row =~ s/[\r\n]+//g; # has to be AFTER autoformat
		$row =~ s/\t+/ /g; # get rid of tabs.
		$row =~ s/\s\s+/ /g;
		$row =~ s/\s+$//g;
		$row =~ s/\(\s+/\(/g;
		$row =~ s/\s+\)/\)/g;
		$row =~ s/^\s+$//;
		if ($row !~ /^$/){
			$newlines = 0;
		}
		else {
			$newlines++;
		}
		# we don't need to write this to info file since it will then be numbered.
		if ($run{'infofix-autonumber'} && !$b_tracks_end && 
		$row =~ /^:an[\s_-]?(\d+)?:$/){
			$number = 1 if $disk_id && $1 && "$1-" ne $disk_id;
			$disk_id= "$1-" if $1;
			$b_autonumber = 1;
			next;
		}
		# see AutoTag::prefill_data() as well if changing the terminator regex
		if (!$b_tracks_end &&
		$row =~ /^\s*[:^<>]{1,}?(E[ST]L?|END([\s_-]?(SETLIST|TRACKS))?)?[:^<>]{1,}\s*$/i){
			$b_tracks_end = 1;
			$b_autonumber = 0;
		}
		if ($b_autonumber && !$b_tracks_end && $row && 
		$row !~ /^(:an|Dis[ck]|Encore|Set).*:.*/i && $row !~ /^\s*$/i){
			my $nu = sprintf('%02d',$number);
			$row = $disk_id . $nu . '. ' . $row;
			$number++;
		}
		# 1-4 / 1-12 / 12 / 213 / d1t04 / 104-d1t04 / xxx-d1t04
		# Note: don't want to trap times, so use \s after :
		if (($run{'infofix-no'} || $run{'infofix-time'} || $run{'infofix-title'}) && !$b_tracks_end &&
		 $row =~ /^(\s*((\d{3}-|xxx-)?([ds]?[1-9][t-])([0-9]{1,2})|t?([0-9]{1,2}|[0-9]{3}))(\]\s*|\.\s*|\s*-\s*|\s*\)\s*|:\s+|\s+))/i){
			# say "$1 '$6'";
			if ($run{'infofix-no'}){
				# say "$1 $2 $3";
				my $nu = (defined $5) ? $5 : $6;
				my $disc = (defined $4) ? $4 : '';
				my $pattern = $1;
				# say "::${pattern}::";
				if ($pattern =~ /^\s*(\d{3}-|xxx-)?[ds]([1-9])t(\d{1,2})/i){
					$nu = $3;
					$disc = $2 . '-';
				}
				elsif ($nu =~ /^\s*([1-9])([0-9]{2})$/){
					$nu = $2;
					$disc = $1 . '-';
				}
				$nu = sprintf('%02d',$nu);
				$nu = $disc . $nu if $disc;
				$row =~ s/^\Q$pattern\E/$nu. /;
			}
			if ($run{'infofix-time'}){
				# Needs to run inside the replace test to avoid carrying over $3, $4
				# from number fix.
				# Add parenthese, also change [..], {...} to (..)
				if ($row =~ s/\s+\|?\s*:?(\d{1,2}(:\d{2}){1,2}(\.\d+)?)\s*\|?(\s*\*+)?(\s+|$)/ ($1)/){
					$row .= $4 if $4;
					$row =~ s/\)(\S)/) $1/; # add space after if not EOL.
					$row =~ s/\s\s+/ /g; # make sure we didn't add in an extra space
				}
				# this should be run with -Xn to make sure syntax works, slice out time
				# from start of title and append to end of string
				if ($row =~ /^(\d-)?\d{1,2}\.\s([\(\[\{]\d{1,2}(:\d{2}){1,2}(\s*\*+)?[\)\]\}])\s*/){
					my $time = $2;
					$row =~ s/\Q$time\E//;
					$row .= ' ' . $time;
					$row =~ s/\s\s+/ /g; # make sure we didn't add in an extra space
				}
			}
			if ($run{'infofix-title'}){
				foreach my $style (qw(highlight)){
					$row = Text::Autoformat::autoformat($row, { case => $style, left=>1, right=>1000 });
					$row =~ s/[\r\n]+//g; # has to be AFTER autoformat
				}
				$row =~ s/\s+[\(\[]?([0-9]{1,2}[\.:]([0-9\.:]+))[\)\]]?\s*$/ ($1)/;
			}
		}
		# $row =~ s//'/g;
		# $row =~ s/�/'/g;
		# say $row;
		push(@prepped,$row) if $newlines < 2;
	}
	print '  Processing completed. ';
	my ($no_write,$write) = ($run{'infofix-zero'}) ? ('','') : ('|: ','| ');
	if (!$run{'infofix-write'}){
		say "Write following changes to $INFO_FILE by adding 'w':";
		say $line_result;
		print $no_write;
		say join("\n$no_write",@prepped);
		say $line_small;
	}
	else {
		say "Writing changes to $INFO_FILE:";
		say $line_small;
		print $write;
		say join("\n$write",@prepped);
		my $contents = join("\r\n",@prepped);
		open(my $fh, '>', $INFO_FILE) or die "failed to open for writing: $INFO_FILE";
		print $fh $contents;;
		close $fh if $fh;
		say $line_small;
		say "  Changes written to $INFO_FILE.";
	}
}

sub get_encoding {
	my $data = $_[0];
	my $string = join(' ', @$data);
	my $encoding;
	my $enc = Encode::Guess::guess_encoding($string, qw(cp1252 utf8));
	if (ref($enc) && $enc->name()){
		$encoding = $enc->name();
	}
	else {
		# true if contains any non-ascii character
		if ($string =~ /[^\x00-\x7f]/){
			# this is a legacy method, previous to Encode, but catches some UTF8 that 
			# Encode for some reason misses.
			my $pattern = '([\0-\x7F])|([\xC0-\xDF][\x80-\xBF])|';
			$pattern .= '([\xE0-\xEF][\x80-\xBF][\x80-\xBF])|';
			$pattern .= '([\xF0-\xF7][\x80-\xBF][\x80-\xBF][\x80-\xBF])|';
			$pattern .= '([\xF8-\xFB][\x80-\xBF][\x80-\xBF][\x80-\xBF][\x80-\xBF])|';
			$pattern .= '([\xFC-\xFE][\x80-\xBF][\x80-\xBF][\x80-\xBF][\x80-\xBF][\x80-\xBF])';
			## Wuld be nice if worked, but doesn't
			# if ($string =~ /[\x{100}-\x{10FFFF}]/){ 
			if ($string =~ /^($pattern)*$/){
				my $pattern2 = '([\xC0-\xC1])|([\xE0][\x80-\x9F])|([\xF0][\x80-\x8F])|';
				$pattern2 .= '([\xF8][\x80-\x87])|([\xFC][\x80-\x83])';
				if ($string !~ /$pattern2/){
					$encoding = 'utf8-2';
				}
				else {
					$encoding = 'utf8-invalid-2';
				}
			}
			# elsif (grep {/[\x{81}\x{8D}\x{8F}\x{90}\x{9D}]/} @$data){
			elsif ($string =~ /[\x82\x84-\x88\x8B\x91-\x99\x9B\xAB\xBB]/){
				$encoding = 'cp1252-2';
			}
			else {
				$encoding = 'non-ascii-2';
			}
		}
		else {
			$encoding = 'ascii-2';
		}
	}
	my $output = ($encoding) ? $encoding : 'none-detected';
	say '  Encoding: ' . $output;
	$encoding =~ s/-2$// if $encoding;
	return $encoding;
}

# try to correct windows cp-1252 special characters
# see: https://en.wikipedia.org/wiki/Windows-1252 to get more mappings
# see: http://www.i18nqa.com/debug/table-iso8859-1-vs-windows-1252.html
sub cp1252_fix {
  my ($data) = @_;
  for (my $i = 0; $i < scalar @$data;$i++){
		## nothing to do, this line is ascii only. This also covers empty lines.
		next if $data->[$i] =~ /^[\x00-\x7F]*$/;
		## if the line contains one of the translation characters, translate
		# if ($data->[$i] =~ /[\x00-\x08\x10-\x1F\x80-\x9F]/){
		if ($data->[$i] =~ /[\x82\x84-\x88\x8B\x91-\x99\x9B\xA9\xAB\xB4\xBB]/){
			say '   CF: line ' . ($i + 1);
			# Map commonly used CP-1252 to ASCII characters
			# ^M
			$data->[$i] =~ s/\x82/,/g;
			$data->[$i] =~ s/\x84/,,/g;
			$data->[$i] =~ s/\x85/.../g;
			$data->[$i] =~ s/\x86/+/g; # †
			$data->[$i] =~ s/\x87/++/g; # ‡
			$data->[$i] =~ s/\x88/^/g;
			$data->[$i] =~ s/\x8B/</g;
			$data->[$i] =~ s/[\x91\x92\xB4]/'/g;
			$data->[$i] =~ s/[\x93\x94]/"/g;
			$data->[$i] =~ s/\x95/*/g;
			$data->[$i] =~ s/\x96/-/g;
			$data->[$i] =~ s/\x97/--/g;
			$data->[$i] =~ s/\x98/~/g;
			$data->[$i] =~ s/\x99/(TM)/g;
			$data->[$i] =~ s/\x9B/>/g;
			$data->[$i] =~ s/\xA9/(C)/g;
			$data->[$i] =~ s/\xAB/<</g; # «
			$data->[$i] =~ s/\xBB/>>/g; # «
		}
		# Check for any untranslated non ASCII characters.
		if ($data->[$i] =~ /[\x80-\x9F\xA0-\xFF]/){
			say '   CF: unhandled-character: line ' . ($i + 1);
			# my $msg = "There is an unhandled special character on line: $line_nu\n  $data->[$i]";
			# main::error_handler('infofix-bad-character',$msg,0);
		}
	}
}

sub date_fix {
	my ($data) = @_;
	my ($b_dow,$dd,$dow,$mm,$regex,$yyyy);
	my $type = 2; # default to standard mm/dd/yyyy type regex
	# first day, aka: 12th July, 1972
	my $pattern1 = '\b(((0?[1-9]|[12]\d|30|31)(nd|rd|st|th)?[\s\/,-]+)?';
	# then string month
	$pattern1 .= '(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\S*?([\s\/,-]+';
	# then alternate position day: July 12th, 1972
	$pattern1 .= '(0?[1-9]|[12]\d|3[01])(nd|rd|st|th)?)?[\s\/,-]+';
	# then year: July 12, 72 or July 12, 1972 or July-12th, 1972 etc
	$pattern1 .= '((19)?[4-9]\d|(20)?[0-3]\d))\b';
	# $pattern1 = qr/$pattern1/i; # qr/../i not supported til Perl 5.014
	# numeric month day year. Can give bad results for 09-10-12 for example
	my $pattern2 = '\b((0?[1-9]|1[0-2])[\/\.-](0?[1-9]|[12]\d|3[01])[\/\.-]';
	$pattern2 .= '((19)?[4-9]\d|(20)?[0-3]\d))\b';
	$pattern2 = qr/$pattern2/;
	# numeric dd.mm.yyyy - verify via test3
	my $pattern3 = '\b((0?[1-9]|[12]\d|3[01])[\/\.-](0?[1-9]|1[0-2])[\/\.-]';
	$pattern3 .= '((19)?[4-9]\d|(20)?[0-3]\d))\b';
	$pattern3 = qr/$pattern3/;
	# yyyy mm dd
	my $pattern4 = '\b((19[4-9]\d|20[0-3]\d)[\.\/_-](0?[1-9]|1[0-2])[\._\/-]';
	$pattern4 .= '(0?[1-9]|[12]\d|3[01]))\b';
	$pattern4 = qr/$pattern4/;
	
	# only to detect for certain mm/dd/yyyy, not used currently, this is default
	my $test2 = '\b((0?[1-9]|1[0-2])[\/\.-](1[3-9]|2\d|3[01])[\/\.-]';
	$test2 .= '((19)?[4-9]\d|(20)?[0-3]\d))\b';
	$test2 = qr/$test2/;
	# only to detect for definite dd/mm/yyyy, where dd > 12.
	my $test3 = '/\b((1[3-9]|2\d|3[01])[\/\.-](0?[1-9]|1[0-2])[\/\.-]';
	$test3 .= '((19)?[4-9]\d|(20)?[0-3]\d))\b/';
	$test3 = qr/$test3/;
	# first we have to find which pattern: mm/dd/yyyy or dd/mm/yyyy
	$type = 3 if grep {$_ =~ $test3} @$data;
	for (my $i = 0; $i < scalar @$data; $i++){
		($dd,$dow,$mm,$regex,$yyyy) =  ('','','','','');
		# NOTE: be careful with (20)?3x ids, might trip month day by accident. Watch for this!
		# Will fail to update if no day found, but that's fine, that's what we want.
		# fix D month, YY, month D, YY, month-DD-YYYY
		# regex: 1: total regex; 2: container for day start; 3: day start
		# 4: optional nd/rd 5: month; 6: container for day; 7: day;
		# 8: optional nd/rd etc; 9: year; 10: 19..; 11: 20..
		if ($data->[$i] =~ /$pattern1/i){
			$regex = $1;
			$yyyy = $9;
			# say "rx: $regex 5: $5 9: $9";
			my $month = lc($5);
			if ($3){$dd = sprintf('%02d',$3)}
			elsif ($7){$dd = sprintf('%02d',$7)}
			if ($month eq 'jan'){$mm = '01';}
			elsif ($month eq 'feb'){$mm = '02';}
			elsif ($month eq 'mar'){$mm = '03';}
			elsif ($month eq 'apr'){$mm = '04';}
			elsif ($month eq 'may'){$mm = '05';}
			elsif ($month eq 'jun'){$mm = '06';}
			elsif ($month eq 'jul'){$mm = '07';}
			elsif ($month eq 'aug'){$mm = '08';}
			elsif ($month eq 'sep'){$mm = '09';}
			elsif ($month eq 'oct'){$mm = '10';}
			elsif ($month eq 'nov'){$mm = '11';}
			elsif ($month eq 'dec'){$mm = '12';}
			# say "y: $yyyy m: $mm d: $dd";
		}
		# Change m/d/yy or m-d-yy or m.d.yy to yyyy-mm-dd 09/19/54
		elsif ($type == 2 && $data->[$i] =~ $pattern2){
			$regex = $1;
			$mm = sprintf('%02d',$2);
			$dd = sprintf('%02d',$3);
			$yyyy = $4;
		}
		# Change dd/mm/yyyy etc to yyyy-mm-dd eg: 23.12.2013
		elsif ($type == 3 && $data->[$i] =~ $pattern3){
			$regex = $1;
			$mm = sprintf('%02d',$3);
			$dd = sprintf('%02d',$2);
			$yyyy = $4;
		}
		# Change YYYY[._/-]MM[._/-]DD 2022-09-23 2022_09_23 2022.09.23 2022/09/23 
		# Note: [\.\/_-]? can lead to 1966-11-29 regex eq 1966-11 and mm eq 01 dd eq 01
		elsif ($data->[$i] =~ $pattern4){
			$regex = $1;
			$yyyy = $2;
			$mm = sprintf('%02d',$3);
			$dd = sprintf('%02d',$4);
		}
		# this would miss something like 6/12/39
		if ($yyyy){
			if ($yyyy =~ /^[4-9]\d$/){
				$yyyy = '19' . $yyyy;
			}
			# future proof to the 2030s
			elsif ($yyyy =~ /^[0-3]\d$/){
				$yyyy = '20' . $yyyy;
			}
		}
		if ($mm && $dd && $yyyy){
			# say "$regex $yyyy $mm $dd";
			# only do the first date found, that would be in the top block.
			if (!$b_dow && $run{'infofix-dow'}){
				$dow = POSIX::strftime("%A", '01', '01', '01', $dd, ($mm-1), ($yyyy-1900));
				# only cut out existing dow with leading OR trailing space/-
				$data->[$i] =~ s/\(?$dow\)?[\s,-]+\s*|[\s,-]+\s*\(?$dow\)?//gi; 
				$dow = ' (' . $dow . ')';
				$b_dow = 1;
			}
			$data->[$i] =~ s/\Q$regex\E/$yyyy-$mm-$dd$dow/i;
		}
	}
}

sub open_info_file {
	my ($source) = @_;
	my $data = [];
	my ($in1) = ('  ');
	if ($source eq 'prefill'){
		chdir "$start_dir";
		chdir "$SOURCE_DIRECTORY";
		($in1) = ('');
	}
	print $in1 . "Checking $INFO_FILE... ";
	if (! -r $INFO_FILE){
		main::error_handler('open',"$INFO_FILE not readable or not present!",1) ;
	}
	else {
		open(my $fh, '<', $INFO_FILE) or 
		main::error_handler('open', "Failed to open: $INFO_FILE with error:\n:$!",1);
		chomp(@$data = <$fh>);
		close $fh if $fh;
		say 'File found and read';
		my $count = scalar @$data;
		if ($count < 10){
			main::error_handler('info-file',"$INFO_FILE only had $count lines",1);
		}
		push(@$data,'') if @$data;
	}
	return $data;
}
}

## TagList
{
package TagList;
my ($file_tags,$key_checks,$tag_file);

sub process {
	say $line_large;
	my $type = '';
	if ($run{'taglist-info'}){
		$type = ($run{'taglist-write'}) ? $INFO_FILE : 'info';
	}
	if ($run{'taglist-full'} || $run{'taglist-condensed'}){
		$tag_file = ($run{'taglist-autotag'}) ? $AUTOTAG_FILE: $TAGLIST_FILE;
		$type .= '/' if $type;
		$type .= ($run{'taglist-write'}) ? $tag_file : 'tag';
	}
	my $checks = ($run{'taglist-skip'}) ? 'Skipping' : 'Starting';
	say "$checks $type file checks in: " . main::sourcer("$SOURCE_DIRECTORY");
	process_directories();
	say "Completed taglist $type processing.";
	say $line_large;
}

sub process_directories {
	$list_type = 'dir';
	say $line_small;
	say "Checking directories...";
	@found_list = ();
	chdir "$start_dir";
	File::Find::find(\&main::wanted, @source_glob);
	say 'start dir: ', $start_dir if $dbg[4];
	my $file_type = lc($INPUT_TYPE) . ',' . uc($INPUT_TYPE) . ',';
	$file_type .= ucfirst(lc($INPUT_TYPE)); # flac,FLAC,Flac
	foreach my $item (sort { "\L$a" cmp "\L$b" } @found_list){
		my $b_valid;
		my $print_src = $item;
		$print_src =~ s|^\Q$SOURCE_DIRECTORY$path_separator\E|| if $VERBOSITY < 3;
		chdir "$start_dir";
		chdir "$item";
		if ($dbg[4]){
			say 'srcdir: ', $SOURCE_DIRECTORY;
			say 'item: ', $item;
			say 'cwd: ', Cwd::getcwd();
			system 'pwd';
		}
		say " Processing: $print_src";
		my @files = main::globber("*.{$file_type}");
		# say $file_type, "\n", Data::Dumper::Dumper \@files;
		if (grep -f, @files){
			my ($b_processed,$b_tags);
			undef $file_tags;
			undef $key_checks;
			if ($run{'taglist-skip'} || (($run{'taglist-info'} && !-e $INFO_FILE) || 
			(($run{'taglist-full'} || $run{'taglist-condensed'}) && !-e $tag_file))){
				say "  Processing $INPUT_TYPE files...";
				$b_tags = process_tags(\@files,$b_tags);
				$b_processed = 1;
			}
			if ($b_processed){
				if ($b_tags){
					if ($run{'taglist-info'}){
						if ($run{'taglist-skip'}){
							my $skip = "  Skip $INFO_FILE file check actions: file ";
							$skip .= (-e $INFO_FILE) ? "exists." : "not present.";
							say $skip;
						}
						elsif (-e $INFO_FILE){
							say "  $INFO_FILE info file present. Skipping info processing.";
						}
						if ($run{'taglist-skip'} || !-e $INFO_FILE){
							generate_info_data();
						}
						say $line_small;
					}
					if ($run{'taglist-full'} || $run{'taglist-condensed'}){
						if ($run{'taglist-skip'}){
							my $skip = "  Skip $tag_file file check actions: file ";
							$skip .= (-e $tag_file) ? "exists." : "not present.";
							say $skip;
						}
						elsif (-e $tag_file) {
							say "  $tag_file tag file present. Skipping taglist processing.";
						}
						if ($run{'taglist-skip'} || !-e $tag_file){
							generate_taglist_data();
						}
						say $line_small;
					}
				}
				else {
					say "  No tag data found in $INPUT_TYPE files.";
					say $line_small;
				}
			}
			else {
				my $exist = '';
				$exist = "$INFO_FILE" if $run{'taglist-info'} && -e $INFO_FILE;
				if (($run{'taglist-full'} || $run{'taglist-condensed'}) && -e $tag_file){
					$exist .= '/' if $exist;
					$exist .= $tag_file;
				}
				$exist .= ' present. Skipping taglist processing.';
				say '  ' . $exist;
				say $line_small;
			}
		}
		else {
			say "  No $INPUT_TYPE files";
			say $line_small;
		}
	}
}

sub process_tags {
	my ($files,$b_tags) = @_;
	foreach my $file (@$files){
		# returns single hash ref of hash ref of tag => [array ref values]
		my $tags = main::get_flac_tags('all',$file);
		$file_tags->{$file} = $tags;
		if ($tags && %$tags){
			map {$key_checks->{$file}{$_} = 1 if @{$tags->{$_}}} keys %$tags;
			$b_tags = 1 if !$b_tags;
		}
	}
	say '1. file_tags: ', Data::Dumper::Dumper $file_tags if $dbg[3];
	say '1. key_checks: ', Data::Dumper::Dumper $key_checks if $dbg[3];
	return $b_tags;
}

# note: in order to handle > 1 type per tag name, all are contained in arrays.
sub generate_info_data {
	my (@general,@info,@main,@performers,@temp,@tracks);
	my (%found,%multi,%track_data);
	say "  Generating $INFO_FILE data...";
	my $tag_checks = [qw(ALBUM ARTIST DATE DISCNUMBER PERFORMER YEAR)];
	my $multi = check_multi($tag_checks);
	my @main_checks = qw(ARTIST ALBUM ALBUMSORT COMPILATION ALBUMARTIST ENSEMBLE 
	COMPOSER CONDUCTOR OPUS DATE ORIGINALDATE YEAR VENUE LOCATION);
	# These can have > 1 item so let's treat them differently
	my @general_checks = qw(COMMENT DESCRIPTION SOURCE);
	my @info_checks = qw(GENRE MOOD BPM MEASURE INITIAL_KEY TUNING
	ORGANIZATION PRODUCER PUBLISHER MIXER REMIXER 
	URL WEBSITE CHAPTER COPYRIGHT LICENSE CONTACT
	BARCODE EAN/UPN UPC PRODUCTNUMBER CATALOGNUMBER LABELNO CATALOG CDDB
	ETREE SHNID CDTOC ENCODED-BY ENCODER ENCODING MEDIA SOURCEMEDIA);
	foreach my $file (sort keys %$file_tags){
		my ($track) = ('');
		my ($tags,$use) = ($file_tags->{$file},$key_checks->{$file});
		foreach my $check (@main_checks){
			my ($lc,$ucf) = (lc($check),ucfirst(lc($check)));
			if (!$found{$lc} && ($check ne 'ARTIST' || !$multi->{'ARTIST'}) && 
			$use->{$check}){
				push(@main,"$ucf: " . join("\n$ucf: ", @{$tags->{$check}}));
				$found{$lc} = 1;
			}
		}
		foreach my $check (@general_checks){
			my ($lc,$ucf) = (lc($check),ucfirst(lc($check)));
			if (!$found{$lc} && $use->{$check}){
				push(@general,'',"$ucf: " . join("\n$ucf: ",@{$tags->{$check}}));
				$found{$lc} = 1;
			}
		}
		foreach my $check (@info_checks){
			my ($lc,$ucf) = (lc($check),ucfirst(lc($check)));
			if (!$found{$lc} && $use->{$check}){
				push(@info,"$ucf: " . join("\n$ucf: ", @{$tags->{$check}}));
				$found{$lc} = 1;
			}
		}
		if ($use->{'DISCNUMBER'} && ($multi->{'DISCNUMBER'} ||
		($use->{'DISCTOTAL'} && $tags->{'DISCTOTAL'}[0] > 1))){
			@temp = split(/\s*\/\s*/,$tags->{'DISCNUMBER'}[0]);
			$track = main::check_int($temp[0]) . '-';
		}
		# this can be 0 or string, remember
		if ($use->{'TRACKNUMBER'} && defined $tags->{'TRACKNUMBER'}[0]){
			@temp = split(/\s*\/\s*/,$tags->{'TRACKNUMBER'}[0]);
			if ($temp[0] =~ /^\d+$/){
				$track .= sprintf('%02d',$temp[0]) . '. ';
			}
			else {
				$track .= $temp[0] . '. ';
			}
		}
		if ($use->{'TITLE'}){
			if ($multi->{'ARTIST'} && $use->{'ARTIST'} && $tags->{'ARTIST'}[0]){
				$track .= $tags->{'ARTIST'}[0] . ' - ';
			}
			$track .= $tags->{'TITLE'}[0]; 
			if ($use->{'VERSION'} && $tags->{'VERSION'}[0]){
				$track .= ' (' . $tags->{'VERSION'}[0] . ')';
			}
			if ($use->{'PART'} && $tags->{'PART'}[0]){
				$track .= ' (' . $tags->{'PART'}[0] . ')';
			}
		}
		# these are probably going to be per file/track
		if ($track){
			$track_data{$file}->{'string'} = $track;
			if ($multi->{'PERFORMER'} && $use->{'PERFORMER'} && 
			join('',@{$tags->{'PERFORMER'}}) ne join('',@performers)){
				$track_data{$file}->{'performers'} = "Performers:\n";
				$track_data{$file}->{'performers'} .= join("\n",@{$tags->{'PERFORMER'}});
				@performers = @{$tags->{'PERFORMER'}};
			}
		}
		# these will be array already, but each track can have different performers
		if (!$multi->{'PERFORMER'} && !$found{'performer'} && $use->{'PERFORMER'}){
			@performers = @{$tags->{'PERFORMER'}};
			$found{'performer'} = 1;
		}
	}
	@performers = () if $multi->{'PERFORMER'};
	say Data::Dumper::Dumper \%track_data if $dbg[20];
	if (%track_data){
		my $b_starter;
		foreach my $track (sort keys %track_data){
			if ($track_data{$track}->{'performers'}){
				push(@tracks,'',$track_data{$track}->{'performers'},'');
			}
			# we want Tracks: to come AFTER first performers list if multiperf.
			if (!$b_starter){
				push (@tracks,'') if !$track_data{$track}->{'performers'};
				push(@tracks,'Tracks:');
				$b_starter = 1;
			}
			push(@tracks,$track_data{$track}->{'string'});
		}
		push(@tracks,'',':et:');
	}
	if (!$multi->{'PERFORMER'} && @performers){
		@performers = ('','Performers:',@performers);
	}
	if (@info){
		@info = ('','Info:',@info);
	}
	my @info_data = (@main,@performers,@tracks,@general,@info);
	say "info_data: ", Data::Dumper::Dumper \@info_data if $dbg[6];
	say "2. file_tags: ", Data::Dumper::Dumper $file_tags if $dbg[22];
	say "2. key_checks: ", Data::Dumper::Dumper $key_checks if $dbg[22];
	if (@info_data){
		if (!$run{'taglist-write'}){
			say "  Here is the $INFO_FILE data (use 'w' to write file):";
			say $line_result;
			say join("\n",@info_data);
		}
		else {
			say "  Writing generated tag data to: $INFO_FILE";
			main::writer($INFO_FILE,\@info_data,1); # 1 forces utf8
		}
	}
	else {
		say "  No useful tag data for $INFO_FILE creation found.";
	}
}

sub generate_taglist_data {
	my (@contents,$disc_data,@disk_tags,@general_tags,%holder,@split,@track_tags);
	my ($b_tagblock,$b_tags);
	my ($index,$multi,$tags,$temp,$track_total,$use,$value);
	my (%tags_set);
	say "  Generating $tag_file data...";
	if (!$file_tags || !%$file_tags){
		say "  No tags found! Are the files tagged?";
		return;
	}
	my $sep = ($run{'taglist-autotag'}) ? '%:': '=' ;
	if ($run{'taglist-condensed'}){
		my $all_tags = get_tags_used();
		state ($disk_pattern,$track_pattern,$general_pattern);
		if (!$disk_pattern){
			# don't include DISCTOTAL because that only shows 1x, top
			$disk_pattern = 'DISCNUMBER|DISCSUBTITLE|TRACKTOTAL';
			$track_pattern = join('|',qw(ACCURATERIPDISCID ACCURATERIPRESULT 
			ACOUSTID_ID ACOUSTID_FINGERPRINT ISRC MUSICBRAINZ_RELEASETRACKID
			MUSICBRAINZ_TRACKID MUSICBRAINZ_WORKID PART REPLAYGAIN_TRACK_GAIN 
			REPLAYGAIN_TRACK_PEAK SUBTITLE TITLE TITLESORT TRACKNUMBER WORK VERSION));
			$general_pattern = $disk_pattern . '|DISCTOTAL|' . $track_pattern;
			$disk_pattern = qr/^($disk_pattern)$/;
			$track_pattern = qr/^($track_pattern)$/;
			$general_pattern = qr/^($general_pattern)$/;
		}
		@disk_tags = grep {$_ =~ $disk_pattern} @$all_tags;
		@track_tags = grep {$_ =~ $track_pattern} @$all_tags;
		@general_tags = grep {$_ !~ $general_pattern} @$all_tags;
		$multi = check_multi($all_tags);
		$disc_data = get_disc_data();
		my @files = sort keys %$file_tags;
		($tags,$use) = ($file_tags->{$files[0]},$key_checks->{$files[0]});
		foreach my $tag (@general_tags){
			if (!$multi->{$tag} && $use && $use->{$tag} && $tags->{$tag} && 
			defined $tags->{$tag}[0]){
				push(@contents,(map {$tag . $sep . $_} @{$tags->{$tag}}));
			}
		}
		if (@contents){
			splice(@contents,0,0,'# General shared tags for all files');
			push(@contents,'');
		}
		$index = scalar @contents;
		# these tests assume numeric values in DISCTOTAL/DISCNUMBER
		if ($disc_data->{'total-discs'} || ($use && %$use && $use->{'DISCTOTAL'} && 
		$tags->{'DISCTOTAL'}[0] > 1)){
			if ($use->{'DISCTOTAL'} && $tags->{'DISCTOTAL'}[0]){
				$temp = $tags->{'DISCTOTAL'}[0];
			}
			else {
				$temp = $disc_data->{'total-discs'};
			}
			push(@contents,"DISCTOTAL$sep" . main::check_int($temp));
			if ($disc_data->{'total-discs'} && $use->{'DISCNUMBER'}){
				@split = split(/\s*\/\s*/,$tags->{'DISCNUMBER'}[0]);
				push(@contents,'DISCNUMBER' . $sep . main::check_int($split[0]));
				$holder{'DISCNUMBER'} = $split[0];
			}
		}
		if ($use->{'DISCSUBTITLE'} && $tags->{'DISCSUBTITLE'}[0]){
			push(@contents,'DISCSUBTITLE' . $sep . $tags->{'DISCSUBTITLE'}[0]);
		}
		if (($use->{'TRACKTOTAL'} && $tags->{'TRACKTOTAL'}[0]) || 
		($disc_data->{'total-discs'} && $use->{'DISCNUMBER'} && 
		$tags->{'DISCNUMBER'}[0] && $disc_data->{$tags->{'DISCNUMBER'}[0]})){
			# correct for tracktotal used as total recording, not disc, tracks
			if ($disc_data->{'total-discs'} && $use->{'TRACKTOTAL'} && 
			$tags->{'TRACKTOTAL'}[0] && 
			$tags->{'TRACKTOTAL'}[0] == $disc_data->{'total-tracks'}){
				$value = $disc_data->{$tags->{'DISCNUMBER'}[0]};
			}
			# we only fallback to this because this can be wrong
			elsif ($use->{'TRACKTOTAL'} && $tags->{'TRACKTOTAL'}[0]){
				$value = $tags->{'TRACKTOTAL'}[0];
			}
			# does this fallback ever happen?
			else {
				$value = $disc_data->{$tags->{'DISCNUMBER'}[0]};
			}
			push(@contents,"TRACKTOTAL$sep" . main::check_int($value));
		}
		elsif (!$disc_data->{'total-discs'} && @contents){
			push(@contents,"TRACKTOTAL$sep" . scalar @files);
		}
		if (@contents){
			splice(@contents,$index,0,'# Disc data/track totals per disc');
			push(@contents,'');
		}
	}
	foreach my $file (sort keys %$file_tags){
		($tags,$use) = ($file_tags->{$file},$key_checks->{$file});
		if (!$tags || !%{$tags}){
			push(@contents,"FILE$sep$file","STATUS${sep}No Tags Found",'');
			next;
		}
		if ($run{'taglist-condensed'}){
			my (@general,@unset);
			foreach my $tag (@general_tags){
				if ($multi->{$tag}){
					if ($use->{$tag} &&
					(!$holder{$tag} || $holder{$tag} ne join('',@{$tags->{$tag}}))){
						$holder{$tag} = join('',@{$tags->{$tag}});
						push(@general,(map {$tag . $sep . $_} @{$tags->{$tag}}));
						$tags_set{$tag} = 1;
					}
					elsif (!$use->{$tag} && $tags_set{$tag}){
						push(@unset,$tag . $sep . 'UNSET');
						$tags_set{$tag} = 0;
					}
				}
			}
			if (@unset){
				push(@contents,'# Unset general tags:',@unset,'');
			}
			if (@general){
				push(@contents,
				'# Following file block general tags:',
				@general,
				'');
			}
			if ($disc_data->{'total-discs'}){ 
				@split = ($use->{'DISCNUMBER'}) ? split(/\s*\/\s*/,$tags->{'DISCNUMBER'}[0]) : ();
				if (defined $split[0] && $split[0] =~ /^\d+$/ && 
				$split[0] != $holder{'DISCNUMBER'}){
					$index = scalar @contents;
					$holder{'DISCNUMBER'} = $split[0];
					foreach my $tag (@disk_tags){
						if ($use->{$tag} && $tags->{$tag}[0]){
							if ($tag eq 'DISCNUMBER'){
								$value = main::check_int($split[0]);
							}
							elsif ($tag eq 'TRACKTOTAL'){
								# correct for tracktotal used as total recording, not disc, tracks
								if ($tags->{'TRACKTOTAL'}[0] == $disc_data->{'total-tracks'}){
									$value = $disc_data->{$tags->{'DISCNUMBER'}[0]};
								}
								else {
									$value = $tags->{'TRACKTOTAL'}[0];
								}
								$value = main::check_int($value);
							}
							else {
								$value = $tags->{$tag}[0];
							}
							push(@contents,$tag . $sep . $value);
						}
					}
					if ((!$use->{'TRACKTOTAL'} || !$tags->{'TRACKTOTAL'}[0]) && 
					$disc_data->{$holder{'DISCNUMBER'}}){
						push(@contents,'TRACKTOTAL' . $sep . main::check_int($disc_data->{$holder{'DISCNUMBER'}}));
					}
					splice(@contents,$index,0,'# Disc number/disc track total');
					push(@contents,'');
				}
			}
			foreach my $tag (@track_tags){
				# track number can be 0, so use defined test, already dumped '' values
				if ($use->{$tag} && defined $tags->{$tag}[0]){
					# known issue with musicbrainz having > 1 isrc id per filej. Sigh...
					if ($tag eq 'ISRC'){
						push(@contents,(map {$tag . $sep . $_} @{$tags->{$tag}}));
					}
					else {
						if ($tag eq 'TRACKNUMBER'){
							@split = split(/\s*\/\s*/,$tags->{'TRACKNUMBER'}[0]);
							$value = main::check_int($split[0]);
						}
						
						else {
							$value = $tags->{$tag}[0];
						}
						push(@contents,$tag . $sep . $value);
					}
				}
			}
			push(@contents,"FILE$sep$file",'') if @contents;
		}
		else {
			if (!$b_tagblock){
				push(@contents,
				'# Switch on TAGBLOCK for -A per file block tagging',
				'UNIQUE%:TAGBLOCK',
				'');
				$b_tagblock = 1;
			}
			foreach my $tag (sort keys %{$tags}){
				if ($use->{$tag}){
					push(@contents,(map {$tag . $sep . $_} @{$tags->{$tag}}));
				}
			}
			push(@contents,"FILE$sep$file",'');
		}
		$b_tags = 1 if @contents;
	}
	if ($b_tags){
		if (!$run{'taglist-write'}){
			say "  Here is the $tag_file data (use 'w' to write file):";
			say $line_result;
			say join("\n",@contents);
		}
		else {
			say "  Writing generated tag data to: $tag_file";
			main::writer($tag_file,\@contents,1); # 1 forces utf8
		}
	}
	else {
		say "  No tag data found to use! Are the files tagged?";
	}
	say "taglist contents: ", Data::Dumper::Dumper \@contents if $dbg[6];
	say "2. file_tags: ", Data::Dumper::Dumper $file_tags if $dbg[22];
	say "2. key_checks: ", Data::Dumper::Dumper $key_checks if $dbg[22];
}

sub get_disc_data {
	my $disc = {};
	$disc->{'total-discs'} = 0;
	$disc->{'total-tracks'} = 0;
	$disc->{1} = 0;
	foreach my $file (sort keys %$file_tags){
		if ($file_tags->{$file}{'DISCNUMBER'}[0]){
			# rarely seen: 1/2, 1 / 2
			my @split = split(/\s*\/\s*/,$file_tags->{$file}{'DISCNUMBER'}[0]);
			if ($split[0] =~ /^\d+$/){
				if ($split[0] > $disc->{'total-discs'}){
					$disc->{'total-discs'} = $split[0];
				}
				$disc->{$split[0]} = 0 if !$disc->{$split[0]};
				$disc->{$split[0]}++;
			}
			else {
				main::error_handler('non-integer',"DISCNUMBER: $split[0]",0);
			}
		}
		else {
			$disc->{1}++;
		}
		$disc->{'total-tracks'}++;
	}
	say 'Disc Totals: ', Data::Dumper::Dumper $disc if $dbg[7];
	return $disc;
}

sub get_tags_used {
	my $tags = [];
	my %keys;
	foreach my $file (sort keys %$file_tags){
		if (defined $file_tags->{$file}){
			map {$keys{$_} = 1} keys %{$file_tags->{$file}};
		}
	}
	$tags = [sort keys %keys];
	say 'Used Tags: ', Data::Dumper::Dumper $tags if $dbg[8];
	return $tags;
}

sub check_multi {
	my ($tags) = @_;
	my ($b_multi,$key,%multi);
	foreach my $file (sort keys %$file_tags){
		foreach my $tag (@$tags){
			if ($file_tags->{$file} && defined $file_tags->{$file}{$tag} && 
			@{$file_tags->{$file}{$tag}}){
				$key = join(';',@{$file_tags->{$file}{$tag}});
				if (!defined $multi{$tag}->{$key}){
					$multi{$tag}->{$key} = 1;
				}
				else {
					$multi{$tag}->{$key}++;
				}
			}
		}
	}
	my $file_count = scalar keys %$file_tags;
	foreach my $tag (keys %multi){
		my $key_count = scalar keys %{$multi{$tag}};
		if ($key_count > 1){
			$multi{$tag} = 1;
		}
		elsif ($key_count == 1) {
			my @keys = keys %{$multi{$tag}};
			# corner case, where only one tag content occured, but not in all files
			if ($multi{$tag}->{$keys[0]} < $file_count || 
			$multi{$tag}->{$keys[0]} < $file_count){
				$multi{$tag} = 1;
			}
			else {
				$multi{$tag} = 0;
			}
		}
	}
	say 'Multi Tags: ', Data::Dumper::Dumper \%multi if $dbg[8];
	return \%multi;
}
}

#### -------------------------------------------------------------------
#### SYNCING
#### -------------------------------------------------------------------

## SyncCollection 
{
package SyncCollection;
my $test_mode = '';

sub process {
	my (@extension_files);
	eval $print_line_heavy;
	$test_mode = 'Test mode: ' if $b_test && $VERBOSITY > 0;
	if ($VERBOSITY > 1){
		say 'Syncing: ' . main::sourcer("$DESTINATION_DIRECTORY") . ' (destination) with:';
		say '  ' . main::sourcer("$SOURCE_DIRECTORY") . ' (source)';
	}
	elsif ($VERBOSITY > 0){
		say 'Starting sync of: ' . main::sourcer("$SOURCE_DIRECTORY");
		say ' to: ' . main::sourcer("$DESTINATION_DIRECTORY");
	}
	if ($run{'source-glob'}){
		if (!@source_glob){
			say "  No source globbing matches found for pattern: $run{'source-glob'}";
		}
		else {
			say " using source globbing pattern: $run{'source-glob'}";
		}
	}
	update_directories();
	$list_type = 'file';
	foreach (@extension_list){
		eval $print_line_large;
		$extension = $_;
		@found_list = ();
		File::Find::find(\&main::wanted, @source_glob);
		# say Dumper \@found_list;
		if ($VERBOSITY > 1){
			say "PROCESSING DATA TYPE: $extension";
		}
		elsif ($VERBOSITY > 0){
			print "\n" . main::dotify("Processing $extension data type");
		}
		if (@found_list){
			update_files();
		}
		else {
			main::print_not_found('extension');
		}
	}
}

# Recreate the directory hierarchy.
sub update_directories {
	my ($b_created,$dest_dir,$dir,$result);
	$list_type = 'dir';
	@found_list = ();
	File::Find::find(\&main::wanted, @source_glob);
	eval $print_line_large;
	# say Dumper \@found_list;
	if ($VERBOSITY > 1){
		say "Checking if $self_name needs to create destination directories...";
	}
	elsif ($VERBOSITY > 0){
		print main::dotify("Updating destination directories");
	}
	foreach $dir (sort { "\L$a" cmp "\L$b" } @found_list){
		# say "\nd1:$dir";
		$result = 'UNSET';
		next if $dir eq $SOURCE_DIRECTORY || $dir eq $DESTINATION_DIRECTORY;
		$dir =~ s|^\Q$SOURCE_DIRECTORY$path_separator\E||; # strip out source path
		next if !$dir;
		$dest_dir = $DESTINATION_DIRECTORY . $path_separator . $dir;
		# say "d2:$dir";
		# check to see if the destination dir already exists
		if (!stat("$dest_dir")){
			# stat failed so create the directory
			eval $print_line_small;
			if ($VERBOSITY > 1){
				say "${test_mode}CREATING NEW DIRECTORY:\n $dest_dir";
			}
			elsif ($VERBOSITY > 0){
				print "\n${test_mode}Creating new directory: $dest_dir";
			}
			$dest_dir =~ s/\`/\'/g; # get rid of weird characters
			$result = mkdir("$dest_dir") if !$b_test;
			$b_created = 1;
			$b_dest_changed = 1;
			say "Create Directory result: $result" if $VERBOSITY > 1;
		}
	}
	main::print_not_found('dirs') if !$b_created;
}

sub update_files {
	my ($b_created);
	my ($dest_file,$file,$result,$action,$src_file) = ('','','','','');
	my ($dest_info,$dest_mod_time,$src_info, $src_mod_time) = ('',0,'',0);
	my $pm = new Parallel::ForkManager($FORK) if $b_fork;
	foreach $file (sort { "\L$a" cmp "\L$b" } @found_list){
		next if !$file;
		$dest_file = $src_file = $file;
		# say "\nIF: $src_file";
		$dest_file =~ s|^$SOURCE_DIRECTORY|$DESTINATION_DIRECTORY|;
		# $dest_file =~ s/\`/\'/g; # don't do this, will break --clean
		# $dest_file =~ s/\$/\\\$/g;
		# get rid of escape sequences in case someone used them: \40
		# $src_file =~ s/\0//g; 
		$dest_file =~ s/\0//g;
		# Figure out what the destination file would be...
		if (lc($extension) eq $INPUT_TYPE){
			$dest_file =~ s/\.$INPUT_TYPE$/\.$OUTPUT_TYPE/i;
		}
		# say "OF: $dest_file";
		($action,$result,$dest_mod_time,$src_mod_time) = ('UNSET','UNSET',0,0);
		# Now stat the destinationFile, and see if it's date is more recent
		# than that of the original file. If so, we re-encode.
		# We also re-encode if the user supplied --force
		$src_info = File::stat::stat("$src_file") or 
		  main::error_handler('stat-infile', "No $src_file: $!",1);
		$src_mod_time = $src_info->mtime if $src_info;
		$dest_info = File::stat::stat("$dest_file");
		if ($dest_info){
			$dest_mod_time = $dest_info->mtime;
			#  :: FORCE: $b_force
			# 			say "DEST_MOD: $dest_mod_time :: SRC_MOD: $src_mod_time"; 
			# 		} else {
			# 			say "NOT EXISTS: $dest_file "; 
			# 			say "P1: $file ==> \n  $dest_file"; 
		}
		# If the destination file does not exist, or the user specified force,
		# or the srcfile is more recent then the dest file, we encode/copy.
		# say "src-mt: $src_mod_time dest-mt:$dest_mod_time";
		if (!$dest_info || $b_force || ($src_mod_time > $dest_mod_time)){
			# these have to be set before the forking
			$b_created = 1;
			$b_dest_changed = 1;
			if (lc($extension) eq $INPUT_TYPE){
				$pm->start and next if $b_fork; # do the fork 
				my @returns = convert_file($src_file, $dest_file);
				$result = $returns[0];
				$action = $returns[1];
				$pm->finish if $b_fork;
			} 
			else {
				$action = 'Copy';
				$result = copy_file($file, $src_file, $dest_file);
			}
			# NOTE: for forking > 1, this will not print out for the output/input conversion files 
			say "$action result: $result" if $VERBOSITY > 1;
		} 
	}
	# wait for all the forks to finish before terminating 
	# the parent.. otherwise terminating the parent force kills 
	# all the forks 
	$pm->wait_all_children if $b_fork;
	main::print_not_found('files') if !$b_created;
}

sub copy_file {
	my ($file, $src_file, $dest_file) = @_;
	my $result = 'UNSET';
	my ($src_print,$dest_print) = ($src_file, $dest_file);
	if ($VERBOSITY < 3){
		$src_print =~ s|^\Q$SOURCE_DIRECTORY$path_separator\E||;
		$dest_print =~ s|^\Q$DESTINATION_DIRECTORY$path_separator\E||;
	}
	eval $print_line_small;
	if ($VERBOSITY > 1){
		say "${test_mode}COPY: $src_print";
		say " ==> $dest_print"; 
	}
	elsif ($VERBOSITY > 0){
		print "\n${test_mode}Copying $src_print...";
	}
	if (!$b_test){
		$result = File::Copy::copy($src_file, $dest_file) or 
			main::error_handler('cp-file', "cp failure: $src_file =>\n$dest_file\nCode: $!",1);
	}
	return $result;
}

sub convert_file { 
	my ($src_file, $dest_file) = @_;
	my ($src_print,$dest_print) = ($src_file,$dest_file);
	my ($result,$cmd,$action,$type) = (1000,'','unset',$OUTPUT_TYPE); 
	my ($encoding,$encoded) = ('Encoding','Encoded');
	my ($analyze,$tool_string);
	# escape characters for conversion processing
	$src_file = main::escape_item("$src_file");
	$dest_file = main::escape_item("$dest_file");
	if ($run{'resample'}){
		($encoding,$encoded) = ('Resampling','Resampled');
		# keys: size,duration,channels,sample-rate,bps,kbs,md5sum;
		$analyze = Analyze::process_metaflac($src_print,$src_file);
		my $rate = sprintf("%0.1f",$run{'resample'}->[1]/1000) + 0;
		$type = $run{'resample'}->[0] . ':' . $rate;
		$rate = sprintf("%0.1f",$analyze->{'sample-rate'}/1000) + 0;
		$type = "$analyze->{'bps'}:$rate to $type"
	}
	else {
		$type = "to $type";
	}
	# 	$src_file =~ s/"/\\"/g;
	# 	$dest_file =~ s/"/\\"/g;
	# 	$src_file =~ s/`/\\`/g;
	# 	$dest_file =~ s/`/\\`/g;
	# 	$src_file =~ s/\$/\\\$/g;
	# 	$dest_file =~ s/\$/\\\$/g;
	if ($VERBOSITY < 3){
		$src_print =~ s|^\Q$SOURCE_DIRECTORY$path_separator\E||;
		$dest_print =~ s|^\Q$DESTINATION_DIRECTORY$path_separator\E||;
	}
	# with forking, the printing gets messed up unless it's done 
	# just like this.
	if (!$b_fork){
		eval $print_line_small;
		if ($VERBOSITY > 1){
			say uc($encoding) . ": $src_print";
			say " ==> $dest_print"; 
		}
		elsif ($VERBOSITY > 0){
			# add line break only when file exists
			print main::dotify("\n$encoding $src_print $type");
		}
	}
	if ($OUTPUT_TYPE eq 'aac' || $OUTPUT_TYPE eq 'm4a'){
		$action = "To $OUTPUT_TYPE";
		# note: sample depth is taken usually from originating file, like shn or
		# wav, but mp3 does not seem to do this in my tests, and outputs to 24 bit
		# note that -aq / -q:a does not seem to do anything, and ffmpeg reverts to default 5 level
		# my $sample_depth = ($INPUT_TYPE eq 'mp3') ? '-sample_fmt s16' : '';
		# $sample_depth 
		# To get rid of: Could not find tag for codec h264 in stream #0 error
		# -map a:0 is needed to specify that there shouldn't be a video track. 
		my $map = ($OUTPUT_TYPE eq 'm4a') ? '-map a:0' : '';
		$cmd = qq($COMMAND_FFMPEG $silent_ffmpeg -y -i "$src_file" -b:a ${quality}k $map -c:a $codec "$dest_file");
		$tool_string = $COMMAND_FFMPEG;
	}
	elsif ($OUTPUT_TYPE eq 'flac'){
		$action = (!$run{'resample'}) ? 'To flac' : $type;
		# note: sample depth is taken usually from originating file, like shn or wav, but 
		# mp3 does not seem to do this in my tests, and outputs to 24 bit
		# note that -aq / -q:a does not seem to do anything, and ffmpeg reverts to default 5 level
		my $sample_data = '';
		if ($INPUT_TYPE eq 'mp3'){
			$sample_data = '-sample_fmt s16';
		}
		elsif ($run{'resample'}){
			$sample_data = '-sample_fmt s' . $run{'resample'}->[0] . ' -ar ' . $run{'resample'}->[1];
			# note: 0, none, is default for ffmpeg. Don't use dither when bit depth the same
			if ($run{'resample'}->[0] < 24 && $run{'resample'}->[0] != $analyze->{'bps'}){
				$action .= ":$DITHER";
				$sample_data .= " -dither_method $DITHER";
			}
		}
		$cmd = qq($COMMAND_FFMPEG $silent_ffmpeg -y -i "$src_file" -compression_level $quality $sample_data "$dest_file");
		$tool_string = $COMMAND_FFMPEG;
	}
	elsif ($OUTPUT_TYPE eq 'mp3'){
		$action = 'To mp3';
		$cmd = main::flac2mp3_cmd($src_file,$dest_file);
		$tool_string = "$COMMAND_FLAC | $COMMAND_LAME";
	}
	elsif ($OUTPUT_TYPE eq 'ogg'){
		$action = 'To ogg';
		# ffmpeg -i 'file.flac'  -map_metadata:s:a 0:g 'file.ogg'
		# my $meta_data = '--map_metadata:s:a 0:g';
		if (!$run{'ffmpeg'}){
			$cmd = qq($COMMAND_OGG $silent_flac -q $quality -o "$dest_file" "$src_file");
			$tool_string = $COMMAND_OGG;
		}
		else {
			$cmd = qq($COMMAND_FFMPEG $silent_ffmpeg -y -i "$src_file" -map_metadata:s:a 0:s -aq $quality "$dest_file");
			$tool_string = $COMMAND_FFMPEG;
		}
	}
	elsif ($OUTPUT_TYPE eq 'opus'){
		$action = 'To opus';
		$cmd = qq($COMMAND_OPUS $silent_opus --bitrate $quality "$src_file" "$dest_file");
		$tool_string = $COMMAND_OPUS;
	}
	if ($b_test){
		if ($VERBOSITY > 0){
			say '' if $VERBOSITY == 1;
			$cmd = ($dbg[5]) ? "\n$cmd": $tool_string;
			say "${test_mode}Would have run: $cmd";
		}
	}
	else {
		say "Running command:\n$cmd" if $dbg[5];
		qx($cmd);
		$result = $?;
	}
	if ($result && $result != 1000){
		my $msg = "Encoding:\n $src_file\n";
		$msg .= "  to $OUTPUT_TYPE failed with error number: $result\n";
		if ($b_fork){
			$msg .=  "Cannot stop when using Forking. ";
		}
		else {
			$msg .=  "Cannot continue, please correct issue. ";
		}
		$msg .=  "Try using -v3 to pinpoint actual error event.";
		main::error_handler('convert',$msg,1);
	}
	if ($b_fork){
		eval $print_line_small;
		if ($VERBOSITY > 1){
			say uc($encoded) . ": $src_print";
			say " ==> $dest_print"; 
			say "$action result: $result";
		}
		elsif ($VERBOSITY > 0){
			# add line break only when file exists
			print main::dotify("\n$encoded $src_print $type");
		}
	}
	# this return only works with non forking
	return ($result,$action); 
}
}

#### -------------------------------------------------------------------
#### TAGGING TOOLS
#### -------------------------------------------------------------------

# Originally added: Odd @2011-03-23 01:52:31
# Revised many times since, full refactor 2022-12-30.
sub flac2mp3_cmd {
	my ($i_file, $o_file) = @_;
	my $tags = get_flac_tags('flac-mp3',$i_file);
	my $lame_params = '';
	foreach (keys %$tags){
		$tags->{$_} = escape_item($tags->{$_});
	}
	# say Dumper $tags;
	state %mp3_tags; # cannot assign values at declare in earlier perls.
	%mp3_tags = (
	'ALBUM' => '--tl "',
	'ALBUMARTIST' => '--tv "TPE2=',
	'ALBUMSORT' => '--tv "TSOA=',
	'ALBUMSORT' => '--tv "TPE2=',
	'ARTIST' => '--ta "',
	'ARTISTSORT' => '--tv "TSOP=',
	'COMMENT' => '--tv "COMM=',
	'COMPILATION' => '--tv "TCMP=',
	'COMPOSER' => '--tv "TCOM=',
	'CONDUCTOR' => '--tv "TPE3=',
	'COPYRIGHT' => '--tv "TCOP=',
	# DATE: mp2tag.de: : v2.3: TDAT; v2.4: TXXX:DATE
	# hydrogenaudio: v2.2: TYE+TDA(+TIM) v2.3: TYER+TDAT(+TIME[22]) v2.4: TDRC 
	# 'DATE' => '--tv "TXXX:DATE=', # this is id3v2.4 syntax
	'DESCRIPTION' => '--tv "TIT3=',
	'DISCSUBTITLE' => '--tv "TSST=',
	'GENRE' => '--tg "',
	'ISRC' => '--tv "TSRC=',
	'LABEL' => '--tv "TPUB=',
	'MEDIA' => '--tv "TMED=',
	'MOOD' => '-tv "TMOO=',
	'MOVEMENTNAME' => '-tv "MVNM=',
	'ORGANIZATION' => '--tv "TPUB=',
	'PUBLISHER' => '--tv "TPUB=',
	'REMIXER' => '--tv "TPE4=',
	'SOURCEMEDIA' => '--tv "TMED=',
	'SUBTITLE' => '--tv "TIT3=',
	'TITLE' => '--tt "',
	'TITLESORT' => '--tv "TSOT=',
	'YEAR' => '--ty "',
	) if !%mp3_tags;
	# start building MP3 id3v2 tags
	foreach my $key (keys %mp3_tags){
		if ($tags->{$key}){
			$lame_params .= ' ' . $mp3_tags{$key} . $tags->{$key} . '"'; 
		}
	}
	# We will construct the YEAR if no YEAR and YYYY found in DATE.
	if (!$tags->{'YEAR'} && $tags->{'DATE'} && 
	$tags->{'DATE'} =~ /\b((1[6-9]|20)\d{2})\b/){
		$lame_params .= ' --ty "' . $1 . '"';
	}
	# complex combinations
	if ($tags->{'DISKNUMBER'} && $tags->{'DISKTOTAL'}){
		$lame_params .= '--tv "TPOS= ' . $tags->{'DISKNUMBER'} . '/' . $tags->{'DISKTOTAL'} . '"';
	}
	if ($tags->{'TRACKNUMBER'}){
		# Providing just track number creates v1.1 tag, providing total forces v2.0
		if ($tags->{'TRACKTOTAL'}){
			$tags->{'TRACKNUMBER'} .= '/' . $tags->{'TRACKTOTAL'};
		}
		$lame_params .= ' --tn "' . $tags->{'TRACKNUMBER'} . '"';
	}
	$lame_params .= ' - "' . $o_file . '"';
	my $cmd = "$COMMAND_FLAC $silent_flac -d -c \"$i_file\" | ";
	$cmd .= "$COMMAND_LAME $silent_lame -h -V $quality $lame_params";
	# say $cmd;
	return $cmd;
}

# args: $type: all|flac-mp3|replaygain; $i_file: file to work on
# Added: Odd @2011-03-23 01:52:17
# Updated routinely afterwards.
# For this function to work reliably, it should be passed tag queries in the order of:
# artist, album, title, genre, date, tracknumber
sub get_flac_tags {
	my ($type,$i_file) = @_;
	my (%tags,@working);
	state @tags_working; # cannot assign values at declare in earlier perls.
	$i_file = escape_item($i_file);
	my $cmd = "$COMMAND_METAFLAC --no-utf8-convert \"$i_file\" ";
	if ($type ne 'all'){
		# replaygain/flac to mp3 tags will never happen in same action: sync, autotag
		if ($type eq 'flac-mp3'){
			if (!@tags_working){
				@tags_working = qw(ALBUM ALBUMARTIST ARTIST ARTISTSORT
				COMMENT COMPILATION COMPOSER CONDUCTOR COPYRIGHT 
				DATE DESCRIPTION DISKNUMBER DISCSUBTITLE DISKTOTAL 
				GENRE ISRC LABEL MEDIA MOOD MOVEMENTNAME ORGANIZATION PUBLISHER REMIXER 
				SOURCEMEDIA SUBTITLE TITLE TITLESORT TRACKNUMBER TRACKTOTAL YEAR);
			}
		}
		elsif ($type eq 'replaygain'){
			if (!@tags_working){
				@tags_working = qw(REPLAYGAIN_TRACK_PEAK REPLAYGAIN_TRACK_GAIN 
				REPLAYGAIN_ALBUM_PEAK REPLAYGAIN_ALBUM_GAIN 
				WAVEFORMATEXTENSIBLE_CHANNEL_MASK);
			}
		}
		foreach (@tags_working){
			$cmd .= "--show-tag=\"$_\" ";
		}
	}
	else {
		 $cmd .= "--export-tags-to=- ";
	}
	my @orig_tags = qx($cmd);
	chomp @orig_tags;
	say 'Raw tags from export: ', Dumper \@orig_tags if $dbg[1];
	push(@orig_tags,'--END--');
	foreach my $tag (@orig_tags){
		# vorbis specs: tag names cannot contain = (0x3D),~ (0x7E)
		# note that in theory, you can have a < 3 character tag name, but too unsafe
		if ($tag =~ /^[^=~]{3,40}=/ || $tag eq '--END--'){
			if ($working[0]){
				# keep in tag data, but it gets cleared out in -A preprocessor
				# $working[1] =~ s/[\s]+$//;
				# $working[1] =~ s/\n{3,}/\n\n/;
				if ($type ne 'all'){
					$tags{$working[0]} = $working[1];
				}
				else {
					# handle stupid multiple entries with different tag name variants that 
					# are standardized below. Yes, looking at musicbrainz...
					if (!grep {$_ eq $working[1]} @{$tags{$working[0]}}){
						push(@{$tags{$working[0]}},$working[1]);
					}
				}
			}
			last if $tag eq '--END--';
			# make all tag names uppercase to make following processing easier
			$tag =~ s/^([^=~]{3,})=/\U$1=/;
			if ($type eq 'all'){
				# map some inconsistent tag names here
				$tag =~ s/^ALBUM[\s_-]ARTIST=/ALBUMARTIST=/;
				$tag =~ s/^DIS[CK][\s_-]?(#|NUMBER)?=/DISCNUMBER=/;
				$tag =~ s/^(DIS[CK](C|[\s_-]?TOTAL)|TOTAL[\s_-]?DIS[CK]S)=/DISCTOTAL=/;
				$tag =~ s/^SOURCE[\s_-]?MED(IA|UM)=/SOURCEMEDIA=/;
				$tag =~ s/^TRACK[\s_-]?NUM(BER)?=/TRACKNUMBER=/;
				$tag =~ s/^(TOTAL[\s_-]?TRACKS|TRACK[\s_-]?TOTAL)=/TRACKTOTAL=/;
			}
			@working = split(/=/,$tag,2);
			# we don't want empty tags, but remember, valid value can be 0
			if (!defined $working[1] || $working[1] eq ''){
				@working = ();
			}
		}
		else {
			$working[1] .= "\n" . $tag if defined $working[1];
		}
	}
	if ($type ne 'all'){
		# create missing hash keys
		foreach (@tags){
			$tags{$_} = '' if !defined $tags{$_};
		}
	}
	say 'Processed raw tag data: ', Dumper \%tags if $dbg[1];
	return \%tags;
}

#########################################################################
### PROGRAM TOOLS ###
#########################################################################

#### -------------------------------------------------------------------
#### UTILITIES
#### -------------------------------------------------------------------

# $1 - Perl module to check
sub check_module {
	my ($module) = @_;
	my $b_present = 0;
	eval "require $module";
	$b_present = 1 if !$@;
	return $b_present;
}

# $1 - tests that expected value is integer, returns error if not
sub check_int {
	return if !defined $_[0];
	return int($_[0]) if $_[0] =~ /^\d+$/;
	error_handler('non-integer',"non-integer value: $_[0]",0);
	return $_[0];
}

sub dotify {
	my $string = $_[0];
	while (length($string) < 53){
		$string .= '.';
	}
	$string .= ' ';
	return $string;
}

sub escape_item {
	my ($file) = @_;
	$file =~ s/"/\\"/g;
	$file =~ s/`/\\`/g;
	$file =~ s/\$/\\\$/g;
	# note: we don't need to escape @, %, they don't work like $.
	# $file =~ s/\@/\\\@/g;
	# $file =~ s/\%/\\\%/g;
	# say "escape: $file";
	return $file;
}

# args: 1 - string value to glob
# Note: because of spaces and other strange user file paths, need to use 
# bsd glob, which returns space agnostic globs, as you'd expect. 
sub globber {
	# say $_[0];
	my @files = glob($_[0]);
	# say Data::Dumper::Dumper \@files;
	return @files;
	# note: creates a fh with global scope? for some weird reason, which will then 
	# be returned each call once set even though that's not what $_[0] is running.
	# return glob($_[0]); 
}

# args: 1 size in KiB
sub print_size {
	my ($size) = $_[0];
	return 'Error' unless defined $size;
	my ($k,$m,$g) = ($VERBOSITY < 2) ? (0,1,2): (1,2,3);
	my ($unit);
	if ($size < 1){
		$unit = 'B';
		$size = sprintf("%.0f", ($size * 1024));
	}
	elsif ($size < 1024){
		$unit = 'KiB';
		$size = sprintf("%.${k}f",$size);
	}
	elsif ($size < 1024**3) {
		$unit = 'MiB';
		$size = sprintf("%.${m}f",$size/1024);
	}
	else {
		$unit = 'GiB';
		$size = sprintf("%.${g}f",$size/1024**3);
	}
	return "$size $unit";
}

# args: 1 - time in seconds
sub print_time {
	my ($seconds) = $_[0];
	return 0 unless $seconds;
	my ($hours,$minutes,$secs,$cents,$time) = (0,0,0,0,'');
	if ($seconds > 60**2){
		$hours = int($seconds/60**2);
		$seconds -= ($hours*60**2);
	}
	if ($seconds > 60){
		$minutes = int($seconds/60);
		$seconds -= ($minutes*60);
	}
	$cents = sprintf("%.2f",($seconds - int($seconds)));
	if ($hours){
		$minutes = sprintf("%02d",$minutes);
		$time = "$hours:";
	}
	$seconds = sprintf("%02d",int($seconds));
	$cents = sprintf("%02d",$cents*100);
	$time .= "$minutes:$seconds.$cents";
	return $time;
}

# arg: 1 - full file path, returns array of file lines.
# 2 - 'strip': strip and clean data
# note: chomp has to chomp the entire action, not just <$fh>
sub reader {
	my ($file,$strip) = @_;
	open(my $fh, '<', $file) or error_handler('open', $file, $!);
	chomp(my @rows = <$fh>);
	if ($strip && @rows){
		@rows = grep {!/^\s*#|^\s*$/} @rows;
		@rows = map {s/^\s+|\s+$//g; $_} @rows if @rows;
	}
	return @rows;
}

# returns either printed source, or in case of ., return full path
sub sourcer {
	($_[0] eq '.') ? Cwd::getcwd() : $_[0];
}

sub trimmer {
	my ($str) = @_;
	$str =~ s/^\s+|\s+$//g; 
	return $str;
}

sub unescape_item {
	my ($file) = @_;
	$file =~ s/\\\"/"/g;
	$file =~ s/\\\`/`/g;
	$file =~ s/\\\$/\$/g;
	# note: we don't need to unescape @, %, they don't work like $.
	# $file =~ s/\\\@/\@/g;
	# $file =~ s/\\\%/\%/g;
	# say "unescape: $file";
	return $file;
}

# NOTE: File::Find will not follow symbolic links
sub wanted {
	state @pruned;
	state $b_first;
	return if -l; # skip symbolic links
	# https://www.perlmonks.org/?node_id=358502 more on prune and exclude diretortories
	# make test more general, only prune from results when actually not readable
	# since on rare occasions, the non readable file might be a file, not a directory, 
	# no test for -d
	# if ($File::Find::name =~ m/\Qlost+found\E/){
	# say $File::Find::name;
	# run the recursion level tests
	if (-d $File::Find::name){
		my $b_skip = 0;
		if ($recurse > -1){
			my $working = $File::Find::name;
			$working =~ s|^\Q$SOURCE_DIRECTORY\E$path_separator?||;
			# my $count = $working =~ tr/$path_separator//;
			my $count = scalar(split (m|$path_separator|, $working));
			$b_skip = 1 if $count > $recurse;
			# say "$working :: $path_separator :: re: $recurse c: $count";
		}
		if (! -r $File::Find::name || $b_skip){
			if (!grep { $_ eq $File::Find::name } @pruned){
				say $line_small if !$b_first;
				$b_first = 1;
				my $message = ($b_skip) ? 'recursion': 'unreadable';
				say "SKIPPING ($message):\n $File::Find::name";
				# say $line_small;
				push @pruned, $File::Find::name;
			}
			$File::Find::prune = 1;
			return;
		}
	}
	if ($list_type eq 'dir' || $list_type eq 'file'){
		return if $list_type eq 'dir' && ! -d; # skip files
		return if $list_type eq 'file' &&  -d; # skip directories
		return if $File::Find::name =~ m/^\Q$DESTINATION_DIRECTORY\E/;
		# skip any dot files/directories
		if (!$run{'dot'} && $File::Find::name =~ m/\/\./){
			$File::Find::prune = 1;
			return; 
		}
		# handle excludes
		if ($run{'sync'} && (
		 (@excludes && (grep {$File::Find::name =~ /\Q$_\E/} @excludes)) ||
		 ($list_type eq 'dir' && @excludes_stripped && 
		 (grep {$File::Find::name =~ /\Q$_\E$/} @excludes_stripped)))
		){
			$File::Find::prune = 1;
			return;
		}
		if ($list_type eq 'file'){
			# extension can be either a full file name, or extension
			return if $File::Find::name !~ /(\.|\/)$extension$/i;
		}
	}
	elsif ($list_type eq 'dir-clean' || $list_type eq 'file-clean'){
		return if $list_type eq 'dir-clean' && ! -d; # skip files
		return if $list_type eq 'file-clean' &&  -d; # skip directories
		my $working = $File::Find::name;
		$working =~ s|^\Q$DESTINATION_DIRECTORY\E|$SOURCE_DIRECTORY|;
		# we want to exclude files that are themselves contained in a sym linked directory
		if ($list_type eq 'file-clean'){
			return if -l File::Basename::dirname($working);
		}
		$working =~ s/\.$OUTPUT_TYPE$/\.$INPUT_TYPE/i if $list_type eq 'file-clean';
		# say '1: ', "$File::Find::name\n$working";
		# return if: 1. file/directory not found in destination 
		# 2. string in exclude array found in destination path
		# 3. if directory and  string found at end of path after trimming off trailing slash.
		# NOTE: if we modify $_, that modification goes back into the array, who would have thunk?
		return unless ! -e $working || (@excludes && 
		(grep {$File::Find::name =~ /\Q$_\E/} @excludes) ||
		($list_type eq 'dir-clean' && @excludes_stripped &&
		(grep {$File::Find::name =~ /\Q$_\E$/} @excludes_stripped)));
		# say '2: ', "$File::Find::name"; print "$_ ::";
	}
	# say $File::Find::name;
	push (@found_list, $File::Find::name);
	return;
}

# arg: 1 file full  path to write to; 2 - arrayof data to write. 
# note: turning off strict refs so we can pass it a scalar or an array reference.
sub writer {
	my ($path, $ref_content,$b_utf8) = @_;
	my ($content);
	no strict 'refs';
	my $utf8 = ($b_utf8) ? ':utf8' : '';
	# say Dumper $ref_content;
	if (ref $ref_content eq 'ARRAY'){
		$content = join "\n", @$ref_content or die "failed with error $!";
	}
	else {
		$content = scalar $ref_content;
	}
	open(my $fh, ">$utf8", $path) or 
	 error_handler('open',"$path failed to open for writing.\nMessage: $!",1);
	print $fh $content;
	close $fh;
}

#### -------------------------------------------------------------------
#### SELF UPDATER
#### -------------------------------------------------------------------

## SelfUpdater
{
package SelfUpdater;
my $self_source = "$SELF_DIRECTORY$path_separator$self_name";
my $man_source = "$MAN_DIRECTORY$path_separator$self_name.1";

sub update {
	say $line_large;
	say "Starting $self_name self updater.";
	validate();
	grab();
	say "Completed self updates.";
	say $line_large;
	exit 0;
}

sub grab {
	my ($cmd,@content);
	my $no_ssl = ($run{'no-ssl'}) ? '--insecure': '';
	my($source,$url) = ('default redirect to repo','https:/smxi.org/');
	if ($run{'update-type'} && $run{'update-type'} == 3){
		$url = 'https:/smxi.org/ac/';
		$source = 'alternate';
	}
	say "Downloading $self_name using $source location: $url";
	if ($no_ssl){
		say "- SSL certificate checks have been disabled.";
	}
	$cmd = "$COMMAND_CURL $no_ssl -L $url$self_name";
	say $line_small;
	@content = qx($cmd);
	if ($? > 0){
		main::error_handler('self-updater',"Self update error: curl returns: $?.",1);
	}
	chomp(@content);
	say $line_small;
	print "Verifying $self_name data... ";
	if (!grep {/###\*\*EOF\*\*###/} @content){
		main::error_handler('self-updater',"Self update error: Download data corrupted.",1);
	}
	else {
		print "Verified.\nWriting to $self_name location... ";
		main::writer($self_source,\@content);
		say 'File updated';
	}
	say "Downloading $self_name.1 man page.";
	$cmd = "$COMMAND_CURL $no_ssl -L $url$self_name.1";
	say $line_small;
	@content = qx($cmd);
	if ($? > 0){
		main::error_handler('self-updater',"Man update error: curl returns: $?.",1);
	}
	chomp(@content);
	say $line_small;
	print "Verifying $self_name.1 data... ";
	# have to keep legacy test for a while
	if (!grep {/\.\\" EOF/} @content){
		main::error_handler('self-updater',"Man update error: Download data corrupted.",1);
	}
	else {
		print "Verified.\nWriting to $self_name.1 location... ";
		main::writer($man_source,\@content);
		say 'File updated';
	}
}

sub validate {
	print 'Validating data... ';
	if (!$ALLOW_UPDATES){
		main::error_handler('self-updater',"Self Updater has been disabled by package maintainer.",1);
	}
	if (!-e $self_source){
		main::error_handler('self-updater',"Only updating $self_name at $self_source supported.",1);
	}
	if (!-e $man_source){
		main::error_handler('self-updater',"Only updating man at $man_source supported.",1);
	}
	if (!-x $COMMAND_CURL){
		main::error_handler('self-updater',"Downloader: $COMMAND_CURL missing or not executable.",1);
	}
	if (!-w $self_source){
		main::error_handler('self-updater',"$self_source is not writeable. Need superuser rights?",1);
	}
	if (!-w $man_source){
		main::error_handler('self-updater',"$man_source is not writeable. Need superuser rights?.",1);
	}
	say 'Valid';
}
}

#### -------------------------------------------------------------------
#### VALIDATION - ERROR HANDLING
#### -------------------------------------------------------------------

## Args: 1: error id; 2: error message; 3: extt 0/1 [f/t]
sub error_handler {
	my ($error,$message,$b_exit) = @_;
	my ($br,$error_text) = ("\n",'');
	state $b_valid = 1;
	state $error_no = 0;
	# validation error block:
	if ($error eq 'unsupported-type'){$error_no = 2;$b_valid=0;}
	elsif ($error eq 'clean-dest-src'){$error_no = 1;$b_valid=0;}
	elsif ($error eq 'dest-src-2-dot'){$error_no = 9;$b_valid=0;}
	elsif ($error eq 'dest-dir'){$error_no = 1;$b_valid=0;}
	elsif ($error eq 'dest-eq-src-dir'){$error_no = 9;$b_valid=0;$br=''}
	elsif ($error eq 'missing-app'){$error_no = 3;$b_valid=0;}
	elsif ($error eq 'quality-invalid'){$error_no = 4;$b_valid=0;}
	elsif ($error eq 'bad-level'){$error_no = 5;$b_valid=0;}
	elsif ($error eq 'bad-fork'){$error_no = 9;$b_valid=0;}
	elsif ($error eq 'bad-nlink'){$error_no = 9;$b_valid=0;}
	elsif ($error eq 'bad-resample'){$error_no = 9;$b_valid=0;}
	elsif ($error eq 'bad-exclude-file'){$error_no = 19;$b_valid=0;}
	elsif ($error eq 'prefill-error'){$error_no = 32;$b_valid=0;}
	# end validation error block
	elsif ($error eq 'self-updater'){$error_no = 17;}
	elsif ($error eq 'open'){$error_no = 14;}
	elsif ($error eq 'file-exists'){$error_no = 16;}
	elsif ($error eq 'file-missing'){$error_no = 15;}
	elsif ($error eq 'stat-infile'){$error_no = 6;}
	elsif ($error eq 'missing-arg'){$error_no = 7;
		$message = "Option: $message";$br=''}
	elsif ($error eq 'application-error'){$error_no = 11;}
	elsif ($error eq 'checksum-delete'){$error_no = 12;}
	elsif ($error eq 'invalid-options'){$error_no = 13;$br=''}
	elsif ($error eq 'unsupported-option'){$error_no = 8;$br=''}
	elsif ($error eq 'autotag-multi'){$error_no = 18;}
	elsif ($error eq 'track-counts'){$error_no = 30;}
	elsif ($error eq 'analyze'){$error_no = 31;}
	elsif ($error eq 'convert'){$error_no = 32;}
	elsif ($error eq 'multiartist-split'){$error_no = 33;}
	elsif ($error eq 'infofix-bad-character'){$error_no = 34;}
	elsif ($error eq 'non-integer'){$error_no = 35;;$br=''}
	if ($error eq 'validation-errors'){
		if (!$b_valid){
			$message = "${br}Failed pretests. Please correct the listed errors.";
		}
		else {
			$message = "Pretests passed. Continuing." if $VERBOSITY > 0;
			$b_exit = 0;
		}
	}
	else {
		$message = "${br}Error $error_no: $message";
	}
	say "$message" if $message;
	exit $error_no if $b_exit;
}

## Validation 
{
package Validation;
my ($b_valid_in_out);

sub run {
	start_text();
	check_src_dest_directories();
	check_in_out_types();
	if ($b_valid_in_out){
		check_quality() if $b_check_out;
		check_application_paths();
	}
	check_codecs();
	check_excludes();
	check_fork();
	check_resample() if $run{'resample'};
	check_nlink();
	check_verbosity();
	main::error_handler('validation-errors', '',1);
}

sub start_text {
	if ($VERBOSITY > 0){
		eval $print_line_heavy;
		say "Running $self_name pretests.";
	}
}

sub check_application_paths {
	my $error_message = '';
	my @app_paths; 
	if ($VERBOSITY > 0){
		print main::dotify("Checking required tools paths");
	}
	if ($b_check_out){
		if ($OUTPUT_TYPE =~ /^(aac|flac|m4a)$/i){
			if (!grep {$_ eq $COMMAND_FFMPEG} @app_paths){
				push(@app_paths,$COMMAND_FFMPEG);
			}
			if (! -x "$COMMAND_FFMPEG"){
				$error_message .= "\n Encoding application not available: $COMMAND_FFMPEG";
			}
			if ($run{'resample'}){
				if (!grep {$_ eq $COMMAND_METAFLAC} @app_paths){
					push(@app_paths,$COMMAND_METAFLAC);
				}
				if (! -x "$COMMAND_METAFLAC"){
					$error_message .= "\n Sample rate application not available: $COMMAND_METAFLAC";
				}
			}
		}
		elsif ($OUTPUT_TYPE =~ /^(ogg|opus)$/i){
			if (!$run{'ffmpeg'}){
				if (lc($OUTPUT_TYPE) eq 'ogg'){
					if (!grep {$_ eq $COMMAND_OGG} @app_paths){
						push(@app_paths,$COMMAND_OGG);
					}
					if (! -x "$COMMAND_OGG"){
							$error_message .= "\n Encoding application not available: $COMMAND_OGG";
					}
				}
				elsif (lc($OUTPUT_TYPE) eq 'opus'){
					if (!grep {$_ eq $COMMAND_OPUS} @app_paths){
						push(@app_paths,$COMMAND_OPUS);
					}
					if (!$run{'ffmpeg'}){
						if (! -x "$COMMAND_OPUS"){
							$error_message .= "\n Encoding application not available: $COMMAND_OPUS";
						}
					}
				}
			}
			else {
				if (!grep {$_ eq $COMMAND_FFMPEG} @app_paths){
					push(@app_paths,$COMMAND_FFMPEG);
				}
				if (! -x "$COMMAND_FFMPEG"){
					$error_message .= "\n Encoding application not available: $COMMAND_FFMPEG";
				}
			}
		}
		elsif (lc($OUTPUT_TYPE) eq 'mp3'){
			if (!grep {$_ eq $COMMAND_LAME} @app_paths){
				push(@app_paths,$COMMAND_LAME);
			}
			if (! -x "$COMMAND_LAME"){
				$error_message .= "\n Encoding application not available: $COMMAND_LAME";
			}
			if (!grep {$_ eq $COMMAND_FLAC} @app_paths){
				push(@app_paths,$COMMAND_FLAC);
			}
			if (! -x "$COMMAND_FLAC"){
				$error_message .= "\n Input processor $COMMAND_FLAC needed by lame ";
				$error_message .= "not available.";
			}
			if (!grep {$_ eq $COMMAND_METAFLAC} @app_paths){
				push(@app_paths,$COMMAND_METAFLAC);
			}
			# Added: Odd @2011-03-23 01:55:28
			if (! -x "$COMMAND_METAFLAC"){
				$error_message .= "\n $COMMAND_METAFLAC not found. Required to copy ";
				$error_message .= "ID3 tags from Flac to MP3.";
			}
		}
	}
	else {
		# Note: -AK but not -AV
		if ($run{'checksum'} || $run{'checksum-verify'}){
			if (!$run{'no-md5'}){
				if (!grep {$_ eq $COMMAND_MD5} @app_paths){
					push(@app_paths,$COMMAND_MD5);
				}
				# Added: Odd @2011-03-23 01:55:28
				if (! -x "$COMMAND_MD5"){
					$error_message .= "\n $COMMAND_MD5 not found. Required to generate ";
					$error_message .= "md5 checksum files (-D,-K,-V).";
				}
			}
			if ($run{'checksum'} && !$run{'no-ffp'}){
				if (!grep {$_ eq $COMMAND_METAFLAC} @app_paths){
					push(@app_paths,$COMMAND_METAFLAC);
				}
				# Added: Odd @2011-03-23 01:55:28
				if (! -x "$COMMAND_METAFLAC"){
					$error_message .= "\n $COMMAND_METAFLAC not found. Required to generate ";
					$error_message .= "ffp checksum files (-D,-K).";
				}
			}
			if (($run{'checksum-verify'} || $run{'infofix-verify'}) && 
			!$run{'no-ffp'}){
				if (!grep {$_ eq $COMMAND_FLAC} @app_paths){
					push(@app_paths,$COMMAND_FLAC);
				}
				if (! -x "$COMMAND_FLAC"){
					$error_message .= "\n $COMMAND_FLAC not found. Required to verify ";
					$error_message .= "$INPUT_TYPE files (-V,-Xv).";
				}
			}
		}
		if ($run{'tagger'} || $run{'taglist'}){
			if (!grep {$_ eq $COMMAND_METAFLAC} @app_paths){
				push(@app_paths,$COMMAND_METAFLAC);
			}
			# Added: Odd @2011-03-23 01:55:28
			if (! -x "$COMMAND_METAFLAC"){
				$error_message .= "\n $COMMAND_METAFLAC not found. Required for -A, -L.";
			}
		}
		if ($run{'prefill'} && $PREFILL_TAG){
			if ($PREFILL_TAG !~ /%:/){
				$error_message .= "\n Bad value in PREFILL_TAG: $PREFILL_TAG. ";
				$error_message .= "Missing '%:'. ";
			}
		}
		if ($run{'infofix-quality'} || $run{'analyze'}){
			if (!$run{'ffprobe'} && lc($INPUT_TYPE) eq 'flac') {
				if (!grep {$_ eq $COMMAND_METAFLAC} @app_paths){
					push(@app_paths,$COMMAND_METAFLAC);
				}
				# Added: Odd @2011-03-23 01:55:28
				if (! -x "$COMMAND_METAFLAC"){
					$error_message .= "\n $COMMAND_METAFLAC not found. Required for ";
					$error_message .= "analyzing $INPUT_TYPE files (-Z/-Xq).";
				}
			}
			else {
				if (!grep {$_ eq $COMMAND_FFPROBE} @app_paths){
					push(@app_paths,$COMMAND_FFPROBE);
				}
				# Added: Odd @2011-03-23 01:55:28
				if (! -x "$COMMAND_FFPROBE"){
					$error_message .= "\n $COMMAND_FFPROBE not found. Required for ";
					$error_message .= "analyzing $INPUT_TYPE files (-Z).";
				}
			}
		}
		if ($run{'infofix-title'} || $run{'infofix-upper'}){
			if (!grep {$_ eq 'Module Text::Autoformat'} @app_paths){
				push(@app_paths,'Module Text::Autoformat');
			}
			if (!main::check_module('Text::Autoformat')){
				$error_message .= "\n Perl module Text::Autoformat not found. Required ";
				$error_message .= "for -Xt/-Xu.";
			}
			else {
				import Text::Autoformat;
			}
		}
		if ($run{'infofix-character'}){
			if (!grep {$_ eq 'Module Encode::Guess'} @app_paths){
				push(@app_paths,'Module Encode::Guess');
			}
			if (!main::check_module('Encode::Guess')){
				$error_message .= "\n Perl module Encode::Guess not found. Required ";
				$error_message .= "for -Xc.";
			}
			else {
				import Encode::Guess;
			}
		}
	}
	if ($error_message){
		main::error_handler('missing-app',$error_message,0);
	}
	elsif ($VERBOSITY > 0){
		my ($join,$joiner) = ($VERBOSITY > 1) ? ("\n ","\n "): (' ','; ');
		say "Available:$join" .  join($joiner,@app_paths);
	}
}

sub check_codecs {
	my $error_message = '';
	if ($VERBOSITY > 0){
		eval $print_line_large;
		print main::dotify("Checking codecs");
	}
	if ($codec && $OUTPUT_TYPE !~ /^(aac|m4a)$/i){
		$error_message .= "--codec only is supported for output types aac/m4a.\n";
	}
	if ($OUTPUT_TYPE =~ /^(aac|m4a)$/i){
		if (!$codec){
			$codec = $CODEC_AAC;
		}
		if (!$codec || ($codec && $codec !~ /^(libfdk_aac|aac)$/)){
			$error_message .= "Only libfdk_aac or aac codecs supported for $OUTPUT_TYPE.\n";
		}
	}
	if ($error_message){
		main::error_handler('unsupported-type',$error_message,0);
	}
	else {
		if ($VERBOSITY > 0){
			say 'Valid';
		}
	}
}

sub check_excludes {
	my $error_message = '';
	if ($EXCLUDE){
		if ($VERBOSITY > 0){
			print main::dotify("Checking EXCLUDE data");
		}
		if ($EXCLUDE =~ /\Q$EXCLUDE_BASE\E/){
			if (! -e $EXCLUDE){
				$error_message .= "\n You must provide a valid exclude file path. ";
				$error_message .= "You used: $EXCLUDE";
			}
		}
		else {
			if ($EXCLUDE =~ /\Q^^^^\E/ || $EXCLUDE =~ /\Q^^\E$/){
				$error_message .= "\n You have an empty value in your EXCLUDE data. ";
				$error_message .= "You used: $EXCLUDE";
			}
		}
		if ($error_message){
			main::error_handler('bad-exclude-file',$error_message,0);
		}
		elsif ($VERBOSITY > 0){
			say "Supported: $FORK";
		}
	}
}

sub check_fork {
	my $error_message = '';
	if ($VERBOSITY > 0){
		print main::dotify("Checking FORK value");
	}
	if ($FORK !~ m/^[0-9]+$/){
		$error_message .= "\n FORK requires value: 0 or more. ";
		$error_message .= "You used: $FORK";
	}
	# note: tests show fork == 1 slower than no forking!!
	if (!$error_message && $FORK > 0){
		if (!main::check_module('Parallel::ForkManager')){
			$error_message .= "\n Perl module Parallel::ForkManager not found. Required ";
			$error_message .= "for --fork / -F.";
		}
		else {
			import Parallel::ForkManager;
			$b_fork = 1;
		}
	}
	if ($error_message){
		main::error_handler('bad-fork',$error_message,0);
	}
	elsif ($VERBOSITY > 0){
		say "Supported: $FORK";
	}
}

sub check_in_out_types {
	my $error_message = '';
	if ($VERBOSITY > 0){
		eval $print_line_large;
		print main::dotify("Checking input and output types");
	}
	if ($run{'ffmpeg'} && ($INPUT_TYPE !~ /^(flac)$/ || 
	$OUTPUT_TYPE !~ m/^(flac|ogg|opus)$/i)){
		$error_message .= "\n The --ffmpeg input/output type combination you entered is not supported: ";
		$error_message .= "in: $INPUT_TYPE out: $OUTPUT_TYPE";
	}
	if ((!$run{'analyze'} && !$run{'infofix-quality'}) && 
	$INPUT_TYPE !~ m/^(aiff?|flac|mp3|raw|shn|wav)$/i){
		$error_message .= "\n The input type you entered is not supported: ";
		$error_message .= "$INPUT_TYPE";
	}
	if ((!$run{'analyze'} && (!$run{'infofix-quality'} || $run{'infofix-verify'})) && 
	($run{'tagger'} || $run{'checksum'} || $run{'checksum-verify'} || 
	$run{'taglist'} || $run{'infofix-verify'} || $run{'resample'}) && 
	$INPUT_TYPE !~ m/^(flac)$/i){
		$error_message .= "\n The input type you entered is not supported for the ";
		$error_message .= "\n checksum/info/resample/tagging operation you are requesting: ";
		$error_message .= "$INPUT_TYPE";
	}
	# 	if (($run{'analyze'} || $run{'info-quality'}) && 
	# 	 $INPUT_TYPE !~ m/^(flac)$/i){
	# 		$error_message .= "\n The input type you entered is not supported for ";
	# 		$error_message .= "\n analyze operations: ";
	# 		$error_message .= "$INPUT_TYPE";
	# 	}
	if ($b_check_out){
		if (!$OUTPUT_TYPE){
			$error_message .= "\n Default output type is no longer set. Please set output type";
			$error_message .= "\n with config item OUTPUT_TYPE or --output/-o.";
		}
		else {
			if ($OUTPUT_TYPE !~ m/^(aac|flac|m4a|mp3|ogg|opus)$/i){
				$error_message .= "\n  The output type you entered is not supported: ";
				$error_message .= "$OUTPUT_TYPE";
			}
			if ($OUTPUT_TYPE =~ /^(m4a|mp3|aac)$/i && lc($INPUT_TYPE) ne 'flac'){
				$error_message .= "\n The output type $OUTPUT_TYPE you entered ";
				$error_message .= "only supports input type: flac";
			}
			if (($run{'resample'} || $INPUT_TYPE =~ m/^(aac|aiff?|m4a|mp3|raw|shn)$/i) &&
			lc($OUTPUT_TYPE) ne 'flac'){
				$error_message .= "\n The input type $INPUT_TYPE you entered ";
				$error_message .= "only supports output type: flac";
			}
		}
	}
	if ($error_message){
		main::error_handler('unsupported-type',$error_message,0);
	}
	else {
		$b_valid_in_out = 1;
		if ($VERBOSITY > 0){
			say "Valid: $INPUT_TYPE(in) $OUTPUT_TYPE(out)";
		}
	}
}

sub check_nlink {
	my $error_message = '';
	if (defined $DONT_USE_NLINK && $DONT_USE_NLINK ne '0'){
		if ($VERBOSITY > 0){
			print main::dotify("Checking DONT_USE_NLINK value");
		}
		if ($DONT_USE_NLINK !~ m/^[01]$/){
			$error_message .= "\n DONT_USE_NLINK only supports 0-1. ";
			$error_message .= "You used: $DONT_USE_NLINK";
		}
		if ($error_message){
			main::error_handler('bad-nlink',$error_message,0);
		}
		elsif ($VERBOSITY > 0){
			say "Supported: $DONT_USE_NLINK";
		}
	}
}

sub check_quality{
	my $error_message = '';
	if ($VERBOSITY > 0){
		print main::dotify("Checking quality support for $OUTPUT_TYPE");
	}
	## NOTE: this is not used
	if (lc($OUTPUT_TYPE) eq 'flac'){
		if ($QUALITY_FLAC !~ m/^[0-8]$/){
			$error_message .= "\n $OUTPUT_TYPE only supports ";
			$error_message .= "0 to 8 quality levels. You entered: $QUALITY_FLAC";
		}
		else {
			$quality = $QUALITY_FLAC;
		}
	}
	if (lc($OUTPUT_TYPE) eq 'ogg'){
		if ($QUALITY_OGG !~ m/^-?[0-9]+(\.[0-9]+)?$/ || 
		 $QUALITY_OGG < -1 || $QUALITY_OGG > 10){
			$error_message .= "\n $OUTPUT_TYPE only supports ";
			$error_message .= "-1 to 10 quality levels. You entered: $QUALITY_OGG";
		}
		else {
			$quality = $QUALITY_OGG;
		}
	}
	if (lc($OUTPUT_TYPE) eq 'aac' || lc($OUTPUT_TYPE) eq 'm4a'){
		if ($QUALITY_AAC !~ m/^[0-9]+$/ || $QUALITY_AAC < 10 || $QUALITY_AAC > 500){
			$error_message .= "\n $OUTPUT_TYPE only supports ";
			$error_message .= "10 to 500 quality levels. You entered: $QUALITY_AAC";
		}
		else {
			$quality = $QUALITY_AAC;
		}
	}
	# supports fractional quality levels
	elsif (lc($OUTPUT_TYPE) eq 'opus'){
		if ($QUALITY_OPUS !~ m/^[0-9]+$/ || $QUALITY_OPUS < 6|| $QUALITY_OPUS > 256){
			$error_message .= "\n $OUTPUT_TYPE only supports ";
			$error_message .= "6 to 256 bitrate quality levels. You entered: $QUALITY_OPUS";
		}
		else {
			$quality = $QUALITY_OPUS;
		}
	}
	elsif (lc($OUTPUT_TYPE) eq 'mp3'){
		if ($QUALITY_MP3 !~ m/^[0-9](\.\d{1,3})?$/){
			$error_message .= "\n $OUTPUT_TYPE only supports 0-9.999 quality levels. ";
			$error_message .= "You entered: $QUALITY_MP3";
		}
		else {
			$quality = $QUALITY_MP3;
		}
	}
	if ($error_message){
		main::error_handler('quality-invalid',$error_message,0);
	}
	elsif ($VERBOSITY > 0){
		say "Supported: $quality ($OUTPUT_TYPE)";
	}
}

# check only runs if $run{'resample'} loaded
sub check_resample {
	my $error_message = '';
	my $dithers = '0|rectangular|triangular|triangular_hp|lipshitz|shibata|';
	$dithers .= 'low_shibata|high_shibata|f_weighted|modified_e_weighted|d'; 
	$dithers .= 'improved_e_weighte';
	if ($VERBOSITY > 0){
		eval $print_line_large;
		print main::dotify("Checking resample values");
	}
	if (!$run{'resample-override'}) {
		if ($run{'resample'}->[0] !~ /(16|20|24)/ || 
		 $run{'resample'}->[1] !~ /(44\.1|48|88\.2|96|192)/){
			$error_message = 'Invalid resample values. Supported bit depths: 16/20/24;';
			$error_message .= "\n sample rates: 44.1/48/88.2/96/192";
		}
	}
	else{
		if (($run{'resample'}->[0] < 4 || $run{'resample'}->[0] > 32) || 
		 ($run{'resample'}->[1] < 1 || $run{'resample'}->[1] > 655)){
			$error_message = 'Invalid resample values. Supported bit depths: 4-32; ';
			$error_message .= "\n sample rates: 1-655khz";
		}
	}
	# only apply dither when bit depth < 24
	if ($run{'resample'}->[0] < 24 && $DITHER !~ /^($dithers)$/) {
		$error_message .= "\n" if $error_message;
		$error_message = 'Invalid dither type. See --help or man for supported types;';
	}
	if ($error_message){
		main::error_handler('bad-resample',$error_message,0);
	}
	else {
		if ($VERBOSITY > 0){
			my $info = 'Valid: ' . $run{'resample'}->[0];
			$info .= ':' .  $run{'resample'}->[1];
			say $info;
		}
		# ffmpeg uses the raw hz values, not khz
		$run{'resample'}->[1] = $run{'resample'}->[1] * 1000;
	}
}

sub check_src_dest_directories {
	my $missing_dirs = '';
	my $error_message = '';
	if ($VERBOSITY > 0){
		eval $print_line_large;
		print main::dotify("Checking source / destination directories");
	}
	if (! -d $SOURCE_DIRECTORY){
		$missing_dirs .= "\n Source Directory: $SOURCE_DIRECTORY";
	}
	if ($b_check_dest && ! -d $DESTINATION_DIRECTORY){
		$missing_dirs .= "\n Destination Directory: $DESTINATION_DIRECTORY";
	}
	if ($missing_dirs){
		$error_message .= "The paths for the following directories are missing:";
		$error_message .= "$missing_dirs";
		$error_message .= "\nPlease check the directory paths you provided.";
		main::error_handler('dest-dir',$error_message,0);
	} 
	elsif ($VERBOSITY > 0){
		say 'Directories: exist';
	}
	if ($SOURCE_DIRECTORY =~ m|^\.\.$path_separator| || 
	 ($b_check_dest && $DESTINATION_DIRECTORY =~ m|^\.\.$path_separator|)){
		$error_message = "Path for -d/-s cannot start with ..$path_separator";
		main::error_handler('dest-src-2-dot',$error_message,0);
	}
	if ($b_check_dest && $run{'clean'} && 
	 ($DESTINATION_DIRECTORY !~ m%^(~$path_separator|$path_separator)% || 
	 $SOURCE_DIRECTORY !~ m%^(~$path_separator|$path_separator)%)){
		$error_message = "--clean option -s/-d paths must start with ";
		$error_message .= "'~$path_separator' or '$path_separator' .\n";
		$error_message .= " OK: -s /home/you/music -d /home/you/music/opus OR\n";
		$error_message .= "     -s ~/music -d ~/music/opus";
		main::error_handler('clean-dest-src',$error_message,0);
	}
	if ($b_check_dest && !$error_message && $DESTINATION_DIRECTORY eq $SOURCE_DIRECTORY){
		$error_message = "Destination directory cannot be same as Source directory!";
		main::error_handler('dest-eq-src-dir',$error_message,0);
	}
	if (((!$b_test && ($run{'autotag-unique'} || $run{'autotag-create'})) || 
	($run{'taglist-autotag'} && $run{'taglist-write'})) && 
	 !(main::globber("$SOURCE_DIRECTORY${path_separator}*.$INPUT_TYPE"))){
		$error_message = "autotag-create/--unique write file options can only be run on ";
		$error_message .= "\ndirectory containing $INPUT_TYPE files. None found in: ";
		$error_message .= "$SOURCE_DIRECTORY";
		main::error_handler('prefill-error',$error_message,0);
	}
}

sub check_verbosity {
	my $error_message = '';
	if ($VERBOSITY > 0){
		print main::dotify("Checking verbosity output level");
	}
	if ($VERBOSITY !~ m/^([0-3])$/){
		$error_message .= "\n VERBOSITY only supports 0-3. ";
		$error_message .= "You used: $VERBOSITY";
	}
	if ($error_message){
		main::error_handler('bad-level',$error_message,0);
	}
	elsif ($VERBOSITY > 0){
		say "Supported: $VERBOSITY";
	}
}
}

#### -------------------------------------------------------------------
#### HELP/VERSION/MESSAGES
#### -------------------------------------------------------------------

sub print_completion_message {
	eval $print_line_heavy;
	if ($b_dest_changed){
		if ($VERBOSITY > 1){
			say 'All done updating. Enjoy your music!';
		}
		elsif ($VERBOSITY > 0){
			say "\nUpdating completed. Enjoy your music!";
		}
	}
	elsif ($VERBOSITY > 0){
		say "\nThere was nothing to update today in your collection.";
	}
	exit 0;
}

sub print_not_found {
	my ($message,$none);
	if ($_[0] eq 'files'){
		$message = "No files to update of type: $extension";
		$none = "None to update";
	}
	elsif ($_[0] eq 'extension'){
		$message = "No files found of type: $extension";
		$none = "None found";
	}
	elsif ($_[0] eq 'dirs'){
		$message = 'No new directories required. Continuing...';
		$none = "None required";
	}
	elsif ($_[0] eq 'directory-cleaned'){
		$message = 'No directories to remove. Continuing...';
		$none = "None to remove\n";
	}
	elsif ($_[0] eq 'file-cleaned'){
		$message = 'No files to remove. Continuing...';
		$none = "None to remove\n";
	}
	if ($VERBOSITY > 1){
		say $message;
	}
	elsif ($VERBOSITY > 0){
		print $none;
	}
}

sub show_options {
	# so it shows the user config data if present
	UserConfigs::set(); 
	# but this should override the config data so follows
	set_basic_data();
	$OUTPUT_TYPE = '(UNSET)' if !$OUTPUT_TYPE;
	my $output = "$self_name v: $self_version-$self_patch ($self_date)\n";
	$output .= "While long option names are provided, it's usually easier to use short forms.\n";
	$output .= "Sample Usage: $self_name\n";
	$output .= "[--quality 8 --destination /music/main/ogg] [-q8 -d /music/main/ogg]\n";
	$output .= "[--input flac --output ogg --append pdf] [-i flac -o ogg -a pdf]\n";
	$output .= "[--copy doc,docx,bmp --clean sync --fork 4] [-c doc,docx,bmp --clean sync -F4]\n";
	$output .= "[--source ./ --infofix cknw] [-s./ -Xcknw]\n";
	$output .= "[--source ./ --prefill --multi 'at:-'] [-s./ -EM 'at:-']\n";
	$output .= "[--source ./ --glob 'BandX*' --checksum --autotag] [-s./ -g 'BandX*' -AK]\n";
	$output .= "When no options are supplied it will sync your configured directories/codecs.\n";
	$output .= $line_small . "\n";
	$output .= "Input/Output options:\n";
	$output .= "--destination, -d Path to the directory where you want the processed\n";
	$output .= "                  (eg, ogg) files to go.\n";
	$output .= "                  Current value: $DESTINATION_DIRECTORY\n";
	$output .= "--dot             Also sync files and directories starting with a '.'. Don't\n";
	$output .= "                  blame $self_name if this creates unintended consequences!!\n";
	$output .= "--input, -i       Input type: {aif,flac,raw,shn,wav}. shn requires codec\n";
	$output .= "                  shorten. raw,shn only output to flac. Current value: $INPUT_TYPE\n";
	$output .= "--nlink           Set \$File::Find::dont_use_nlink = 0. Expert use only.\n";
	$output .= "--no-dot          Override user configuration setting for DOT (--dot) [default].\n";
	$output .= "--no-nlink        Set \$File::Find::dont_use_nlink = 1 [default].\n";
	$output .= "                  Expert use only.\n";
	$output .= "--output, -o      Output types: {aac|flac|m4a|mp3|ogg|opus}. All types except flac\n";
	$output .= "                  require input type flac. To preserve flac tags for aac, use m4a.\n";
	$output .= "                  Current value: $OUTPUT_TYPE\n";
	$output .= "--recurse {0-xx}  Set directory recursion levels. Default infinite. Useful\n";
	$output .= "                  for having syncing or checksum tools ignore sub directories.\n";
	$output .= "--source, -s      Path to the top-most directory containing your source files.\n";
	$output .= "                  Current value: $SOURCE_DIRECTORY\n";
	$output .= "--source-glob, --glob, -g {Path inside -s directory with wildcards}\n";
	$output .= "                  Wildcard path, using *, paths relative to -s path.\n";
	$output .= "                  Allows work on only a subset of files/directories in -s path.\n";
	$output .= $line_small . "\n";
	$output .= "Syncing options:\n";
	$output .= "--append, --copy-append, -a\n";
	$output .= "                  Add extension type(s) to existing extension copy list.\n";
	$output .= "                  1 or more, comma separated, no spaces.\n";
	$output .= "                  Sample: -a tag,pdf\n";
	$output .= "--clean           Clean directories and files from destination not found in\n";
	$output .= "                  source music directory. Will show you directories/files to\n";
	$output .= "                  be deleted then ask you to confirm (twice) that you want to\n";
	$output .= "                  remove that set of files or directories. Exits at end.\n";
	$output .= "                  optional value 'sync' is used: --clean sync\n";
	$output .= "--codec           {libfdk-aac|aac} - if you want to use alternate codec.\n";
	$output .= "                  Output -o aac/m4a only\n";
	$output .= "--copy, -c        Comma separated list of alternate data types to copy to\n";
	$output .= "                  Output type directories. Comma separated, no spaces.\n";
	$output .= "                  Current copy types: ";
	$output .= (length(join(' ',@extension_list))>40) ? "\n                  " : '';
	$output .= "@extension_list\n";
	$output .= "                  Sample: -c png,jpg,pdf,txt,tag\n";
	$output .= "--dither          Alternate dither type for resampling. Possible values:\n";
	$output .= "                  [0|rectangular|triangular|triangular_hp|lipshitz|shibata|\n";
	$output .= "                  low_shibata|high_shibata|f_weighted|modified_e_weighted|’\n";
	$output .= "                  improved_e_weighted]. Current: $DITHER\n";
	$output .= "--exclude, -x     Exclude a list of unique strings separated by ^^. Excludes\n";
	$output .= "                  sync/copy action to destination directory. Replaces\n";
	$output .= "                  \$EXCLUDE values if present. Anything matching in file path\n";
	$output .= "                  will be excluded. Can also be path to a file of excludes.\n";
	$output .= "                  Sample: --exclude='artwork^^Daisy Queen^^Bon Jovi'\n";
	$output .= "                  Sample: --exclude='/home/me/music/excludes/$EXCLUDE_BASE.txt\n";
	$output .= "--exclude-append, -y\n";
	$output .= "                  Append an item to the list of excludes or file.\n";
	$output .= "                  Sample: --exclude-append='My Sharona^^Dancing Queen'\n";
	$output .= "--ffmpeg          Force flac to ogg/opus conversions to use ffmpeg. Useful if\n";
	$output .= "                  you want to include embedded images to oggs (Experimental).\n";
	$output .= "--force, -f       Force overwrite the mp3/ogg/opus/jpg/txt/etc. files, even\n"; 
	$output .= "                  if they already exist.\n";
	$output .= "--fork, -F        [0-x] - Number of forks/threads to use. 0 default, disables.\n";
	$output .= "                  Requires Perl module: Parallel::ForkManager. Current: $FORK\n";
	$output .= "--quality, -q {n} Qualities for output types:\n";
	$output .= "                  flac: n 0-8. 8 smallest, but > 4 generally pointless.\n";
	$output .= "                  aac/m4a: n 10-500. 500 best.\n";           
	$output .= "                  ogg: n -1-10. 10 best. Decimals OK.\n";
	$output .= "                  opus: n 6-256. Variable bit rate. 256 best.\n";
	$output .= "                  mp3: n 0-9.999. Variable bit rate, 0 best. Decimals OK.\n";
	$output .= "                  Current values: $QUALITY_AAC (aac/m4a); $QUALITY_FLAC (flac); \n";
	$output .= "                  $QUALITY_MP3 (mp3); $QUALITY_OGG (ogg); $QUALITY_OPUS (opus)\n";
	$output .= "--resample        {bit depth:sample rate khz} - Supports bit depth 16|20|24 and\n";
	$output .= "                  sample rates 44.1|48|88.2|96|192. Read manual for more info.\n";
	$output .= "                  Example --resample 16:48\n";
	$output .= "--resample-override\n";
	$output .= "                  Allows all supported flac resampling values: 2-32 bit depths;\n";
	$output .= "                  1-655 khz sampling rates (decimals ok).\n";
	$output .= $line_small . "\n";
	$output .= "Tagging options:\n";
	$output .= "--autotag, -A     Requires auto.tag formatted file in each directory. Flac only.\n";
	$output .= "                  Deletes all existing tags, then creates a fully tagged set of\n";
	$output .= "                  files.\n";
	$output .= "--autotag-create, -C\n";
	$output .= "                  Create $AUTOTAG_FILE template in source directory. Will be\n";
	$output .= "                  populated with file names for recording filled in already in\n";
	$output .= "                  track listing. Preserves existing REPLAYGAIN values. Not\n";
	$output .= "                  recommended, use -S or -M instead.\n";
	$output .= "--autotag-create-multi, -M {disc ID}\n";
	$output .= "                  Required argument tells logic how to determine your disc\n";
	$output .= "                  numbering method. % is used to indicate the value is a number\n";
	$output .= "                  between 1-9. @ is used to indicate a letter between A-Z. \n";
	$output .= "                  Will complete TRACKTOTAL, DISCNUMBER, TRACKNUMBER values\n";
	$output .= "                  in auto.tag file, which saves a lot of time. See -E for more\n";
	$output .= "                  prefill options.\n";
	$output .= "                  Samples: -M d% [d1track02.flac]; -M d\%- [d2-track04.flac];\n";
	$output .= "                  -M % [112.flac]; -M 2015-03-21.d%. [2015-03-21.d1.t03.flac]\n";
	$output .= "                  -M @ [A12.flac]; -M d\@- [da-track02.flac];\n";
	$output .= "                  Flac input type only.\n";
	$output .= "--autotag-create-single, -S\n";
	$output .= "                  For single disc recordings, will also add TRACKTOTAL and\n";
	$output .= "                  TRACKNUMBER counts when creating and populating the\n";
	$output .= "                  $AUTOTAG_FILE file. See -E for more prefill options.\n";
	$output .= "--autotag-file, --atf, --af {file name}\n";
	$output .= "                  One time change of $AUTOTAG_FILE file name. Use unique name.\n";
	$output .= "--ffprobe         Force use of ffprobe for FLAC analysis in -Z/-Xq\n";
	$output .= "--image, -I [jpg,png image file name|remove]\n";
	$output .= "                  Embed image file into single recording directory. 'remove'\n";
	$output .= "                  value only removes images. -RI also removes existing images.\n";
	$output .= "--info-file, --if, --prefill-file, --pf {file name}\n";
	$output .= "                  Alternate file name to use for --prefill, --taglist=i,\n";
	$output .= "                  --infofix.\n";
	$output .= "--infofix, -X [0acdklmnqtuvw]\n";
	$output .= "                  Fix info.txt files. Use any combination. Always corrects white\n";
	$output .= "                  space.\n";
	$output .= "                  0: (zero) No printed leading delimiter characters per line.\n";
	$output .= "                  a: Add numbering to unnumbered track/setlists. See man.\n";
	$output .= "                  c: Correct Windows CP-1252 special character issues.\n";
	$output .= "                  d: Make iso date: YYYY-MM-DD from various random date formats.\n";
	$output .= "                  k: Adds (weekday) after iso date. Activates d.\n";
	$output .= "                  l: Set all lowercase, and apply simple upper case first fixes.\n";
	$output .= "                  m: Add parentheses around track times, move to end, in track\n";
	$output .= "                     title.\n";
	$output .= "                  n: Make track numbering 'NN. ' (0 padded) or 'N-NN. '\n";
	$output .= "                  q: Add FLAC/Quality: /[quality] items to info file.\n";
	$output .= "                  t: Fix upper/lower case track titles.\n";
	$output .= "                  u: Fix entire file upper/lower.\n";
	$output .= "                  v: Add FFP verification to q (FLAC only). Activates q.\n";
	$output .= "                  w: Write changes to file.\n";
	$output .= "                  t and u use Title upper case main words.\n";
	$output .= "--info-rating {2-xxx}\n";
	$output .= "                  Change default rating value for --infofix q. Current: $INFO_RATING;\n";
	$output .= "--multiartist, --ma {[at|ta]:separator}\n";
	$output .= "                  Use with --prefill if track titles in info file contain artist\n";
	$output .= "                  and song name. Examples:\n";
	$output .= "                  \'at:-\' (2. Artist - Title); \'ta\:\\' (2. Title \\ Artist)\n";
	$output .= "                  Separator: one or more non-space characters that separate the\n";
	$output .= "                  artist/title values. Separator must have 1 or more spaces on\n";
	$output .= "                  each side in the info track title. Invalid: 'at:-' Band-Title\n";
	$output .= "                  Valid: 'at:-' Band-Name - Title\n";
	$output .= "--prefill, -E     Attempt to prefill auto.tag file using info.txt data. See man\n";
	$output .= "                  for required syntax and structure of data in info file.\n";
	$output .= "--prefill-tag, --pft, --pt {tag data}\n";
	$output .= "                  Uses same syntax as --tag. Adds tag+values to auto.tag.\n";
	$output .= "                  Value 'UNSET' removes the PREFILL_TAG configuration value.\n";
	$output .= "--no-replaygain   Does not preserve REPLAYGAIN values for autotag-create.\n";
	$output .= "--remove-images, -R, --image-remove\n";
	$output .= "                  Remove all embedded images and image padding from file. Either\n";
	$output .= "                  prior to embedding new one with -I/-A, or just to remove them.\n";
	$output .= "--remove-padding, -P\n";
	$output .= "                  For --autotag and --tag also removes block padding. Slows.\n";
	$output .= "                  tagging down significantly.\n";
	$output .= "--start {0-xx}    For auto.tag, starts tag numbering of tracks at supplied\n";
	$output .= "                  integer value. Useful if tracks start at 00.flac or 07.flac\n";
	$output .= "--tag, -T         {\"TAG1%:tag value^^TAG2%:tag value}\n";
	$output .= "                  Updates one or more recordings with the supplied FLAC\n";
	$output .= "                  tag/value pairs. Removes existing sets of the given type.\n";
	$output .= "                  Use ^^ to separate tag key/value pairs, and %: to separate\n";
	$output .= "                  the tag name and its value. Value 'UNSET' removes the tag.\n";
	$output .= "--taglist, -L     [acfisw] Print to screen generated tag data in FLAC\n";
	$output .= "                  directories using Vorbis tag data found in each file.\n";
	$output .= "                  Use 'w' to write to data to file(s).\n";
	$output .= "                  a: Switch to '%:' separator, and write to $AUTOTAG_FILE.\n";
	$output .= "                  c: Generate condensed tag list data.\n";
	$output .= "                  f: Generate full per audio file tag list data (default).\n";
	$output .= "                  i: Generate $INFO_FILE data. Can be used with 'a' or 'f'.\n";
	$output .= "                  s: Skip file(s) exist tests. Don't use with 'w'.\n";
	$output .= "                  w: Write changes to file(s). Only writes if $INFO_FILE\n";
	$output .= "                  and/or $TAGLIST_FILE/$AUTOTAG_FILE files are not already present.\n";
	$output .= "--taglist-file, --tlf, --tf {file}\n";
	$output .= "                  Alternate file name to use for --taglist.\n";
	$output .= "--unique {tags}   Comma separated list of tags to be used only 1x per file, and\n";
	$output .= "                  unset after. Can also be manually set on top of $AUTOTAG_FILE\n";
	$output .= "                  with UNIQUE%:. Use with -C/-S/-M to set UNIQUE%: in\n";
	$output .= "                  $AUTOTAG_FILE, and with -A to manually add UNIQUE%: tag list.\n";
	$output .= "                  See man page.\n";
	$output .= $line_small . "\n";
	$output .= "Analyze/Checksum options:\n";
	$output .= "--analyze, -Z     Analyze contents of directories, creates screen report per\n";
	$output .= "                  file/directory. Uses default input type, or -i type. Shows\n";
	$output .= "                  total time, size, ffp avg kbs, etc. Similar to -Xq but prints\n";
	$output .= "                  to screen. -v0 shows only per directory summary data;\n";
	$output .= "                  -v2 adds ffp (flac only), changes to 1 key:value pair per\n";
	$output .= "                  line; -v 3 adds precision. See also -Xq, -Xv\n";
	$output .= "--checksum, -K    Create .ffp and .md5 checksum files in your source directory.\n";
	$output .= "                  Checksum files are only created inside directories where flac\n";
	$output .= "                  files are found. Use --checksum-delete if you also want\n";
	$output .= "                  to delete existing checksum files. Only flac type\n";
	$output .= "                  is supported.\n";
	$output .= "                  Do not use together with cleaning/syncing options!\n";
	$output .= "--checksum-delete, -D\n";
	$output .= "                  Delete all existing .ffp, .md5, .ffp.txt, and md5.txt files\n";
	$output .= "                  before creating the new checksum files. Files only deleted in\n";
	$output .= "                  directories where flac files are found.\n";
	$output .= "--checksum-ffps, --ffps\n";
	$output .= "                  Prints only ffps to screen, turns off md5 tests\n";
	$output .= "--checksum-verify, -V\n";
	$output .= "                  Verifies FLAC files and confirms MD5 file data matches actual\n";
	$output .= "                  files found. Can be run alone or with --checksum options.\n";
	$output .= "--duplicates, --dupes\n";
	$output .= "                  Test a directory of many releases for duplicated ffp files.\n";
	$output .= "--no-ffp          Skips ffp processing on --checksum or --checksum-verify.\n";
	$output .= "--no-md5          Skips md5 processing on --checksum or --checksum-verify.\n";
	$output .= "--z-min-size {0-xxx}\n";
	$output .= "                  Use with -Z. Change min lossless file size for ALERT.\n";
	$output .= "--z-min-time {0-xxx}\n";
	$output .= "                  Use with -Z. Change min audio file time for ALERT.\n";
	$output .= $line_small . "\n";
	$output .= "Miscellaneous options:\n";
	$output .= "--aggregate, -G   [filename|extension] Copy file name or extension type to\n";
	$output .= "                  --destination directory. If no argument given, copies over\n";
	$output .= "                  $AUTOTAG_FILE. Do not use . in extension (jpg good, .jpg bad)\n";
	$output .= "                  You can supply more than one filename or extension:\n";
	$output .= "                  (file name): acxi -d ~/music/cdinfo --aggregate info.txt\n";
	$output .= "                  (extension): acxi -d ~/music/cdinfo --aggregate jpg\n";
	$output .= "                  (several): acxi -d ~/music/cdinfo --aggregate png,jpg,info.txt\n";
	$output .= "--config, --configuration\n";
	$output .= "                  Show active configuration values, by file, then exit.\n";
	$output .= "--help, -h        This help menu.\n";
	if ($ALLOW_UPDATES){
		$output .= "--no-ssl          Disable SSL for update. Useful if legacy server and -U3.\n";
		$output .= "--update, -U      Update $self_name and man page. Set paths if not Linux.\n";
		$output .= "                  -U 3 updates from smxi.org server directly, skips codeberg.\n";
		$output .= "                  Current values: $self_name: $SELF_DIRECTORY\n";
		$output .= "                  man $self_name.1: $MAN_DIRECTORY\n";
	}
	$output .= "--version         Show $self_name version.\n";
	$output .= $line_small . "\n";
	$output .= "Debug/Output control options:\n";
	$output .= "--dbg             {1-xx}[,xx] Trigger specific debuggers data. See man.\n";
	$output .= "--dry, --dry-run, --test\n";
	$output .= "                  Test copy, sync, autotag, checksum without actually doing\n";
	$output .= "--quiet           Turns off most screen output, except for error messages.\n";
	$output .= "                  Only disables screen output is logical to do so. Same as -v 0\n";
	$output .= "                  the action.\n";
	$output .= "--verbosity, -v   {0-3} Dynamically set VERBOSITY. Current value: $VERBOSITY\n";
	$output .= "                  0: Turns off all output, except for error messages.\n";
	$output .= "                  1: Basic single line per operation output, default value.\n";
	$output .= "                  2: Without full verbosity of -v3, no flac/lame/oggenc/opusenc\n";
	$output .= "                  for conversion process screen output.\n";
	$output .= "                  3: Full output, including full verbosity of ffmpeg/flac/\n";
	$output .= "                  lame/oggenc/opusenc conversion process for aac, m4a, mp3, ogg,\n";
	$output .= "                  or opus output.\n";
	$output .= $line_small . "\n";
	$output .= "User Configs ";
	if (!$CONFIG_DIRECTORY){
		$output .= "(checked in this order):\n/etc/$self_name.conf\n";
		$output .= "\$XDG_CONFIG_HOME/$self_name.conf\n";
		$output .= "\$HOME/.config/$self_name.conf\n";
		$output .= "\$HOME/.$self_name.conf\n";
	}
	else {
		$output .= "(manually set):\n$CONFIG_DIRECTORY/$self_name.conf\n";
	}
	$output .= "Requires this syntax (any user modifiable variable can be used)\n";
	$output .= "SOURCE_DIRECTORY=/home/me/music/flac\n";
	$output .= "DESTINATION_DIRECTORY=/home/me/music/opus\n";
	$output .= "Do not use \$, \", or \' in config options or values (except in path names).\n";
	say $output;
	exit 0;
}

sub show_version {
	say "$self_name version: $self_version-$self_patch ($self_date)";
	exit 0;
}

#### -------------------------------------------------------------------
#### SET RUNTIME VALUES
#### -------------------------------------------------------------------

sub set_basic_data {
	$start_dir = getcwd();
	# if --copy/-c is set, then use that data instead of default copy types
	if (defined $COPY_TYPES){
		@extension_list = split(/\s*,\s*/, $COPY_TYPES);
	}
	$DESTINATION_DIRECTORY =~ s|$path_separator$||;
	$SOURCE_DIRECTORY =~ s|$path_separator$||;
	$CONFIG_DIRECTORY =~ s|$path_separator$|| if $CONFIG_DIRECTORY;
	$INPUT_TYPE = lc($INPUT_TYPE) if $INPUT_TYPE;
	$OUTPUT_TYPE = lc($OUTPUT_TYPE) if $OUTPUT_TYPE;
	@extension_list = (@extension_list,$INPUT_TYPE);
	$File::Find::dont_use_nlink = $DONT_USE_NLINK;
	if ($run{'source-glob'}){
		$run{'source-glob'} =~ s|$path_separator$||;
		# say $start_dir;
		chdir $SOURCE_DIRECTORY;
		my @temp = globber($run{'source-glob'});
		# say Data::Dumper::Dumper \@temp;exit;
		# There is a highly non-intuitive globber behavior, it returns the path 
		# for the glob result even if it doesn't exist, which seems like bug.
		foreach (@temp){
			if (-e  $SOURCE_DIRECTORY . $path_separator . $_){
				push(@source_glob,$SOURCE_DIRECTORY . $path_separator . $_);
			}
		}
		# say Data::Dumper::Dumper \@source_glob;exit;
		chdir $start_dir;
	}
	else {
		push(@source_glob,$SOURCE_DIRECTORY);
	}
	if ($EXCLUDE){
		if ($EXCLUDE =~ /\Q$EXCLUDE_BASE\E/){
			@excludes = reader($EXCLUDE, 'strip');
		}
		else {
			@excludes = split /\Q^^\E/, $EXCLUDE;
		}
		if ($exclude_append){
			my @temp = split /\Q^^\E/, $exclude_append;
			@excludes = (@excludes,@temp);
		}
		@excludes_stripped = map { my $temp = $_;$temp =~ s|/$||;$temp} @excludes if @excludes;
		# say Data::Dumper::Dumper \@excludes;
		# say Data::Dumper::Dumper \@excludes_stripped;
	}
	if ($PREFILL_TAG){
		my @prefills = split(/\Q^^\E/,$PREFILL_TAG);
		foreach my $tag (@prefills){
			my @info = split(/\s*%:\s*/,$tag);
			$run{'prefill-tag'}->{$info[0]} = $info[1];
		}
	}
}

sub set_display_data {
	if ($VERBOSITY < 3){
		if ($OUTPUT_TYPE eq 'mp3'){
			$silent_flac = '--silent'; # flac output 
			$silent_lame = '--silent'; # lame output
		}
		elsif (!$run{'ffmpeg'} && $OUTPUT_TYPE eq 'ogg'){
			$silent_flac = '--quiet'; # for oggenc output
		}
		elsif ($OUTPUT_TYPE =~ /^(aac|m4a)$/){
			$silent_ffmpeg = '-v quiet'; # for ffmpeg output
		}
		elsif (($run{'ffmpeg'} && $OUTPUT_TYPE =~ /^(ogg)$/) || $OUTPUT_TYPE eq 'flac'){
			# -nostats -loglevel 0
			# -hide_banner -loglevel panic
			# -v quiet
			$silent_ffmpeg = '-hide_banner -loglevel panic'; # 
		}
		elsif ($OUTPUT_TYPE eq 'opus'){
			$silent_opus = '--quiet'; # for oggenc output
		}
	}
	if ($VERBOSITY < 2){
		$print_line_heavy = '';
		$print_line_large = '';
		$print_line_small = '';
	}
	else {
		$print_line_heavy = 'say $line_heavy';
		$print_line_large = 'say $line_large';
		$print_line_small = 'say $line_small';
	}
}

# get defaults from user config files if present
## UserConfigs
{
package UserConfigs;

sub set {
	my ($b_show) = @_;
	my ($b_files,@config_files,$file);
	# set list of supported config files
	@config_files = (
	"/etc/$self_name.conf",
	"/etc/$self_name.conf.d/$self_name.conf"
	);
	if ($ENV{'XDG_CONFIG_HOME'} && -r "$ENV{XDG_CONFIG_HOME}/$self_name.conf"){
		push(@config_files,"$ENV{'XDG_CONFIG_HOME'}/$self_name.conf");
	}
	elsif (-r "$ENV{HOME}/.config/$self_name.conf"){
		push(@config_files,"$ENV{HOME}/.config/$self_name.conf");
	}
	elsif (-r "$ENV{HOME}/.$self_name.conf"){
		push(@config_files,"$ENV{HOME}/.$self_name.conf");
	}
	elsif (-r "$CONFIG_DIRECTORY/$self_name.conf"){
		push(@config_files,"$CONFIG_DIRECTORY/$self_name.conf");
	}
	foreach $file (@config_files){
		next unless -r $file && open (my $fh,'<',$file);
		my $b_configs;
		$b_files = 1;
		say "$line_small\n  Configuration file: $file" if $b_show;
		while (<$fh>){
			chomp;                  # no newline
			s/#.*//;                # no comments
			s/^\s+//;               # no leading white
			s/\s+$//;               # no trailing white
			s/('|"|\$)//g;       # get rid of all non valid characters
			next unless length;     # anything left?
			my ($var, $value) = split(/\s*=\s*/, $_, 2);
			if (defined $var && defined $value){
				if (!$b_show){
					assign_value($var, $value);
				}
				else {
					say $line_result if !$b_configs;
					say "$var=$value";
					$b_configs = 1;
				}
			}
		}
		if ($b_show && !$b_configs){
			say "  No configuration items found in file.";
		}
	}
	return $b_files if $b_show;
}

sub show {
	say 'Showing current active/set configurations, by file. Last overrides previous.';
	my $b_files = set(1);
	say $line_small;
	if ($b_files){
		print "All done! Everything look good? If not, fix it.\n";
	}
	else {
		print "No configuration files found. Is that what you expected?\n";
	}
	exit 0;
}

sub assign_value {
	my ($var,$value) = @_;
	# TAG_FILE is legacy, but just leave it
	if ($var eq 'AUTOTAG_FILE' || $var eq 'TAG_FILE'){$AUTOTAG_FILE = $value}
	elsif ($var eq 'CLEAN'){
		$value = get_boolean($value);
		if ($value =~ /^[10]$/){
			$run{'clean'} = $value;
			$run{'clean-sync'} = $value;
		}}
	elsif ($var eq 'CODEC_AAC'){$CODEC_AAC = $value;}
	elsif ($var eq 'COMMAND_CURL'){$COMMAND_CURL = $value;}
	elsif ($var eq 'COMMAND_FLAC'){$COMMAND_FLAC = $value;}
	elsif ($var eq 'COMMAND_FFMPEG'){$COMMAND_FFMPEG = $value;}
	elsif ($var eq 'COMMAND_LAME'){$COMMAND_LAME = $value;}
	elsif ($var eq 'COMMAND_METAFAC'){$COMMAND_METAFLAC = $value;}
	elsif ($var eq 'COMMAND_OGG'){$COMMAND_OGG = $value;}
	elsif ($var eq 'COMMAND_OPUS'){$COMMAND_OPUS = $value;}
	elsif ($var eq 'COPY_TYPES' || $var eq 'USER_TYPES'){
		$COPY_TYPES = $value;}
	elsif ($var eq 'DESTINATION_DIRECTORY' || $var eq 'DIR_PREFIX_DEST'){
		$DESTINATION_DIRECTORY = $value;}
	elsif ($var eq 'EXCLUDE'){$EXCLUDE = $value;}
	elsif ($var eq 'EXCLUDE_BASE'){$EXCLUDE_BASE = $value;}
	elsif ($var eq 'DITHER'){$DITHER = $value;}
	elsif ($var eq 'DONT_USE_NLINK'){$DONT_USE_NLINK = $value;}
	elsif ($var eq 'DOT'){
		$value = get_boolean($value);
		$run{'dot'} = $value if $value =~ /^[10]$/;}
	elsif ($var eq 'FORK'){$FORK = $value;}
	elsif ($var eq 'FFP_FILE'){$FFP_FILE = $value;}
	elsif ($var eq 'INFO_FILE'){$INFO_FILE = $value;}
	elsif ($var eq 'INFO_RATING'){$INFO_RATING = $value;}
	elsif ($var eq 'INPUT_TYPE'){$INPUT_TYPE = $value;}
	elsif ($var eq 'MAN_DIRECTORY'){$MAN_DIRECTORY = $value;}
	elsif ($var eq 'MD5_FILE'){$MD5_FILE = $value;}
	elsif ($var eq 'OUTPUT_TYPE'){$OUTPUT_TYPE = $value;}
	elsif ($var eq 'PREFILL_TAG'){$PREFILL_TAG = $value;}
	# legacy configs, sets both
	elsif ($var eq 'QUALITY'){$QUALITY_OGG = $value;$QUALITY_MP3 = $value;}
	elsif ($var eq 'QUALITY_AAC'){$QUALITY_AAC = $value;}
	elsif ($var eq 'QUALITY_FLAC'){$QUALITY_FLAC = $value;}
	elsif ($var eq 'QUALITY_MP3'){$QUALITY_MP3 = $value;}
	elsif ($var eq 'QUALITY_OGG'){$QUALITY_OGG = $value;}
	elsif ($var eq 'QUALITY_OPUS'){$QUALITY_OPUS = $value;}
	elsif ($var eq 'SELF_DIRECTORY'){$SELF_DIRECTORY = $value;}
	elsif ($var eq 'SOURCE_DIRECTORY' || $var eq 'DIR_PREFIX_SOURCE'){
		$SOURCE_DIRECTORY = $value;}
	elsif ($var eq 'TAGLIST_FILE'){$TAGLIST_FILE = $value;}
	# LOG_LEVEL is legacy name, leave in always.
	elsif ($var eq 'VERBOSITY' || $var eq 'LOG_LEVEL'){$VERBOSITY = $value;}
	elsif ($var eq 'Z_MIN_TIME'){$Z_MIN_TIME = $value;}
	elsif ($var eq 'Z_MIN_SIZE'){$Z_MIN_SIZE = $value;}
}

sub get_boolean {
	my ($value) = @_;
	$value =~ s/^(yes|1|true|enable|on)$/1/;
	$value =~ s/^(no|0|false|disable|off)$/0/;
	return $value;
}
}

# Get Options and set values, this overrides defaults 
# from top globals and config files
# OptionsHandler
{
package OptionsHandler;
my (%test,$msg);

sub get{
	# single letters used:
	# aAcCdDEfFgGhiIKLMoPqRsSTUvVxXyZ
	Getopt::Long::GetOptions (
	'G|aggregate:s' => sub { 
		my ($opt,$arg) = @_;
		if ($arg){
			if ($arg eq 'file'){
				$run{'aggregate-file'} = 1;
				$arg = $AUTOTAG_FILE;
			}
			$run{'ag-file'} = $arg;
		}
		else {
			$run{'ag-file'} = $AUTOTAG_FILE;
		}
		$run{'aggregate'} = 1;},
	'Z|analyze' => sub { 
		$run{'checksum'} = 1;
		$run{'analyze'} = 1;
		$run{'no-md5'} = 1;
		$test{'checksum'} = 1;},
	'a|append|copy-append:s' => sub { 
		my ($opt,$arg) = @_;
		$COPY_TYPES .= ',' . $arg if $arg;
		$test{'sync'} = 1;},
	'A|autotag' => sub { 
		$run{'autotagger'} = 1;
		$run{'tagger'} = 1;
		$test{'tag'} = 1;},
	'C|autotag-create' => sub { 
		$run{'autotag-create'} = 1;
		$test{'autotag'} = 1;},
	'M|autotag-create-multi:s' => sub { 
		my ($opt,$arg) = @_;
		if (!$arg || $arg !~ /%|@/){
			$msg = "$opt requires multidisk file name identifier. See -h.";
			main::error_handler('missing-arg',$msg,1);
		}
		$autotag_multi = $arg;
		$run{'autotag-create'} = 1;
		$run{'autotag-multi'} = 1;
		$test{'autotag'} = 1;},
	'S|autotag-create-single' => sub { 
		$run{'autotag-create'} = 1;
		$run{'autotag-single'} = 1;
		$test{'autotag'} = 1;},
	'autotag-file|atf|af:s' => sub { 
		my ($opt,$arg) = @_;
		main::error_handler('missing-arg',"$opt requires file name.",1) if !$arg;
		$AUTOTAG_FILE = $arg;
		$test{'autotag-file'} = 1;},
	# legacy
	'basic|default' => sub { 
		$VERBOSITY = 1 },
	'K|checksum' => sub { 
		$run{'checksum'} = 1;
		$test{'checksum'} = 1;
		$test{'checksum-generate'} = 1;},
	'D|checksum-delete' => sub { 
		$run{'checksum'} = 1;
		$run{'checksum-delete'} = 1;
		$test{'checksum'} = 1;
		$test{'checksum-generate'} = 1;},
	'checksum-ffps|ffps' => sub { 
		$run{'checksum'} = 1;
		$run{'checksum-ffps'} = 1;
		$run{'no-md5'} = 1;
		$test{'checksum'} = 1;
		$test{'checksum-generate'} = 1;},
	'V|checksum-verify' => sub { 
		$run{'checksum-verify'} = 1;
		$test{'checksum'} = 1;},
	'clean:s' => sub { 
		my ($opt,$arg) = @_;
		$run{'clean-sync'} = 1 if $arg && $arg eq 'sync';
		$run{'clean'} = 1;
		$test{'sync'} = 1;},
	'codec:s' => sub { 
		my ($opt,$arg) = @_;
		main::error_handler('missing-arg',"$opt requires codec.",1) if !$arg;
		$codec = $arg;
		$test{'sync'} = 1;},
	'config|configs|configuration|configurations' => sub {
		UserConfigs::show();},
	# accepts null value so users can not copy anything
	'c|copy:s' => sub { 
		my ($opt,$arg) = @_;
		$COPY_TYPES = $arg;
		$test{'sync'} = 1;},
	'dbg:s' => sub { 
		my ($opt,$arg) = @_;
		if (!$arg || $arg !~ /^\d+(,\d+)*$/){
			main::error_handler('missing-arg',"$opt requires valid dbg list.",1);
		}
		for (split(',',$arg)){
			$dbg[$_] = 1;
		}},
	'd|destination:s' => sub { 
		my ($opt,$arg) = @_;
		main::error_handler('missing-arg',"$opt requires path.",1) if !$arg;
		$DESTINATION_DIRECTORY = $arg;
		$test{'destination'} = 1;},
	'dither:s' => sub { 
		my ($opt,$arg) = @_;
		main::error_handler('missing-arg',"$opt requires value.",1) if !defined $arg;
		$DITHER = $arg;
		$test{'dither'} = 1;
		$test{'sync'} = 1;},
	'dot' => sub { 
		$run{'dot'} = 1;},
	'dry-run|dry|test' => sub { 
		$b_test = 1;},
	'dupes|duplicates' => sub { 
		$run{'checksum'} = 1;
		$run{'duplicates'} = 1;
		$run{'no-md5'} = 1;
		$test{'checksum'} = 1;},
	'x|exclude:s' => sub { 
		my ($opt,$arg) = @_;
		main::error_handler('missing-arg',"$opt requires exclude list or file.",1) if !$arg;
		$arg = '' if uc($arg) eq 'UNSET';
		$EXCLUDE = $arg;
		$test{'sync'} = 1;},
	'y|exclude-append:s' => sub { 
		my ($opt,$arg) = @_;
		main::error_handler('missing-arg',"$opt requires exclude list.",1) if !$arg;
		$exclude_append = $arg;
		$test{'sync'} = 1;},
	'f|force' => sub { 
		$b_force = 1;},
	'ffmpeg' => sub { 
		$run{'ffmpeg'} = 1;
		$test{'sync'} = 1;},
	'ffprobe' => sub { 
		$run{'ffprobe'} = 1;},
	'F|fork:i' => sub { 
		my ($opt,$arg) = @_;
		main::error_handler('missing-arg',"$opt requires fork count integer.",1) if not defined $arg;
		$FORK = $arg;
		$test{'sync'} = 1;},
	# legacy
	'full' => sub { 
		$VERBOSITY = 3;},
	'h|help|?' => sub { 
		main::show_options();},
	'I|image:s' => sub { 
		my ($opt,$arg) = @_;
		if ($arg){
			if ($arg !~ /(\.png|\.jpe?g|^remove)$/i){
				if ($arg eq 'R'){
					$msg = "-IR is not allowed. Must be -RI. See -h.";
					main::error_handler('missing-arg',$msg,1);
				}
				else {
					$msg = "$opt requires a valid cover image file name/remove. See -h.";
					main::error_handler('missing-arg',$msg,1);
				}
			}
			else {
				if (lc($image_embed) eq 'remove'){
					$run{'image-remove'} = 1;
				}
				else {
					$image_embed = $arg;
				}
			}
		}
		$run{'image-embed'} = 1;
		$run{'tagger'} = 1;
		$test{'tag'} = 1;},
	'info-file|prefill-file|if|pf:s' => sub { 
		my ($opt,$arg) = @_;
		main::error_handler('missing-arg',"$opt requires file name.",1) if !$arg;
		$INFO_FILE = $arg;
		$test{'info'} = 1;
		$test{'info-file'} = 1;},
	'X|infofix:s' => sub { 
		my ($opt,$arg) = @_;
		if ($arg ne ''){
			if ($arg =~ /^[acdklmnqutvw0]+$/){
				$run{'infofix-autonumber'} = 1 if $arg =~ /a/;
				$run{'infofix-character'} = 1 if $arg =~ /c/;
				$run{'infofix-date'} = 1 if $arg =~ /[dk]/;
				$run{'infofix-dow'} = 1 if $arg =~ /k/;
				$run{'infofix-lower'} = 1 if $arg =~ /l/;
				$run{'infofix-time'} = 1 if $arg =~ /m/;
				$run{'infofix-no'} = 1 if $arg =~ /n/;
				$run{'infofix-quality'} = 1 if $arg =~ /[qv]/;
				$run{'infofix-title'} = 1 if $arg =~ /t/;
				$run{'infofix-upper'} = 1 if $arg =~ /u/;
				$run{'infofix-verify'} = 1 if $arg =~ /v/;
				$run{'infofix-write'} = 1 if $arg =~ /w/;
				$run{'infofix-zero'} = 1 if $arg =~ /0/;
			}
			else {
				$msg = "For $opt use no arguments, or d k l m n q t u v w 0. See -h.";
				main::error_handler('missing-arg',$msg,1);
			}
		}
		$run{'infofix'} = 1;
		$test{'info'} = 1;},
	'info-rating:i' => sub { 
		my ($opt,$arg) = @_;
		if (!$arg || $arg < 2){
			$msg = "$opt requires rating integer > 1. See -h.";
			main::error_handler('missing-arg',$msg,1);
		}
		$INFO_RATING = $arg;
		$test{'info'} = 1;
		$test{'info-rating'} = 1;},
	'i|input:s' => sub { 
		my ($opt,$arg) = @_;
		main::error_handler('missing-arg',"$opt requires type.",1) if !$arg;
		$INPUT_TYPE = $arg;},
	'multiartist|ma:s' => sub { 
		my ($opt,$arg) = @_;
		if (!$arg || $arg !~ /^(at|ta):\S+$/i){
			main::error_handler('missing-arg',"$opt at|ta:[separator].",1);
		}
		else {
			$run{'multiartist'} = [split(/:/,$arg)];
			$run{'multiartist'}->[0] = lc($run{'multiartist'}->[0]);
			$test{'autotag'} = 1;
			$test{'prefill'} = 1;
		}},
	'nlink' => sub { 
		$DONT_USE_NLINK = 0;},
	'no-dot' => sub { 
		$run{'dot'} = 0;},
	'no-ffp' => sub { 
		$run{'no-ffp'} = 1;},
	'no-md5' => sub { 
		$run{'no-md5'} = 1;},
	'no-nlink' => sub { 
		$DONT_USE_NLINK = 1;},
	'no-replaygain' => sub { 
		$run{'no-replaygain'} = 1;
		$test{'autotag'} = 1;
		$test{'prefill'} = 1;},
	'no-ssl' => sub { 
		$run{'no-ssl'} = 1;
		$test{'update'} = 1;},
	'o|output:s' => sub {
		my ($opt,$arg) = @_;
		main::error_handler('missing-arg',"$opt requires type.",1) if !$arg;
		$OUTPUT_TYPE = $arg;
		$test{'output'} = 1;
		$test{'sync'} = 1;},
	'E|prefill' => sub { 
		$run{'prefill'} = 1;
		$test{'prefill'} = 1;
		$test{'autotag'} = 1;},
	'prefill-tag|prefill-tags|pft|pt:s' => sub { 
		my ($opt,$arg) = @_;
		if (!$arg || (lc($arg) ne 'unset' && $arg !~ /%:/)){
			$msg = "$opt requires TAG%:value sets. See -h.";
			main::error_handler('missing-arg',$msg,1);
		}
		if (lc($arg) ne 'unset'){
			$PREFILL_TAG = $arg;
			$test{'autotag'} = 1;
			$test{'prefill'} = 1;
			$test{'prefill-tag'} = 1;
		}
		else {
			$PREFILL_TAG = '';
		}},
	'q|quality:f' => sub { 
		my ($opt,$arg) = @_;
		if (!defined $arg || $arg !~ /^-?[0-9\.]+$/){
			main::error_handler('missing-arg',"$opt requires number.",1);
		}
		# validate these later
		$QUALITY_AAC = $arg;
		$QUALITY_FLAC = $arg;
		$QUALITY_MP3 = $arg;
		$QUALITY_OGG = $arg;
		$QUALITY_OPUS = $arg;
		$test{'sync'} = 1;}, 
	'quiet|silent' => sub { 
		$b_quiet = 1; 
		$VERBOSITY = 0;},
	'recurse:i' => sub { 
		my ($opt,$arg) = @_;
		if (!defined $arg){
			$msg = "$opt requires directory recursion level value. See -h.";
			main::error_handler('missing-arg',$msg,1);
		}
		$recurse = $arg;},
	'R|remove-images|image-remove' => sub { 
		$run{'image-remove'} = 1;
		$test{'tag'} = 1;},
	'P|remove-padding' => sub { 
		$padding = ' --dont-use-padding';
		$test{'remove-padding'} = 1;
		$test{'tag'} = 1;},
	'resample:s' => sub {
		my ($opt,$arg) = @_;
		if (defined $arg && $arg =~ /^([0-9]+):([0-9]+(\.[0-9]+)?)$/){
			$run{'resample'} = [$1,$2];
			$test{'sync'} = 1;
		}
		else {
			main::error_handler('missing-arg',"Incorrect syntax for $opt: bits:khz, see -h or man.",1);
		}},
	'resample-override' => sub {
		$run{'resample-override'} = 1;
		$test{'sync'} = 1;},
	's|source:s' => sub { 
		my ($opt,$arg) = @_;
		main::error_handler('missing-arg',"$opt requires path.",1) if !$arg;
		$SOURCE_DIRECTORY = $arg;
		$test{'source'} = 1;},
	'g|glob|source-glob:s' => sub { 
		my ($opt,$arg) = @_;
		main::error_handler('missing-arg',"$opt requires path.",1) if !$arg;
		$run{'source-glob'} = $arg;},
	'start:i' => sub { 
		my ($opt,$arg) = @_;
		if (!defined $arg || $start < 0){
			$msg = "$opt requires track number start value. See -h.";
			main::error_handler('missing-arg',$msg,1);
		}
		$start = $arg;
		$test{'prefill'} = 1;
		$test{'autotag'} = 1;},
	'T|tag:s' => sub { 
		my ($opt,$arg) = @_;
		if (!$arg || $arg !~ /%:/){
			$msg = "$opt requires TAG%:value sets. See -h.";
			main::error_handler('missing-arg',$msg,1);
		}
		@tags = split(/\Q^^\E/, $arg);
		$run{'tag-update'} = 1;
		$run{'tagger'} = 1;
		$test{'tag'} = 1;},
	'L|taglist:s' => sub {
		my ($opt,$arg) = @_;
		if ($arg){
			if ($arg =~ /^[acfisw]+$/){
				$run{'taglist-autotag'} = 1 if $arg =~ /a/;
				$run{'taglist-condensed'} = 1 if $arg =~ /c/;
				$run{'taglist-full'} = 1 if $arg =~ /f/;
				$run{'taglist-info'} = 1 if $arg =~ /i/;
				$run{'taglist-skip'} = 1 if $arg =~ /s/;
				$run{'taglist-write'} = 1 if $arg =~ /w/;
				if ($run{'taglist-autotag'} && !$run{'taglist-full'}){
					$run{'taglist-condensed'} = 1;
				}
			}
			else {
				main::error_handler('missing-arg',"$opt unsupported argument. See -h.",1);
			}
		}
		else {
			$run{'taglist-full'} = 1;
		}
		$run{'taglist'} = 1;
		$test{'info'} = 1;},
	'taglist-file|tlf|tf:s' => sub { 
		my ($opt,$arg) = @_;
		main::error_handler('missing-arg',"$opt requires file name.",1) if !$arg;
		$TAGLIST_FILE = $arg;
		$test{'info'} = 1;
		$test{'taglist-file'} = 1;},
	'unique:s' => sub { 
 		my ($opt,$arg) = @_;
		main::error_handler(
		'missing-arg',"$opt requires comma separated list of tags.",1) if !$arg;
 		$run{'autotag-unique'} = $arg;},
	'U|update:i'=> sub { 
		my ($opt,$arg) = @_;
		$run{'update'} = 1; 
		$run{'update-type'} = $arg if $arg; 
		$test{'update'} = 1;},
	# legacy
	'verbose'=> sub { 
		$VERBOSITY = 2;},
	'v|verbosity|log:i' => sub { 
		my ($opt,$arg) = @_;
		main::error_handler('missing-arg',"$opt verbosity integer.",1) if not defined $arg;
		$VERBOSITY = $arg;},
	'version' => sub { 
		main::show_version(); },
	'z-min-size:i' => sub { 
		my ($opt,$arg) = @_;
		if (!defined $arg){
			$msg = "$opt requires minimum size in KiB. See -h.";
			main::error_handler('missing-arg',$msg,1);
		}
		else {
			$Z_MIN_SIZE = $arg;
			$test{'checksum'} = 1;
			$test{'z-min'} = 1;
		}},
	'z-min-time:i' => sub { 
		my ($opt,$arg) = @_;
		if (!defined $arg){
			$msg = "$opt requires minimum time in seconds. See -h.";
			main::error_handler('missing-arg',$msg,1);
		}
		else {
			$Z_MIN_TIME = $arg;
			$test{'checksum'} = 1;
			$test{'z-min'} = 1;
		}},
	'<>' => sub {
		my ($opt) = @_;
		main::error_handler('unsupported-option', "Unsupported option: $opt",1);}
	);
	set_switches();
	verify_selections();
}

sub set_switches {
	# Test to see if sync should be enabled/disabled
	if ($run{'aggregate'} || $test{'autotag'} || $test{'checksum'} || 
	$test{'info'} || $test{'tag'} || $test{'update'}){
		$run{'clean'} = 0;
		$run{'sync'} = 0;
		$b_check_dest = 0 if !$run{'aggregate'};
		$b_check_out = 0;
		$OUTPUT_TYPE = 'UNSET';
	}
	else {
		$run{'sync'} = 1;
	}
}

sub verify_selections {
	##############################
	## Destination/Source Tests ##
	##############################
	# aggregate requires destination and cannot be used with any other action
	if ($run{'aggregate'} && !$test{'destination'}){
		main::error_handler('invalid-options', 
		 "--aggregate requires destination.",1);
	}
	# prevent tagging/checksum operations without explicitly set source
	if (!$test{'source'} && ($run{'aggregate'} || $test{'autotag'} || 
	$test{'checksum'} || $test{'info'} || $test{'tag'})){
		main::error_handler('invalid-options', 
		 "explicit --source is required for all checksum/info/tagging options.",1);
	}
	if ($run{'source-glob'} && ! -e $SOURCE_DIRECTORY){
		main::error_handler('invalid-options', 
		 "--source-glob/-g must be used with valid --source/-s.",1);
	}
	if ($run{'source-glob'} && ($run{'clean'} || $run{'prefill'})){
		main::error_handler('invalid-options', 
		 "--source-glob/-g cannot be used with --clean/--prefill.",1);
	}
	###############################
	## Invalid Combination Tests ##
	###############################
	## Specific Run Alone ##
	# aggregate only run alone
	if ($run{'aggregate'} && ($test{'autotag'} || $test{'checksum'} || 
	$test{'info'} || $test{'sync'} || $test{'tag'} || $test{'update'})){
		main::error_handler('invalid-options', 
		 "--aggregate cannot be combined with any other actions.",1);
	}
	# update only run alone
	if ($run{'update'} && ($run{'aggregate'} || $test{'autotag'} || 
	$test{'checksum'} || $test{'info'} || $test{'sync'} || $test{'tag'})){
		main::error_handler('invalid-options', 
		 "--update cannot be combined with any other actions.",1);
	}
	## Most General To Less General ##
	# prevent running any syncing options with tagging/checksum/info options
	if ($test{'sync'} && ($test{'autotag'} || $test{'checksum'} || 
	$test{'info'} || $test{'tag'})){
		main::error_handler('invalid-options', 
		 "checksum/tagging/info options cannot be used with sync options.",1);
	}
	# checksum can be run with taggingr: -AD -AK but not with info/tag-create
	if ($test{'checksum'} && ($test{'autotag'} || $test{'info'})){
		main::error_handler('invalid-options', 
		 "checksum options cannot be used with info/prefill/tag-create options.",1);
	}
	# creating auto.tag cannot be combined with info, tagging options
	if ($test{'autotag'} && ($test{'info'} || $test{'tag'})){
		main::error_handler('invalid-options', 
		 "tag-create/prefill options cannot be used with info/image/tagging options.",1);
	}
	# analyze options cannot be used with checksum/verify options
	if ($run{'analyze'} && ($test{'checksum-generate'} || $run{'checksum-verify'} || 
	$test{'tag'})){
		main::error_handler('invalid-options', 
		 "you cannot use --analyze with other checksum or tagging options.",1);
	}
	# You cannot run infofix with anything else 
	if ($run{'infofix'} && ($run{'taglist'} || $test{'tag'})){
		main::error_handler('invalid-options', 
		 "you cannot use --infofix with checksum/taglist/tagging options.",1);
	}
	# You cannot run taglist with anything else 
	if ($run{'taglist'} && ($run{'infofix'} || $test{'tag'})){
		main::error_handler('invalid-options', 
		 "you cannot use --taglist with checksum/infofix/tagging options.",1);
	}
	## Specific Not Allowed Combos ##
	# there's no tests to run if you block md5 and ffp!
	if (($test{'checksum'} || $run{'infofix-verify'}) && $run{'no-ffp'} && 
	$run{'no-md5'}){
		main::error_handler('invalid-options', 
		 "you cannot use both --no-ffp and --no-md5 with checksum options.",1);
	}
	# duplicates/checksum-ffps uses ffps
	if (($run{'duplicates'} || $run{'checksum-ffps'}) && $run{'no-ffp'}){
		main::error_handler('invalid-options', 
		 "you cannot use --no-ffp with --duplicates or --checksum-verify.",1);
	}
	if ($run{'taglist-condensed'} && $run{'taglist-full'}){
		main::error_handler('invalid-options', 
		 "you cannot use --taglist=c with --taglist=f.",1);
	}
	if ($run{'taglist-skip'} && $run{'taglist-write'}){
		main::error_handler('invalid-options', 
		 "you cannot use --taglist=s with --taglist=w.",1);
	}
	#################################
	## Required Option Missing Combo Tests ##
	#################################
	if ($run{'autotag-unique'} && !$run{'autotag-create'} && !$run{'autotagger'}){
		main::error_handler('invalid-options', 
		 "--unique requires --autotag-create/-A.",1);
	}
	if ($test{'autotag-file'} && !$run{'autotagger'} && !$run{'autotag-create'} &&
	!$run{'taglist-autotag'}){
		main::error_handler('invalid-options', 
		 "--autoag-file can only be used with -A/-C/-L/-M/-S.",1);
	}
	if ($test{'dither'} && !$run{'resample'}){
		main::error_handler('invalid-options', 
		 "--dither requires --resample.",1);
	}
	if ($run{'image-remove'} && !$run{'autotagger'} && !$run{'image-embed'}){
		main::error_handler('invalid-options', 
		 "--image-remove/-R requires -A/-I.",1);
	}
	if ($run{'image-embed'} && !$run{'image-remove'} && !$image_embed){
		main::error_handler('invalid-options', 
		 "--image requires either image/remove or -R.",1);
	}
	# info-file requires either prefill, taglist-info, or infofix options
	if ($test{'info-file'} && !$run{'prefill'} && !$run{'taglist-info'} && 
	!$run{'infofix'}){
		main::error_handler('invalid-options', 
		 "--info-file/--prefill-file require -L i/--infofix/--prefill.",1);
	}
	# prefill options require tag-create options
	if ($test{'prefill'} && !$run{'autotag-create'}){
		main::error_handler('invalid-options', 
		 "prefill options require an autotag-create option (-C,-M,-S).",1);
	}
	if ($test{'info-rating'} && !$run{'infofix-quality'}){
		main::error_handler('invalid-options', 
		 "--info-rating can only be used with --infofix [qv].",1);
	}
	if ($run{'no-ssl'} && !$run{'update'}){
		main::error_handler('invalid-options', 
		 "--no-ssl can only be used with --update.",1);
	}
	if (($run{'remove-image'} || $test{'remove-padding'}) && !$run{'tagger'}){
		main::error_handler('invalid-options', 
		 "remove image/padding can only be used with --image/--autotag.",1);
	}
	if ($test{'taglist-file'} && !$run{'taglist-full'} && 
	!$run{'taglist-condensed'}){
		main::error_handler('invalid-options', 
		 "--taglist-file can only be used with -L/-L [cf].",1);
	}
	if ($test{'z-min'} && !$run{'analyze'}){
		main::error_handler('invalid-options', 
		 "--z-min-time/--z-min-size can only be used with --analyze.",1);
	}
	##################################################
	## Invalid Data - Usually Handled in Validation ##
	##################################################
	if ($run{'update-type'} && $run{'update-type'} != 3){
		main::error_handler('invalid-options', 
		 "-U 3 is the only alternate update type currently supported.",1);
	}
}
}

#########################################################################
### EXECUTE / PROCESS MUSIC ###
#########################################################################

main();

###**EOF**###
