#!/usr/bin/perl # # Rockbox song database docs: # http://www.rockbox.org/twiki/bin/view/Main/TagCache # use mp3info; use vorbiscomm; # configuration settings my $db = "tagcache"; my $dir; my $strip; my $add; my $verbose; my $help; my $dirisalbum; my $littleendian = 0; my $dbver = 0x54434806; # file data my %entries; while($ARGV[0]) { if($ARGV[0] eq "--path") { $dir = $ARGV[1]; shift @ARGV; shift @ARGV; } elsif($ARGV[0] eq "--db") { $db = $ARGV[1]; shift @ARGV; shift @ARGV; } elsif($ARGV[0] eq "--strip") { $strip = $ARGV[1]; shift @ARGV; shift @ARGV; } elsif($ARGV[0] eq "--add") { $add = $ARGV[1]; shift @ARGV; shift @ARGV; } elsif($ARGV[0] eq "--dirisalbum") { $dirisalbum = 1; shift @ARGV; } elsif($ARGV[0] eq "--littleendian") { $littleendian = 1; shift @ARGV; } elsif($ARGV[0] eq "--verbose") { $verbose = 1; shift @ARGV; } elsif($ARGV[0] eq "--help" or ($ARGV[0] eq "-h")) { $help = 1; shift @ARGV; } else { shift @ARGV; } } if(! -d $dir or $help) { print "'$dir' is not a directory\n" if ($dir ne "" and ! -d $dir); print < [--db ] [--strip ] [--add ] [--dirisalbum] [--littleendian] [--verbose] [--help] Options: --path Where your music collection is found --db Prefix for output files. Defaults to tagcache. --strip Removes this string from the left of all file names --add Adds this string to the left of all file names --dirisalbum Use dir name as album name if the album name is missing in the tags --littleendian Write out data as little endian (for x86 simulators and ARM- based targets such as iPods and iriver H10) --verbose Shows more details while working --help This text MOO ; exit; } sub get_oggtag { my $fn = shift; my %hash; my $ogg = vorbiscomm->new($fn); my $h= $ogg->load; # Convert this format into the same format used by the id3 parser hash foreach my $k ($ogg->comment_tags()) { foreach my $cmmt ($ogg->comment($k)) { my $n; if($k =~ /^artist$/i) { $n = 'ARTIST'; } elsif($k =~ /^album$/i) { $n = 'ALBUM'; } elsif($k =~ /^title$/i) { $n = 'TITLE'; } $hash{$n}=$cmmt if($n); } } return \%hash; } sub get_ogginfo { my $fn = shift; my %hash; my $ogg = vorbiscomm->new($fn); my $h= $ogg->load; return $ogg->{'INFO'}; } # return ALL directory entries in the given dir sub getdir { my ($dir) = @_; $dir =~ s|/$|| if ($dir ne "/"); if (opendir(DIR, $dir)) { my @all = readdir(DIR); closedir DIR; return @all; } else { warn "can't opendir $dir: $!\n"; } } sub extractmp3 { my ($dir, @files) = @_; my @mp3; for(@files) { if( (/\.mp[23]$/i || /\.ogg$/i) && -f "$dir/$_" ) { push @mp3, $_; } } return @mp3; } sub extractdirs { my ($dir, @files) = @_; $dir =~ s|/$||; my @dirs; for(@files) { if( -d "$dir/$_" && ($_ !~ /^\.(|\.)$/)) { push @dirs, $_; } } return @dirs; } sub singlefile { my ($file) = @_; my $hash; my $info; if($file =~ /\.ogg$/i) { $hash = get_oggtag($file); $info = get_ogginfo($file); } else { $hash = get_mp3tag($file); $info = get_mp3info($file); if (defined $$info{'BITRATE'}) { $$hash{'BITRATE'} = $$info{'BITRATE'}; } if (defined $$info{'SECS'}) { $$hash{'SECS'} = $$info{'SECS'}; } } return $hash; } sub dodir { my ($dir)=@_; my %lcartists; my %lcalbums; print "$dir\n"; # getdir() returns all entries in the given dir my @a = getdir($dir); # extractmp3 filters out only the mp3 files from all given entries my @m = extractmp3($dir, @a); my $f; for $f (sort @m) { my $id3 = singlefile("$dir/$f"); if (not defined $$id3{'ARTIST'} or $$id3{'ARTIST'} eq "") { $$id3{'ARTIST'} = ""; } # Only use one case-variation of each artist if (exists($lcartists{lc($$id3{'ARTIST'})})) { $$id3{'ARTIST'} = $lcartists{lc($$id3{'ARTIST'})}; } else { $lcartists{lc($$id3{'ARTIST'})} = $$id3{'ARTIST'}; } #printf "Artist: %s\n", $$id3{'ARTIST'}; if (not defined $$id3{'ALBUM'} or $$id3{'ALBUM'} eq "") { $$id3{'ALBUM'} = ""; if ($dirisalbum) { $$id3{'ALBUM'} = $dir; } } # Only use one case-variation of each album if (exists($lcalbums{lc($$id3{'ALBUM'})})) { $$id3{'ALBUM'} = $lcalbums{lc($$id3{'ALBUM'})}; } else { $lcalbums{lc($$id3{'ALBUM'})} = $$id3{'ALBUM'}; } #printf "Album: %s\n", $$id3{'ALBUM'}; if (not defined $$id3{'GENRE'} or $$id3{'GENRE'} eq "") { $$id3{'GENRE'} = ""; } #printf "Genre: %s\n", $$id3{'GENRE'}; if (not defined $$id3{'TITLE'} or $$id3{'TITLE'} eq "") { # fall back on basename of the file if no title tag. ($$id3{'TITLE'} = $f) =~ s/\.\w+$//; } #printf "Title: %s\n", $$id3{'TITLE'}; my $path = "$dir/$f"; if ($strip ne "" and $path =~ /^$strip(.*)/) { $path = $1; } if ($add ne "") { $path = $add . $path; } #printf "Path: %s\n", $path; if (not defined $$id3{'COMPOSER'} or $$id3{'COMPOSER'} eq "") { $$id3{'COMPOSER'} = ""; } #printf "Composer: %s\n", $$id3{'COMPOSER'}; if (not defined $$id3{'YEAR'} or $$id3{'YEAR'} eq "") { $$id3{'YEAR'} = "-1"; } #printf "Year: %s\n", $$id3{'YEAR'}; if (not defined $$id3{'TRACKNUM'} or $$id3{'TRACKNUM'} eq "") { $$id3{'TRACKNUM'} = "-1"; } #printf "Track num: %s\n", $$id3{'TRACKNUM'}; if (not defined $$id3{'BITRATE'} or $$id3{'BITRATE'} eq "") { $$id3{'BITRATE'} = "-1"; } #printf "Bitrate: %s\n", $$id3{'BITRATE'}; if (not defined $$id3{'SECS'} or $$id3{'SECS'} eq "") { $$id3{'SECS'} = "-1"; } #printf "Length: %s\n", $$id3{'SECS'}; $$id3{'PATH'} = $path; $entries{$path} = $id3; } # extractdirs filters out only subdirectories from all given entries my @d = extractdirs($dir, @a); my $d; for $d (sort @d) { $dir =~ s|/$||; dodir("$dir/$d"); } } dodir($dir); print "\n"; sub dumpshort { my ($num)=@_; # print "int: $num\n"; if ($littleendian) { print DB pack "v", $num; } else { print DB pack "n", $num; } } sub dumpint { my ($num)=@_; # print "int: $num\n"; if ($littleendian) { print DB pack "V", $num; } else { print DB pack "N", $num; } } sub dump_tag_string { my ($s, $index) = @_; my $strlen = length($s)+1; my $padding = $strlen%4; if ($padding > 0) { $padding = 4 - $padding; $strlen += $padding; } dumpshort($strlen); dumpshort($index); print DB $s."\0"; for (my $i = 0; $i < $padding; $i++) { print DB "X"; } } sub dump_tag_header { my ($entry_count) = @_; my $size = tell(DB) - 12; seek(DB, 0, 0); dumpint($dbver); dumpint($size); dumpint($entry_count); } sub openfile { my ($f) = @_; open(DB, "> $f") || die "couldn't open $f"; binmode(DB); } sub create_tagcache_index_file { my ($index, $key, $unique) = @_; my $num = 0; my $prev = ""; my $offset = 12; openfile $db ."_".$index.".tcd"; dump_tag_header(0); for(sort {uc($entries{$a}->{$key}) cmp uc($entries{$b}->{$key})} keys %entries) { if (!$unique || !($entries{$_}->{$key} eq $prev)) { my $index; $num++; $prev = $entries{$_}->{$key}; $offset = tell(DB); printf(" %s\n", $prev) if ($verbose); if ($unique) { $index = 0xFFFF; } else { $index = $entries{$_}->{'INDEX'}; } dump_tag_string($prev, $index); } $entries{$_}->{$key."_OFFSET"} = $offset; } dump_tag_header($num); close(DB); } if (!scalar keys %entries) { print "No songs found. Did you specify the right --path ?\n"; print "Use the --help parameter to see all options.\n"; exit; } my $i = 0; for (sort keys %entries) { $entries{$_}->{'INDEX'} = $i; $i++; } if ($db) { # tagcache index files create_tagcache_index_file(0, 'ARTIST', 1); create_tagcache_index_file(1, 'ALBUM', 1); create_tagcache_index_file(2, 'GENRE', 1); create_tagcache_index_file(3, 'TITLE', 0); create_tagcache_index_file(4, 'PATH', 0); create_tagcache_index_file(5, 'COMPOSER', 1); # Master index file openfile $db ."_idx.tcd"; dump_tag_header(0); # current serial dumpint(0); for (sort keys %entries) { dumpint($entries{$_}->{'ARTIST_OFFSET'}); dumpint($entries{$_}->{'ALBUM_OFFSET'}); dumpint($entries{$_}->{'GENRE_OFFSET'}); dumpint($entries{$_}->{'TITLE_OFFSET'}); dumpint($entries{$_}->{'PATH_OFFSET'}); dumpint($entries{$_}->{'COMPOSER_OFFSET'}); dumpint($entries{$_}->{'YEAR'}); dumpint($entries{$_}->{'TRACKNUM'}); dumpint($entries{$_}->{'BITRATE'}); dumpint($entries{$_}->{'SECS'}); # play count dumpint(0); # play time dumpint(0); # last played dumpint(0); # status flag dumpint(0); } dump_tag_header(scalar keys %entries); close(DB); }