#!/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);
}