448 lines
9.9 KiB
Perl
448 lines
9.9 KiB
Perl
|
#!/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 = 0x54434804;
|
||
|
|
||
|
# 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 <<MOO
|
||
|
|
||
|
songdb --path <dir> [--db <file>] [--strip <path>] [--add <path>] [--dirisalbum] [--littleendian] [--verbose] [--help]
|
||
|
|
||
|
Options:
|
||
|
|
||
|
--path <dir> Where your music collection is found
|
||
|
--db <file> Prefix for output files. Defaults to tagcache.
|
||
|
--strip <path> Removes this string from the left of all file names
|
||
|
--add <path> 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 simulator)
|
||
|
--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'} = "<Untagged>";
|
||
|
}
|
||
|
|
||
|
# 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'} = "<Untagged>";
|
||
|
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'} = "<Untagged>";
|
||
|
}
|
||
|
#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'} = "<Untagged>";
|
||
|
}
|
||
|
#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) {
|
||
|
# Artists
|
||
|
create_tagcache_index_file(0, 'ARTIST', 1);
|
||
|
# Albums
|
||
|
create_tagcache_index_file(1, 'ALBUM', 1);
|
||
|
# Genres
|
||
|
create_tagcache_index_file(2, 'GENRE', 1);
|
||
|
# Titles
|
||
|
create_tagcache_index_file(3, 'TITLE', 0);
|
||
|
# Filenames
|
||
|
create_tagcache_index_file(4, 'PATH', 0);
|
||
|
# Composers
|
||
|
create_tagcache_index_file(5, 'COMPOSER', 1);
|
||
|
|
||
|
# Master index file
|
||
|
openfile $db ."_idx.tcd";
|
||
|
dump_tag_header(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'});
|
||
|
dumpint(0);
|
||
|
}
|
||
|
|
||
|
dump_tag_header(scalar keys %entries);
|
||
|
close(DB);
|
||
|
}
|