#!/usr/bin/perl
=head MP3 Deduplicator
 
=cut #--------------------------------------------------------------------------
#===============================================================================
#                              MODULES AND PRAGMAS
#===============================================================================
use strict; use warnings;
use File::Basename;
use File::Find    ;
use File::Spec    ;
use Cwd           ;
use MP3_Dedupe_Dir;
use MP3::Info     ; # Used to obtain bitrate info from MP3 files.
#-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-

#===============================================================================
#                                   VARIABLES
#===============================================================================
my $TRUE  = 1;
my $FALSE = 0;
my $ts = basename $0; # 'ts' = "this script"
my @music_patt = '([mM][pP]3|[mM]4[aA])'; # Finds .mp3 and m4a suffixes.
#-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-

#===============================================================================
#                                  SUBROUTINES
#===============================================================================
sub complain_about_non_dir_inputs { #-------------------------------------------
=head DESCRIPTION
=head INPUT
=head OUTPUT
=head NOTES
=cut #--------------------------------------------------------------------------
    my @not_dirs = @_;
    return if $#not_dirs == -1;
    warn "$ts: these arguments are not directories: "    .
        join( ', ', map { "\"" . $_ . "\"" } @not_dirs ) .
        "\n"                                             ;
    exit 1;
}

sub determine_whether_args_are_dirs { #-----------------------------------------
=head DESCRIPTION
=head INPUT
=head OUTPUT
=head NOTES
=cut #--------------------------------------------------------------------------
    my @dirs_to_search = grep {   -d $_ } @_;
    my @not_dirs       = grep { ! -d $_ } @_;
    complain_about_non_dir_inputs @not_dirs ;
    push(@dirs_to_search, $ENV{"PWD"}) if $#dirs_to_search == -1;
    return @dirs_to_search;
}

sub get_dirs { #----------------------------------------------------------------
=head DESCRIPTION
=head INPUT
=head OUTPUT
=head NOTE
 note1: Without this option $File::Find::name refers to the "full path" (which
        is, by the way, not the "fully-qualified path") but is inside of the
        directory hierarchy (by virtue of changing dirs), so the path contained
        in $File::Find::name contains the current dir, where the program has
        temporarily situated itself, meaning the full path is not accurate for
        purposes of a -d test. Here's an example:
        I have dir structure X/Y/Z. If $File::Find::name is X/Y and we've done
        a chdir to X/Y, then -d will not pass for X/Y/Z, but it will for Z.
        Ultimately though, the easiest thing to do is turn off directory
        traversal.
 note2: We're taking note of the absolute paths to each directory rather than
        relative paths since we'll ultimately want absolute paths for our 
        outputs. 
        Also, rel2abs seems to absolutely require the parentheses around its 
        args. Alas, I do not know why. Seems to be common to any module-level 
        calls to subroutines.
 NOTE : We're doing some redundant work here because the dir class is going to
        do another round of reads beyond the find() call in this sub. 
        Oh well! So it goes. Only fix that if it significantly impedes 
        performance.
=cut #--------------------------------------------------------------------------
    my @dirs_to_search = @_ ;
    my $find_sequence  =  1 ; # Order in which directories were found.
    my %found          = ( ); # Notes directories found and order of discovery.
    my @subdirs        = ( ); 
    find( { "no_chdir" => 1, #note1
        "wanted" => sub { 
            my $current_file = $File::Find::name;
            if( -d $current_file && ! defined( $found{$current_file} ) ){
                push( @subdirs, File::Spec->rel2abs( $current_file ) ); #note2
                $found{ $current_file } = $find_sequence++;
            }
        } }, 
        @dirs_to_search  
    );
    return sort { $a cmp $b } @subdirs;
}

sub get_all_dirs { #------------------------------------------------------------
=head DESCRIPTION
=head INPUT
=head OUTPUT
=head NOTES
=cut #--------------------------------------------------------------------------
    my @dirs_to_search = determine_whether_args_are_dirs @_;
    my @dirs_found     = get_dirs @dirs_to_search;
    return @dirs_found;
}

sub get_empty_and_branch_dirs { #-----------------------------------------------
=head DESCRIPTION
=head INPUT
=head OUTPUT
=head NOTES
=cut #--------------------------------------------------------------------------
    my @dirs     = @_;
    my @empties  = ();
    my @branches = ();
    foreach my $dir ( @dirs ){
        push(@empties , $dir->get_name ) if $dir->is_empty ;
        push(@branches, $dir->get_name ) if $dir->is_branch;
    }
    return( \@empties, \@branches );
}

