#!/usr/bin/perl use strict; use encoding ':locale'; use Fcntl ':flock'; use Readonly; use File::Basename qw(basename dirname); use MP3::Tag; use MP3::Tag::ID3v2; Readonly::Scalar my $COVER_IMAGE_FILENAME_PREFIX => 'cover'; Readonly::Scalar my $APIC_FRAME_ID => 'APIC'; Readonly::Scalar my $PICTURE_TYPE_SUBFRAME_ID => 'Picture Type'; Readonly::Scalar my $MIME_TYPE_SUBFRAME_ID => 'MIME type'; Readonly::Scalar my $PICTURE_DATA_SUBFRAME_ID => '_Data'; Readonly::Scalar my $PICTURE_TYPE_VALUE_SUBSTRING => 'Cover'; Readonly::Scalar my $THIS_SCRIPT_NAME => basename $0; Readonly::Scalar my $FILE => $ARGV[0]; my %cover_apic_frame; my $cover_image_filename; my $cover_picture_data; _ensure_file_exists(); _set_cover_apic_frame(); _set_cover_image_filename(); _set_picture_data(); _write_cover_image_file(); sub _write_cover_image_file { open my $cover_image_fh, '>', $cover_image_filename or die "Can't write cover file '${cover_image_filename} (Source: '${FILE}')!\n"; flock $cover_image_fh, LOCK_EX; binmode $cover_image_fh; print $cover_image_fh $cover_picture_data; close $cover_image_fh; } sub _set_picture_data { $cover_picture_data = $cover_apic_frame{$PICTURE_DATA_SUBFRAME_ID}; if (not defined $cover_picture_data) { _error_message_picture_data_undefined(); exit 1; } } sub _set_cover_image_filename { my $dirname = dirname $FILE; my $suffix = _get_cover_image_filename_suffix(); $cover_image_filename = "${dirname}/${COVER_IMAGE_FILENAME_PREFIX}.${suffix}"; if (-f $cover_image_filename) { _error_message_cover_image_file_already_exists(); exit 1; } } sub _get_cover_image_filename_suffix { my $mime_type = $cover_apic_frame{$MIME_TYPE_SUBFRAME_ID}; if (not defined $mime_type || length $mime_type == 0) { _error_message_mime_type_subframe_undefined(); exit 1; } my $suffix = $mime_type; $suffix =~ s%image/%%i; return $suffix; } sub _set_cover_apic_frame { my $apic_frame_refs = _get_apic_frame_refs(); if (defined $apic_frame_refs) { my @cover_apic_frame_refs = (); foreach my $apic_frame_ref (@{$apic_frame_refs}) { if (_contains_apic_frame_cover($apic_frame_ref)) { push @cover_apic_frame_refs, $apic_frame_ref; } } my $front_cover_apic_frame_ref = _get_front_cover_apic_frame(\@cover_apic_frame_refs); if (defined $front_cover_apic_frame_ref) { %cover_apic_frame = %$front_cover_apic_frame_ref; } else { _error_message_no_cover_picture_in_apic_frame(); exit 1; } } else { _error_message_cover_apic_frame_not_found (); exit 1; } } sub _get_front_cover_apic_frame { my ($cover_apic_frame_refs) = @_; foreach my $cover_apic_frame_ref (@{$cover_apic_frame_refs}) { if ($cover_apic_frame_ref->{$PICTURE_TYPE_SUBFRAME_ID} =~ m/front/i) { return $cover_apic_frame_ref; } } my $cover_apic_frame_count = @$cover_apic_frame_refs; return $cover_apic_frame_count > 0 ? $cover_apic_frame_refs->[0] : undef; } sub _contains_apic_frame_cover { my ($apic_frame_ref) = @_; return defined $apic_frame_ref->{$PICTURE_TYPE_SUBFRAME_ID} && $apic_frame_ref->{$PICTURE_TYPE_SUBFRAME_ID} =~ m/$PICTURE_TYPE_VALUE_SUBSTRING/i; } sub _get_apic_frame_refs { my $mp3_tag_reader = MP3::Tag->new($FILE); $mp3_tag_reader->get_tags(); if (exists $mp3_tag_reader->{ID3v2}) { my $id3v2_tag_reader = $mp3_tag_reader->{ID3v2}; my @frames = keys %{$id3v2_tag_reader->get_frame_ids()}; my @apic_frame_refs = (); foreach my $frame (@frames) { if ($frame =~ m/^${APIC_FRAME_ID}*/) { my $apic_frame_ref = $id3v2_tag_reader->get_frame($frame); if (defined $apic_frame_ref && ref $apic_frame_ref eq "HASH") { push @apic_frame_refs, $apic_frame_ref; } } } my $apic_frame_count = @apic_frame_refs; return $apic_frame_count > 0 ? \@apic_frame_refs : undef; } return undef; } sub _ensure_file_exists { die "${THIS_SCRIPT_NAME}: File '${FILE}' does not exist (command line parameter)!\n" if not -f $FILE; } sub _error_message_mime_type_subframe_undefined { print "${THIS_SCRIPT_NAME}:" . " Did not find image's '${MIME_TYPE_SUBFRAME_ID}' subframe in ${APIC_FRAME_ID}" . " (Source: '${FILE}')!\n" } sub _error_message_no_cover_picture_in_apic_frame { print "${THIS_SCRIPT_NAME}:" . " Did not find a cover" . " ('${PICTURE_TYPE_SUBFRAME_ID}' does not contain '${PICTURE_TYPE_VALUE_SUBSTRING}')" . " in cover frame (${APIC_FRAME_ID})" . " (Source: '${FILE}')!\n"; } sub _error_message_cover_apic_frame_not_found { print "${THIS_SCRIPT_NAME}:" . " Did not find a cover frame (${APIC_FRAME_ID})" . " (Source: '${FILE}')!\n"; } sub _error_message_cover_image_file_already_exists { print "${THIS_SCRIPT_NAME}:" . " Cover image file '${cover_image_filename}' already exists" . " (Source: '${FILE}')!\n"; } sub _error_message_picture_data_undefined { print "${THIS_SCRIPT_NAME}:" . " Cover frame (${APIC_FRAME_ID}) does not contain image data" . " whithin subframe '${PICTURE_DATA_SUBFRAME_ID}'" . " (Source: '${FILE}')!\n"; } __END__ =head1 NAME extract_id3v2_cover.pl - extracts from an ID3v2 tag the (front) cover =head1 SYNOPSIS =head1 DESCRIPTION This script extracts from an ID3v2 tag of a MP3 file the (frong) cover and writes it into the same directory as the MP3 file as C, e.g. C odr C. If a file of the same name, e.g. C, already exists, it will not be overwritten. At least one C<'APIC'> frame must exist into the tag and contain the subframes C<'_Data'>, C<'MIME type'> and C<'Picture Type'>. The C<'Picture type'> subframe must contain C<'Cover'>. If multiple C<'APIC'> frames are existing with a C<'Picture type'> subframe containing C<'Cover'>, picture types containing the substring C<'front'> will be preferred. =head1 AUTHOR Elmar Baumann 2011/01/29 =cut