#!/usr/bin/perl
use strict;
use warnings;

# takes a bunch of mp3 files (expected to be from the same album) and syncs
# common tags.
#
# In other words: if one file has a picture, all the other will.
#
# This is done for the following tags:
# artist, album and picture

use IO::File;
use Music::Tag;
use MP3::Tag;
use MP3::Tag::ID3v2;

# if set to 1, don't ask any question, sync all
my $AUTO_SYNC = 0;

my @SYNC = qw(album artist picture);
my $DEFAULT = {};
my @PROCESSED = ();

#  local subs
sub read_stdin() {
    if ($AUTO_SYNC) {
        print "\n";
        return "";
    }
    my $r = <STDIN>;
    chomp($r);
    return $r;
}

sub find_cover($) {
    my ($dir) = @_;
    my $cover = undef;

    opendir LOCALDIR, $dir or die $!;
    foreach my $f (readdir(LOCALDIR)) {
        next if $f eq '.' or $f eq '..';
        if (-f $f && $f =~ /\.(je?pg|png)/i) {
            $cover = $f;
        }
    }
    closedir LOCALDIR;
    return $cover;
}

sub init_defaults {
    foreach my $tag (@SYNC) {
        # init the key
        $DEFAULT->{$tag} = undef;
        # if an image file is found, set it as a default value for the cover
        $DEFAULT->{$tag} = find_cover('.') if ($tag eq 'picture');
    }
}

sub binslurp {
    my $file = shift;
    my $fh = IO::File->new("<$file") || die "$file: $!\n";
    local $/ = undef;    # file slurp mode
    my $data = <$fh>;
    return $data;
}

# The good way to add a picture frame to the ID3v2 tags of the mp3
sub set_picture_to_mp3 ($$) {
    my ($mp3_file, $picture_file) = @_;
    return undef unless defined $mp3_file and defined $picture_file;
    print "Setting picture \"$picture_file\" for file \"$mp3_file\".\n";

    my $mp3 = MP3::Tag->new($mp3_file);
    $mp3->get_tags;

    $mp3->{ID3v2}->remove_frame('APIC') if defined $mp3->{ID3v2}->get_frame('APIC');
    $mp3->{ID3v2}->add_frame('APIC',
        chr(0x0),                   # Text Encoding
        mime_type($picture_file),   # MIME Type
        chr(0x3),                   # Picture Type
        $picture_file,              # Description
        binslurp($picture_file)     # Binary Data
    );
    # save changes to the file
    eval { $mp3->{ID3v2}->write_tag() };
    if ($@) {
        print "! error saving file $mp3_file: $@\n"
    }
}

sub set_defaults {
    foreach my $tag (keys %$DEFAULT) {
        
        if ($tag eq 'picture') {
            $DEFAULT->{$tag} = find_cover('.') || $DEFAULT->{$tag};
        }

        if (defined $DEFAULT->{$tag}) {
            print "$tag (".(ref $DEFAULT->{$tag} ? 'object defined' : $DEFAULT->{$tag})."): ";
            $DEFAULT->{$tag} = read_stdin() || $DEFAULT->{$tag};
        }
        else {
            print "$tag: ";
            $DEFAULT->{$tag} = read_stdin();
        }
    }
}
sub confirm_defaults {
    print "Default values: \n";
    foreach my $tag (keys %$DEFAULT) {
        print "\t- $tag: ".(defined $DEFAULT->{$tag} ? $DEFAULT->{$tag} : 'undef')."\n";
    }
    print "OK ? (y/n): ";
    my $ok = read_stdin;
    return 1 if $ok eq 'y' || $ok eq '';
    return 0;
}

sub load_mp3_meta($) {
    my $file = shift;
    return unless -r $file;
    return unless $file =~ /\.mp3$/;
    
    my $meta = Music::Tag->new($file, {quiet => 1}, 'MP3');
    $meta->get_tag;

    foreach my $tag (@SYNC) {
        $DEFAULT->{$tag} = $meta->$tag if defined $meta->$tag;
    }

    push @PROCESSED, $file;
}

sub mime_type($) {
    my $filename = shift;
    my $mime_type;

    chomp $filename;
    $mime_type = 'image/jpeg' if $filename =~ /\.jpe?g$/i;
    $mime_type = 'image/png' if $filename =~ /\.png$/i;

    return $mime_type;
}

sub sync_tags($) {
    my $file = shift;
    
    my $meta = Music::Tag->new($file, {quiet => 1}, 'MP3');
    $meta->get_tag;
    
    foreach my $tag (@SYNC) {
        if (! defined $DEFAULT->{$tag}) {
            print "! Cannot sync tag $tag (no default value avalaible)\n";
            next;
        }
    
        # the picture is always overwritten
        if ($tag eq 'picture') {
            # if the default is a Music::Tag object, just copy
            if (ref $DEFAULT->{$tag}) {
                $meta->picture( $DEFAULT->{$tag} );
                $meta->set_tag();
                $meta->close;
            }
            # else, set the picture as a filename
            else {
                set_picture_to_mp3($file, $DEFAULT->{$tag});
            }
        }

        # other tags can be compared
        else {
            if ($DEFAULT->{$tag} ne $meta->$tag) {
                print "- setting tag \"$tag\" to \"".$DEFAULT->{$tag}."\" for file \"$file\".\n";
                $meta->$tag( $DEFAULT->{$tag} );
                $meta->set_tag();
                $meta->close;
            }
        }
    }
    
}

sub check_for_enough_files() {
    if ( scalar(@PROCESSED) < 2 ) {
        print "Not enough mp3 file given, cannot sync with less than 2 files.\n";
        exit 10;
    }
}

sub set_id3v2($) {
    my ($file) = @_;
    my $mp3 = MP3::Tag->new($file);
    $mp3->get_tags;
    unless (exists $mp3->{ID3v2}) {
        print "No ID3v2 tag in file \"$file\", creating one.\n";
        $mp3->new_tag("ID3v2");
        $mp3->{ID3v2}->add_frame("TALB", ""); # one frame for sanity
        $mp3->{ID3v2}->write_tag;
    }
    undef $mp3;
}

###################################################"
# main
init_defaults();

# We have to read every files first
my @files = @ARGV;
foreach my $mp3_file (@files) {
    # in order to be sure ID3v2 is there
    set_id3v2($mp3_file);
    # then load default values
    load_mp3_meta($mp3_file);
}

check_for_enough_files();

# set default values for the sync
set_defaults();

# then we can sync all files
foreach my $mp3 (@PROCESSED) {
    # then sync the tags
    sync_tags($mp3);
}