sub analyze_dirs { #------------------------------------------------------------
=head NOTES
 All analysis is performed inside of the MP3_Dedupe_Dir class's new() sub
=cut #--------------------------------------------------------------------------
    my @dir_names = @_;
    my @dirs      = map { MP3_Dedupe_Dir->new( $_ ) } @dir_names;
    return @dirs;
}

sub get_interesting_dirs { #----------------------------------------------------
    my @dirs             = @_;
    my @interesting_dirs = ();
    foreach my $dir ( @dirs ){
        if( ! $dir->is_empty && ! $dir->is_branch ){
            push @interesting_dirs, $dir;
        }
    }
    return @interesting_dirs;
}

sub interesting_mp3_dirs_only { #-----------------------------------------------
=head DESCRIPTION
 The previous definition of interesting dirs consist of any dirs that are
 not empty and are not branch dirs, but for many queries it pays to narrow that
 criteria down, particularly for any queries that only operate on MP3s. That
 being the case, this sub takes a list of MP3_Dedupe_Dir objects with analyzed
 MP3_Dedupe_Filesets and filters them down to only those containing mp3s.
=head NOTES
 note1: This does little more than spare us the pain of an if/else statement.
        Notice how we give the outcomes of the ternary operator as references
        to arrays rather than bare arrays. That distinction appears to be
        necessary.
=cut #--------------------------------------------------------------------------
    my @dirs = @_; #presumed to have been judged interesting already.
    my @has_mp3s = ();
    my @no_mp3s  = ();
    foreach my $dir ( @dirs ){
        my @mp3s = $dir->get_fileset->get_mp3s;
        push( @mp3s ? \@has_mp3s : \@no_mp3s, $dir ); #note1
    }
    return @has_mp3s;
}

sub analyze_dir_contents { #----------------------------------------------------
    map { $_->analyze } @_;
}

sub find_dirs_with { #----------------------------------------------------------
=head NOTES
 note1: What we are calling here is a member function presumably from fileset. 
        Since we are calling it using a generalized syntax we must explicitly 
        pass it a $self, which will be the fileset of each dir called.
=cut #--------------------------------------------------------------------------
    my $get_sub = shift;
    my @dirs    = @_   ;
    my @newdirs = ();
    foreach my $dir( @dirs ){
        if( $get_sub->( $dir->get_fileset ) ){ #note1
            push( @newdirs, $dir );
        }
    }
    return @newdirs;
}

sub find_dirs_with_non_music_files { #------------------------------------------
    my @newdirs = find_dirs_with( \&MP3_Dedupe_Fileset::get_non_music, @_ );
    return @newdirs;
}

sub find_X_files { #------------------------------------------------------------
    my $get_sub = shift;
    my @dirs    = @_   ;
    my @files   = map  { 
        my $dir = $_;
        (map {$dir->get_name."/".$_;} $get_sub->($dir->get_fileset) );
    } @dirs; #We append the fully-qualified paths to each filename.
    return @files;
}

sub find_non_music_files { #----------------------------------------------------
    my @files = find_X_files( \&MP3_Dedupe_Fileset::get_non_music, @_ );
    return @files;
}

sub find_dirs_with_m4as { #-----------------------------------------------------
    my @newdirs = find_dirs_with( \&MP3_Dedupe_Fileset::get_m4as, @_ );
    return @newdirs;
}

sub find_m4as { #---------------------------------------------------------------
    my @files = find_X_files( \&MP3_Dedupe_Fileset::get_m4as, @_ );
    return @files;
}

sub find_dirs_with_oggs { #-----------------------------------------------------
    my @newdirs = find_dirs_with( \&MP3_Dedupe_Fileset::get_oggs, @_ );
    return @newdirs;
}

sub find_oggs { #---------------------------------------------------------------
    my @files = find_X_files( \&MP3_Dedupe_Fileset::get_oggs, @_ );
    return @files;
}

sub find_mp3s_with_abnormal_suffix { #------------------------------------------
    my @files = find_X_files( \&MP3_Dedupe_Fileset::get_abnormal_mp3s, @_);
    return @files;
}

sub find_X_diff { #-------------------------------------------------------------
    my $get_sub = shift;
    my @dirs    = @_   ;
    my %diffs   = ()   ;
    foreach my $dir (@dirs){
        my %diff = $get_sub->($dir->get_fileset);
        if( scalar(keys(%diff)) ){ 
        #-----------------------------------------------------------------------
        # We only bother to note directories containing results for the diff.
        # All other directories are dropped as irrelevant.
        #-----------------------------------------------------------------------
            $diffs{ $$dir{"name"} } = \%diff;
        }
    }
    return %diffs; #%%%
    # %dirname -> %same criteria -> % filename => diff criteria
}

sub find_cs_w_diff_MD5s { #-----------------------------------------------------
# Implies either damaged files or difference in bitrate.
    my %same_cs_diff_md5 = find_X_diff( 
        \&MP3_Dedupe_Fileset::get_cs_w_diff_MD5s, @_ );
    return %same_cs_diff_md5; #
    #
}

sub find_MD5s_w_diff_cs { #-----------------------------------------------------
# Probably means two copies of the same file with wildly different file names.
    my %same_md5_diff_cs = find_X_diff(
        \&MP3_Dedupe_Fileset::get_MD5s_w_diff_cs, @_ );
    return %same_md5_diff_cs;
}

sub find_repeat_MD5s_across_dirs { #--------------------------------------------
    my @dirs       = @_;
    my %md5s_found = ();
    my %recurrent  = ();
    #---------------------------------------------------------------------------
    # Take note of every MD5, the dir(s) containing it, and the files in that
    # dir using a hash of hashes of arrays %%@.
    #---------------------------------------------------------------------------
    foreach my $dir ( @dirs ){
        my $fs   = $dir->get_fileset;
        my %files = $fs->group_mp3s_by_MD5;
        foreach my $md5 ( keys %files ){
            $recurrent{$md5} = $recurrent{$md5} // { };
            $recurrent{$md5}->{$dir->get_name} = $files{$md5};
        }
    }
    #---------------------------------------------------------------------------
    # Now delete entries for any MD5 hash that only corresponds to a single
    # directory. This sub is only interested in hashes with 2+ directories.
    #---------------------------------------------------------------------------
    foreach my $md5 ( keys %recurrent ){
        if( scalar(keys(%{$recurrent{$md5}})) <= 1 ){
            delete $recurrent{$md5};
        }
    }
    return %recurrent; #%%@ MD5->dirname->filenames
}

sub classify_mp3s_to_keep { #---------------------------------------------------
    my @dirs = @_;
    my %classified = ();
    foreach my $dir ( @dirs ){
        my $fs   = $dir->get_fileset;
        my $name = $dir->get_name;
        my %keep = $fs->get_shortest_from_common_substrings;
        my %lose = $fs->get_longest_from_common_substrings;
        $classified{$name} = { 
            "keep" => [ sort map { $keep{$_} } keys %keep    ],
            "lose" => [ sort map { @{$lose{$_}} } keys %lose ],
        };
    }
    return %classified; #%%@
    # %directory name -> %keep/lose -> @file lists for that dir
}

sub filter_dirs_without_losers { #----------------------------------------------
    my %dirs     = @_;
    my %filtered = (); #these have dupes, we want them.
    my %rejects  = (); #these lack dupes, we shun them.
    foreach my $dir ( sort keys %dirs ){
        my @losers = @{ $dirs{$dir}->{"lose"} };
        if( @losers ){
            $filtered{$dir} = $dirs{$dir};
        }
        else{
            $rejects{$dir} = $dirs{$dir};
        }
    }
    return %filtered;
}

sub banner { #------------------------------------------------------------------
    my $banner_char     = shift ;
    my @lines           = @_    ;
    my $string          = '#' . $banner_char x 79 . "\n";
    my @lines_formatted = map { "# " . $_ . "\n" } @lines;
    my $final           = join ('', $string, @lines_formatted, $string );
    return $final;
}

sub dump_array { #--------------------------------------------------------------
    my $filename = shift;
    my @to_dump  = @_;
    open( my $FH, ">", $filename ) or 
        die "$ts: failed to open file \"$filename\" for writing!\n";
    foreach my $i ( @to_dump ){
        print $FH "$i\n";
    }
    close( $FH ) or die "$ts: failed to close \"$filename\" after write\n";
}

sub dump_dirnames { #-----------------------------------------------------------
    my $filename = shift;
    my @dirs     = @_;
    my @dirnames = map { $_->get_name . "/" } @dirs; 
    dump_array( $filename, @dirnames );
}

