#!/usr/bin/perl -w use File::Basename; use Getopt::Std; use MPEG::ID3v2Tag; use IO::File; use strict; use 5.005; # Subroutine declarations: ------------------------------------------ <<<1 sub scandir($); sub scanfile($); sub read_header($); sub read_id3v2($); sub read_id3($); sub usage($); # Global variables: ------------------------------------------------- <<<1 my $DOWARN = 1; $SIG{'__WARN__'} = sub { warn $_[0] if $DOWARN }; my %opts; my $total_time = 0; my $total_size = 0; my @genre_list = ( # <<<2 "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "Alternative Rock", "Bass", "Soul", "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "HardRock", "Folk", "Folk/Rock", "National Folk", "Swing", "Fast-Fusion", "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo", "Acapella", "Euro-House", "Dance Hall", "Goa", "Drum & Bass", "ClubHouse", "Hardcore", "Terror", "Indie", "Brit Pop", "Neger Punk", "Polsk Punk", "Beat", "Christian Gangsta", "Heavy Metal", "Black Metal", "Crossover", "Contemporary C", "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "J Pop", "Synth Pop", ); my %mp3_1kb = ( # <<<2 0x10 => 32, 0x20 => 40, 0x30 => 48, 0x40 => 56, 0x50 => 64, 0x60 => 80, 0x70 => 96, 0x80 => 112, 0x90 => 128, 0xA0 => 160, 0xB0 => 192, 0xC0 => 224, 0xD0 => 256, 0xE0 => 320, ); my %mp3_1freq = ( # <<<2 0x00 => 44.1, 0x04 => 48, 0x08 => 32, ); my %mp3_2kb = ( # <<<2 0x10 => 8, 0x20 => 16, 0x30 => 24, 0x40 => 32, 0x50 => 40, 0x60 => 48, 0x70 => 56, 0x80 => 64, 0x90 => 80, 0xA0 => 96, 0xB0 => 112, 0xC0 => 128, 0xD0 => 144, 0xE0 => 160, ); my %mp3_2freq = ( # <<<2 0x00 => 22.05, 0x04 => 24, 0x08 => 16, ); my %mp3misc = ( # <<<2 0x00 => "Stereo", 0x40 => "Joint-Stereo", 0x80 => "Dual-Channel", 0xC0 => "Mono", ); my %id3v2frameids = ( # <<<2 AENC => "Audio Encryption", APIC => "Attached Picture", COMM => "Comments", COMR => "Commercial Frame", ENCR => "Encryption Method Registration", EQUA => "Equalization", ETCO => "Event Timing Codes", GEOB => "General Encapsulated Object", GRID => "Group Identification Registration", IPLS => "Involved People List", LINK => "Linked Information", MCDI => "Music CD Identifier", MLLT => "MPEG Location Lookup Table", OWNE => "Ownership Frame", PRIV => "Private Frame", PCNT => "Play Counter", POPM => "Popularimeter", POSS => "Position Synchronisation Frame", RBUF => "Recommended Buffer Size", RVAD => "Relative Volume Adjustment", RVRB => "Reverb", SYLT => "Synchronized Lyric/Text", SYTC => "Synchronized Tempo Codes", #TALB => "Album/Movie/Show title", TALB => "Album", TBPM => "BPM (beats per minute)", TCOM => "Composer", #TCON => "Content Type", TCON => "Genre", TCOP => "Copyright Message", TDAT => "Date", TDLY => "Playlist Delay", TENC => "Encoded By", TEXT => "Lyricist/Text Writer", TFLT => "File Type", TIME => "Time", TIT1 => "Content Group Description", #TIT2 => "Title/songname/content description", TIT2 => "Title", #TIT3 => "Subtitle/Description refinement", TIT3 => "Subtitle", TKEY => "Initial Key", TLAN => "Language(s)", TLEN => "Length", TMED => "Media Type", #TOAL => "Original album/movie/show title", TOAL => "Original Album", TOFN => "Original Filename", #TOLY => "Original lyricist(s)/text writer(s)", TOLY => "Original Lyricist(s)", #TOPE => "Original artist(s)/performer(s)", TOPE => "Original Artist(s)", TORY => "Original Release Year", TOWN => "File Owner/Licensee", #TPE1 => "Lead Performer(s)/Soloist(s)", TPE1 => "Performer", TPE2 => "Band/Orchestra/Accompaniment", TPE3 => "Conductor/Performer Refinement", TPE4 => "Interpreted, Remixed, or Otherwise Modified by", TPOS => "Part of a set", TPUB => "Publisher", #TRCK => "Track Number/Position in set", TRCK => "Track", TRDA => "Recording Dates", TRSN => "Internet Radio Station Name", TRSO => "Internet Radio Station Owner", TSIZ => "Size", TSRC => "ISRC (International Standard Recording Code)", TSSE => "Software/Hardware and Settings Used for Encoding", TYER => "Year", TXXX => "User Defined Text Information Frame", UFID => "Unique File Identifier", USER => "Terms of Use", USLT => "Unsychronized Lyric/Text Transcription", WCOM => "Commercial Information", WCOP => "Copyright/Legal Information", WOAF => "Official Audio File Webpage", WOAR => "Official Artist/Performer Webpage", WOAS => "Official Audio Source Webpage", WORS => "Official Internet Radio Station Homepage", WPAY => "Payment", WPUB => "Publishers Official Webpage", WXXX => "User defined URL link frame", ); # >>>2 # Main code: -------------------------------------------------------- <<<1 getopts('hrt', \%opts) or usage(1); usage(0) if $opts{'h'}; @ARGV = ('.') unless @ARGV; foreach my $file (@ARGV) { if (-d $file) { scandir($file); } else { scanfile($file); } } if ($opts{'t'}) { if ($total_size >= 1048576) { $total_size = sprintf("%.1fM", $total_size / 1048576); } elsif ($total_size >= 1024) { $total_size = sprintf("%.1fK", $total_size / 1024); } printf("\nTotal time: %i:%02i:%02i\nTotal size: %s\n", int $total_time / 3600, int ($total_time % 3600) / 60, $total_time % 60, $total_size ); } # Subroutines: ------------------------------------------------------ <<<1 sub scandir($) # <<<2 { my $dir = shift; my ($file); # So recursive calls of this function don't close the dirhandle: local *DIR; opendir(DIR,$dir) or warn "Can't read directory \"$dir\": $!\n"; while ( defined($file = readdir(DIR)) ) { next if $file =~ m/^\./; if (-d "$dir/$file" && $opts{'r'}) { scandir("$dir/$file"); } elsif ($file =~ m/\.mp[23]$/i) { scanfile("$dir/$file"); } } closedir(DIR); } sub scanfile($) # <<<2 { my $file = shift; my ($mp3file, $text); if (! ($mp3file = IO::File->new($file))) { warn ("Can't open \"$file\": $!\n"); return 0; } binmode($mp3file); # No-op in Unix, necessary for Winblowz. if ($text = read_header($mp3file)) { $text .= read_id3($mp3file); print basename($file) . ":\n$text\n"; } close($mp3file); } sub read_header($) # <<<2 { my $mp3file = shift; my $text = ""; my ($size, $time, @data, $data, $id3v2); # Get the filesize, which we use to compute the time of the mp[23]: $size = (stat($mp3file))[7]; # Four bytes in the mp3 header info: read($mp3file, $data, 4); # Skip ID3v2 tags, if we see them: if (substr($data,0,3) eq 'ID3') { seek($mp3file, 6, 0); read($mp3file, $data, 4); @data = unpack('CCCC',$data); # The ID3v2 tag size is encoded with four bytes where the most # significant bit (bit 7) is set to zero in every byte, making # a total of 28 bits. The zeroed bits are ignored, so a 257 # bytes long tag is represented as $00 00 02 01 $data = $data[0]; $data = ($data << 7) | $data[1]; $data = ($data << 7) | $data[2]; $data = ($data << 7) | $data[3]; # Store the size of the ID3v2 for use later: $id3v2 = $data; # The ID3v2 doesn't count towards the time of the mp3: $size = $size - ($id3v2 + 10); # The ID3v2 tag size is the size of the complete tag excluding # the header but not excluding the extended header (total tag # size - 10). seek($mp3file, $id3v2 + 10, 0); read($mp3file, $data, 4); } # Unpack the mp3 header so it can be used: @data = unpack('nCC',$data); # Skip non-mp[23] files: return 0 unless (($data[0] & 0xFFF0) == 0xFFF0); # Deprecated: # print " This file has an ID3v2 tag of $id3v2 bytes, skipped.\n" if $id3v2; # Determine whether it's mp3, or mp2: if (($data[0] & 0x02) == 0x02) { $text .= "MP3, "; } elsif (($data[0] & 0x04) == 0x04) { $text .= "MP2, "; } # Determine the rest: if (($data[0] & 0x08) == 0x08) { # MPEG 1.0 $text .= sprintf("%3i kBits, %2.2f kHz, %s", $mp3_1kb{$data[1] & 0xF0}, $mp3_1freq{$data[1] & 0x0C}, $mp3misc{$data[2] & 0xC0} ); $time = int $size / ($mp3_1kb{$data[1] & 0xF0} * 1000 / 8); } elsif (($data[0] & 0x08) == 0x00) { # MPEG 2.0 $text .= sprintf("%3i kBits, %2.2f kHz, %s", $mp3_2kb{$data[1] & 0xF0}, $mp3_2freq{$data[1] & 0x0C}, $mp3misc{$data[2] & 0xC0} ); $time = int $size / ($mp3_2kb{$data[1] & 0xF0} * 1000 / 8); } # Total some stuff: $total_time += $time; $total_size += $size; $text .= sprintf(" - Time: %i:%02i\n", int $time / 60, $time % 60); # If there's an ID3v2, print it: if ($id3v2) { $text .= "** This file has an ID3v2 tag of $id3v2 bytes:\n"; $text .= read_id3v2($mp3file); } return($text); } sub read_id3v2($) # <<<2 { my $mp3file = shift; my $text = ""; my ($id3v2, $data); seek($mp3file, 0, 0); $DOWARN=0; read($mp3file, $data, 250000); $id3v2 = MPEG::ID3v2Tag->parse($data); $DOWARN=1; unless ($id3v2) { warn "Something went wrong while reading the ID3v2...\n"; return ''; } foreach my $frame ($id3v2->frames()) { if ($frame->fully_parsed() && $frame->frameid =~ /^T.../) { $text .= sprintf("%-10s %s\n", $id3v2frameids{$frame->frameid()}. ':', $frame->text()) if $frame->text; } if ($frame->fully_parsed() && $frame->frameid =~ /^W.../) { $text .= sprintf("%10s: %s\n", $id3v2frameids{$frame->frameid()}, $frame->url()) if $frame->url; } } return($text); } sub read_id3($) # <<<2 { my $mp3file = shift; my $text = ""; my ($taginfo); seek($mp3file, -128, 2); read($mp3file, $taginfo, 3); if ($taginfo eq 'TAG') { $text .= "** This file has an ID3 tag:\n"; read($mp3file, $taginfo, 30); $taginfo =~ s/[\0 ]+$//; $taginfo =~ s/[\r\n]//g; $text .= "Title: $taginfo\n" if $taginfo; read($mp3file, $taginfo, 30); $taginfo =~ s/[\0 ]+$//; $taginfo =~ s/[\r\n]//g; $text .= "Artist: $taginfo\n" if $taginfo; read($mp3file, $taginfo, 30); $taginfo =~ s/[\0 ]+$//; $taginfo =~ s/[\r\n]//g; $text .= "Album: $taginfo\n" if $taginfo; read($mp3file, $taginfo, 4); $taginfo =~ s/[\0 ]+$//; $taginfo =~ s/[\r\n]//g; $text .= "Year: $taginfo\n" if $taginfo; read($mp3file, $taginfo, 30); $taginfo =~ s/(?:[\0 ]+)?(\0.)?$//; $taginfo =~ s/[\r\n]//g; $text .= "Comment: $taginfo\n" if $taginfo; $taginfo = unpack("C",$1) if $1; $text .= "Track: $taginfo\n" if ($1 && $taginfo); read($mp3file, $taginfo, 1); $taginfo = unpack("C",$taginfo); $text .= "Genre: $genre_list[$taginfo]\n" if $genre_list[$taginfo]; } return($text); } sub usage($) # <<<2 { my $exitval = shift; my ($where, $basename); require File::Basename; $basename = File::Basename::basename($0); if ($exitval) { $where=\*STDERR; } else { $where=\*STDOUT; } print $where <<"EOF"; Usage: $basename [options] [files] [directories] If no files/directories are provided, the current directory is assumed. Subdirectories are not scanned. Options: -r Recursively scan directories. -t Show totals. -h Show this help. EOF exit $exitval; } # >>>2 # vim: set ts=2 sw=2 ai nu tw=75 fo=croq2: # vim600:fdm=marker:fmr=<<<,>>>:fdc=3:fdl=1:cms=\ #\ %s: