rockbox/tools/songdb.pl

454 lines
10 KiB
Perl
Executable File

#!/usr/bin/perl
#
# Rockbox song database docs:
# http://www.rockbox.org/wiki/DataBase
#
use mp3info;
use vorbiscomm;
# configuration settings
my $db = "database";
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 <<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 database.
--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 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'} = "<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) {
# 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);
}