sub get_length_of_longest { #---------------------------------------------------
    my @files = @_;
    my @filename_lengths = sort { $b <=> $a } map { 
        length(Encode::encode_utf8($_)) } @files;
    my $longest = $filename_lengths[0];
    return $longest;
}

sub dump_diff { #---------------------------------------------------------------
=head NOTES
 note1: We want our filenames to appear in a format such that the factors
        differentiating them appear as a comment on the right side. However,
        since filenames have different lengths we want to ensure that our 
        comment blocks are formatted so that all comments begin at exactly the
        same columnar width for every single file in a given directory. To do
        that we simply format the filenames so that they all occupy as much 
        space as the longest filename.
=cut #--------------------------------------------------------------------------
    my $filename = shift;
    my %diff     = @_;
    open( my $FH, ">", $filename ) or 
        die "$ts: failed to open file \"$filename\" for writing!\n";
    foreach my $dir ( sort { $a cmp $b } keys %diff ){
        my $pdir = banner "=", $dir;
        foreach my $same ( sort keys %{ $diff{$dir}} ){
            my @files   = sort keys %{$diff{$dir}->{$same}};
            my $longest = get_length_of_longest @files; #note1
            my @diffs   = map { $diff{$dir}->{$same}->{$_} } @files;
            my @plines  = ();
            for( my $i=0 ; $i <= $#files; $i++ ){
                push( @plines, sprintf '%-'.$longest."s  #%s\n", #note1
                    $files[$i], $diffs[$i] );
            }
            print $FH $pdir.join('',@plines);
        }
    }
    close( $FH ) or die "$ts: failed to close \"$filename\" after write\n";
}

sub sprint_mp3_bitrate_info { #-------------------------------------------------
=head NOTES
 note1: We should probably think of a better way to deal with files that do not
 return bitrate information. If they don't do that then they're probably not
 real mp3 files. However, this possibility is so remote (though it happens in
 our test data) that we probably shouldn't get too bent out of shape about it.
 That being the case we'll just settle for putting question marks into the
 fields in the event that we cannot determine mp3info for a given file.
=cut #--------------------------------------------------------------------------
    my @filenames = @_;
    my @info      = ();
    foreach my $file ( @filenames ){
        my $info = get_mp3info( $file )
            or die "$ts: Cannot get mp3info for \"$file\"! $!\n";
        push( @info, sprintf "vbr=\"%1s\" bitrate=\"%-3s\"\%s", 
            $info->{"VBR"} // "?", $info->{"BITRATE"} // "???", #note1
            defined($info->{"VBR"})                    ?
            $info->{"VBR"} == $TRUE                    ?
            " vbr_scale= \"".$info->{"VBR_SCALE"}."\"" :
            "" : ""                                   );
    }
    return @info;
}

sub dump_bitrate_comparison_diff { #--------------------------------------------
    my $filename = shift;
    my %diff     = @_;
    open( my $FH, ">", $filename ) or 
        die "$ts: failed to open file \"$filename\" for writing!\n";
    foreach my $dir ( sort { $a cmp $b } keys %diff ){
        my $pdir = banner "=", $dir;
        foreach my $same ( sort keys %{ $diff{$dir}} ){
            my @files    = sort keys %{$diff{$dir}->{$same}};
            my $longest  = get_length_of_longest @files; #note1
            my @bitrates = sprint_mp3_bitrate_info( map { "$dir/".$_ } @files );
            my @plines   = ();
            for( my $i=0 ; $i <= $#files; $i++ ){
                push( @plines, sprintf '%-'.$longest."s  #%s\n", #note1
                    $files[$i], $bitrates[$i] );
            }
            print $FH $pdir.join('',@plines);
        }
    }
    close( $FH ) or die "$ts: failed to close \"$filename\" after write\n";
}

sub dump_recurrent { #----------------------------------------------------------
=head NOTES
 The preprended 'p' means 'formatted for printing'. 
 It ain't no collar-popper, baby.
=cut #--------------------------------------------------------------------------
    my $filename = shift;
    my %repeats  = @_;
    open( my $FH, ">", $filename ) or 
        die "$ts: failed to open file \"$filename\" for writing!\n";
    foreach my $rep ( sort keys %repeats ){
        my $prep = banner '=', $rep; #note1
        my @dirs = sort keys %{$repeats{$rep}};
        my @files = map { my $dir = $_; 
            my @files_from_dir = @{$repeats{$rep}->{$dir}};
            sort map { $dir . "/" . $_ } @files_from_dir;
        } @dirs;
        print $FH $prep.join("\n", @files)."\n";
    }
    close( $FH ) or die "$ts: failed to close \"$filename\" after write\n";
}

sub dump_mp3_lists { #----------------------------------------------------------
    my $filename = shift;
    my $sets     = shift;
    my $commented= shift;
    my $print_dir_header = shift;
    my @sets     = @{ $sets }; #keys to check in middle hash, the sets.
    my @c_sets   = @{ $commented };
    #  ^ keys from middle hash whose list entries should be commented out.
    my %dirs     = @_;
    open( my $FH, ">", $filename ) or
        die "$ts: failed to open file \"$filename\" for writing!\n";
    #---------------------------------------------------------------------------
    # Sift through each directory in Perl's default sort order (i.e. cmp).
    #---------------------------------------------------------------------------
    foreach my $dir ( sort keys %dirs ){
        my $pdir = banner "-", $dir;
        print $FH $pdir if $print_dir_header;
        my @files = ();
        my @files_to_comment = ();
        #-----------------------------------------------------------------------
        # First take the files for this dir from all of the dir's sets and 
        # collect them into one list for sorting.
        #-----------------------------------------------------------------------
        foreach my $set ( @sets ){
            push( @files, @{ $dirs{$dir}->{$set} } );
            push( @files_to_comment, @{ $dirs{$dir}->{$set}}) 
                if( grep { $set eq $_ } @c_sets );
        }
        @files = sort @files;

        #-----------------------------------------------------------------------
        # Second, prepend the directory name to each file name, preserving the
        # filename sort sequence. Also, comment any files from sets that have
        # been designated to be commented out.
        #-----------------------------------------------------------------------
        my @pfiles;
        foreach my $file ( @files ){
            my $com = scalar(grep { $file eq $_ } @files_to_comment) ?
                $TRUE : $FALSE                                       ;
            push( @pfiles, sprintf("%s%s/%s\n", $com ? "#" : '', $dir, $file));
        }
        print $FH join('', @pfiles);
    }
    close( $FH ) or die "$ts: failed to close \"$filename\" after write\n";
}

sub mk_out_dir { #--------------------------------------------------------------
    if( ! -d "out" ){
        mkdir "out" or 
            die "$ts: Cannot create dir \"out\" in $ts ".__LINE__." $!\n";
    }
    my @date = localtime( time );
    my $year = 1900 + $date[5];
    my $mon  = $date[4] + 1;
    my $ts   = sprintf '%04d%02d%02d_%02d%02d%02d',
        $year   , $mon    , $date[3],
        $date[2], $date[1], $date[0];
    my $ndir = "out/".$ts;
    mkdir $ndir if -d $ndir;
    return $ndir;
}

sub dump_analyses { #-----------------------------------------------------------
    my %a = @_;
    #---------------------------------------------------------------------------
    # Fix up an output directory for the current scan.
    #---------------------------------------------------------------------------
    my $outd = mk_out_dir;
    if( ! -d $outd ){
        mkdir $outd or 
            die "$ts: Cannot create dir \"$outd\" in $ts ".__LINE__." $!\n";
    }
    chdir($outd) or 
        die "$ts: Could not change to \"$outd\" dir at ".__LINE__." $!\n";
    #---------------------------------------------------------------------------
    # Perform the Simpler Dumps (simplicity, in this case, being a function of
    # of the functions involved in this module and the number of args they take,
    # rather than the
    #---------------------------------------------------------------------------
    my @arrays = ( "m4as", "oggs", "abnormals", );
    my @dirs   = ( "m4a_dirs", "ogg_dirs", "mp3_dirs", );
    my @diffs  = ( "vbr_or_dmg", "misnamed" );
    #---------------------------------------------------------------------------
    # Dump the names and locations of files with simple criteria, including the
    # non mp3 music formats m4a and ogg, as well as MP3s with file format
    # suffixes containing upper-case characters (which we have referred to as
    # abnormals). These dumps will be simple lists of fully-qualified paths to
    # files in some sort of alphabetical-like order.
    #---------------------------------------------------------------------------
    foreach my $ar ( @arrays ){
        dump_array( $ar.".txt", @{$a{$ar}} );
    }
    #---------------------------------------------------------------------------
    #---------------------------------------------------------------------------
    foreach my $d ( @dirs ){
        dump_dirnames( $d.".txt", @{$a{$d}} );
    }
    #---------------------------------------------------------------------------
    #---------------------------------------------------------------------------
    foreach my $diff ( @diffs ){
        dump_diff( $diff.".txt", %{$a{$diff}} );
    }
    #---------------------------------------------------------------------------
    # Dump same CS w/ diff MD5s, and include mp3 bitrate info in the comments.
    #---------------------------------------------------------------------------
    dump_bitrate_comparison_diff( "vbr_w_mp3info.txt", %{$a{"vbr_or_dmg"}} ); 
    #---------------------------------------------------------------------------
    # MD5 Dumps
    #---------------------------------------------------------------------------
    dump_recurrent "MD5_Xdir_repeats.txt", %{$a{"xdir_repeats"}};

    #---------------------------------------------------------------------------
    # MP3 Dumps
    #---------------------------------------------------------------------------
    my %mp3s_all =  %{$a{"class_mp3s"}}; # All mp3 dirs, even w/out losers
    my %mp3s_dup =  %{$a{"dupe_mp3s"}};  # Dirs containing only dupe MP3s
    dump_mp3_lists "mp3_keep.txt"         , 
        [ "keep" ], [ ], $FALSE, %mp3s_dup;
    dump_mp3_lists "mp3_lose.txt"         , 
        [ "lose" ], [ ], $FALSE, %mp3s_dup;
    dump_mp3_lists "mp3_keep_w_empty.txt" , 
        [ "keep" ], [ ], $TRUE, %mp3s_all ;
    dump_mp3_lists "mp3_lose_w_empty.txt" , 
        [ "lose" ], [ ], $TRUE, %mp3s_all ;
    dump_mp3_lists "mp3s.txt"             , 
        [ "keep", "lose" ]                , 
        [ "keep" ], $TRUE                 , 
        %mp3s_dup                         ;
}

#-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-

#===============================================================================
#                                     MAIN
#===============================================================================
=head NOTES
 note1: @interesting_dirs is modified in-place here.
=cut #==========================================================================
my @dir_names          = get_all_dirs @ARGV;
my @dirs               = analyze_dirs @dir_names;
my ( $empty, $branch ) = get_empty_and_branch_dirs @dirs;
my @branches = @{ $branch };
my @empties  = @{ $empty  };
undef $branch; undef $empty;
my @interesting_dirs   = get_interesting_dirs( @dirs );
analyze_dir_contents @interesting_dirs; #note1
my @dirs_w_non_music_files = find_dirs_with_non_music_files @interesting_dirs;
my @non_music_files        = find_non_music_files     @dirs_w_non_music_files;
my @m4a_dirs               = find_dirs_with_m4as            @interesting_dirs;
my @m4as                   = find_m4as                              @m4a_dirs;
my @ogg_dirs               = find_dirs_with_oggs            @interesting_dirs;
my @oggs                   = find_oggs                              @ogg_dirs;
my @abnormal_suffix_mp3s   = find_mp3s_with_abnormal_suffix @interesting_dirs;
my @mp3_dirs               = interesting_mp3_dirs_only      @interesting_dirs;
my %VBR_or_damaged_mp3s    = find_cs_w_diff_MD5s                    @mp3_dirs;
my %MD5s_w_differing_cs    = find_MD5s_w_diff_cs                    @mp3_dirs;
my %repeat_MD5s            = find_repeat_MD5s_across_dirs           @mp3_dirs;
my %classified_mp3s        = classify_mp3s_to_keep                  @mp3_dirs;
my %mp3_dirs_w_dupes       = filter_dirs_without_losers      %classified_mp3s;
my %analysis = (
    "m4a_dirs"     => \@m4a_dirs            ,
    "m4as"         => \@m4as                ,
    "ogg_dirs"     => \@ogg_dirs            ,
    "oggs"         => \@oggs                ,
    "abnormals"    => \@abnormal_suffix_mp3s,
    "mp3_dirs"     => \@mp3_dirs            ,
    "vbr_or_dmg"   => \%VBR_or_damaged_mp3s ,
    "misnamed"     => \%MD5s_w_differing_cs ,
    "xdir_repeats" => \%repeat_MD5s         ,
    "class_mp3s"   => \%classified_mp3s     ,
    "dupe_mp3s"    => \%mp3_dirs_w_dupes    ,
);
dump_analyses %analysis;
print STDERR "Goodbye World!\n";
#-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
#                                    THE END
#-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
