Code police, no functional changes (yet)
[maemo-rb.git] / tools / songdb.pl
blobfba8c2a3553255d4efb932441d19c83fc5be01db
1 #!/usr/bin/perl
3 # Rockbox song database docs:
4 # http://www.rockbox.org/twiki/bin/view/Main/DataBase
7 use mp3info;
8 use vorbiscomm;
10 # configuration settings
11 my $db = "database";
12 my $dir;
13 my $strip;
14 my $add;
15 my $verbose;
16 my $help;
17 my $dirisalbum;
18 my $littleendian = 0;
19 my $dbver = 0x54434806;
21 # file data
22 my %entries;
24 while($ARGV[0]) {
25 if($ARGV[0] eq "--path") {
26 $dir = $ARGV[1];
27 shift @ARGV;
28 shift @ARGV;
30 elsif($ARGV[0] eq "--db") {
31 $db = $ARGV[1];
32 shift @ARGV;
33 shift @ARGV;
35 elsif($ARGV[0] eq "--strip") {
36 $strip = $ARGV[1];
37 shift @ARGV;
38 shift @ARGV;
40 elsif($ARGV[0] eq "--add") {
41 $add = $ARGV[1];
42 shift @ARGV;
43 shift @ARGV;
45 elsif($ARGV[0] eq "--dirisalbum") {
46 $dirisalbum = 1;
47 shift @ARGV;
49 elsif($ARGV[0] eq "--littleendian") {
50 $littleendian = 1;
51 shift @ARGV;
53 elsif($ARGV[0] eq "--verbose") {
54 $verbose = 1;
55 shift @ARGV;
57 elsif($ARGV[0] eq "--help" or ($ARGV[0] eq "-h")) {
58 $help = 1;
59 shift @ARGV;
61 else {
62 shift @ARGV;
66 if(! -d $dir or $help) {
67 print "'$dir' is not a directory\n" if ($dir ne "" and ! -d $dir);
68 print <<MOO
70 songdb --path <dir> [--db <file>] [--strip <path>] [--add <path>] [--dirisalbum] [--littleendian] [--verbose] [--help]
72 Options:
74 --path <dir> Where your music collection is found
75 --db <file> Prefix for output files. Defaults to database.
76 --strip <path> Removes this string from the left of all file names
77 --add <path> Adds this string to the left of all file names
78 --dirisalbum Use dir name as album name if the album name is missing in the
79 tags
80 --littleendian Write out data as little endian (for x86 simulators and ARM-
81 based targets such as iPods and iriver H10)
82 --verbose Shows more details while working
83 --help This text
84 MOO
86 exit;
89 sub get_oggtag {
90 my $fn = shift;
91 my %hash;
93 my $ogg = vorbiscomm->new($fn);
95 my $h= $ogg->load;
97 # Convert this format into the same format used by the id3 parser hash
99 foreach my $k ($ogg->comment_tags())
101 foreach my $cmmt ($ogg->comment($k))
103 my $n;
104 if($k =~ /^artist$/i) {
105 $n = 'ARTIST';
107 elsif($k =~ /^album$/i) {
108 $n = 'ALBUM';
110 elsif($k =~ /^title$/i) {
111 $n = 'TITLE';
113 $hash{$n}=$cmmt if($n);
117 return \%hash;
120 sub get_ogginfo {
121 my $fn = shift;
122 my %hash;
124 my $ogg = vorbiscomm->new($fn);
126 my $h= $ogg->load;
128 return $ogg->{'INFO'};
131 # return ALL directory entries in the given dir
132 sub getdir {
133 my ($dir) = @_;
135 $dir =~ s|/$|| if ($dir ne "/");
137 if (opendir(DIR, $dir)) {
138 my @all = readdir(DIR);
139 closedir DIR;
140 return @all;
142 else {
143 warn "can't opendir $dir: $!\n";
147 sub extractmp3 {
148 my ($dir, @files) = @_;
149 my @mp3;
150 for(@files) {
151 if( (/\.mp[23]$/i || /\.ogg$/i) && -f "$dir/$_" ) {
152 push @mp3, $_;
155 return @mp3;
158 sub extractdirs {
159 my ($dir, @files) = @_;
160 $dir =~ s|/$||;
161 my @dirs;
162 for(@files) {
163 if( -d "$dir/$_" && ($_ !~ /^\.(|\.)$/)) {
164 push @dirs, $_;
167 return @dirs;
170 sub singlefile {
171 my ($file) = @_;
172 my $hash;
173 my $info;
175 if($file =~ /\.ogg$/i) {
176 $hash = get_oggtag($file);
177 $info = get_ogginfo($file);
179 else {
180 $hash = get_mp3tag($file);
181 $info = get_mp3info($file);
182 if (defined $$info{'BITRATE'}) {
183 $$hash{'BITRATE'} = $$info{'BITRATE'};
186 if (defined $$info{'SECS'}) {
187 $$hash{'SECS'} = $$info{'SECS'};
191 return $hash;
194 sub dodir {
195 my ($dir)=@_;
197 my %lcartists;
198 my %lcalbums;
200 print "$dir\n";
202 # getdir() returns all entries in the given dir
203 my @a = getdir($dir);
205 # extractmp3 filters out only the mp3 files from all given entries
206 my @m = extractmp3($dir, @a);
208 my $f;
210 for $f (sort @m) {
212 my $id3 = singlefile("$dir/$f");
214 if (not defined $$id3{'ARTIST'} or $$id3{'ARTIST'} eq "") {
215 $$id3{'ARTIST'} = "<Untagged>";
218 # Only use one case-variation of each artist
219 if (exists($lcartists{lc($$id3{'ARTIST'})})) {
220 $$id3{'ARTIST'} = $lcartists{lc($$id3{'ARTIST'})};
222 else {
223 $lcartists{lc($$id3{'ARTIST'})} = $$id3{'ARTIST'};
225 #printf "Artist: %s\n", $$id3{'ARTIST'};
227 if (not defined $$id3{'ALBUM'} or $$id3{'ALBUM'} eq "") {
228 $$id3{'ALBUM'} = "<Untagged>";
229 if ($dirisalbum) {
230 $$id3{'ALBUM'} = $dir;
234 # Only use one case-variation of each album
235 if (exists($lcalbums{lc($$id3{'ALBUM'})})) {
236 $$id3{'ALBUM'} = $lcalbums{lc($$id3{'ALBUM'})};
238 else {
239 $lcalbums{lc($$id3{'ALBUM'})} = $$id3{'ALBUM'};
241 #printf "Album: %s\n", $$id3{'ALBUM'};
243 if (not defined $$id3{'GENRE'} or $$id3{'GENRE'} eq "") {
244 $$id3{'GENRE'} = "<Untagged>";
246 #printf "Genre: %s\n", $$id3{'GENRE'};
248 if (not defined $$id3{'TITLE'} or $$id3{'TITLE'} eq "") {
249 # fall back on basename of the file if no title tag.
250 ($$id3{'TITLE'} = $f) =~ s/\.\w+$//;
252 #printf "Title: %s\n", $$id3{'TITLE'};
254 my $path = "$dir/$f";
255 if ($strip ne "" and $path =~ /^$strip(.*)/) {
256 $path = $1;
259 if ($add ne "") {
260 $path = $add . $path;
262 #printf "Path: %s\n", $path;
264 if (not defined $$id3{'COMPOSER'} or $$id3{'COMPOSER'} eq "") {
265 $$id3{'COMPOSER'} = "<Untagged>";
267 #printf "Composer: %s\n", $$id3{'COMPOSER'};
269 if (not defined $$id3{'YEAR'} or $$id3{'YEAR'} eq "") {
270 $$id3{'YEAR'} = "-1";
272 #printf "Year: %s\n", $$id3{'YEAR'};
274 if (not defined $$id3{'TRACKNUM'} or $$id3{'TRACKNUM'} eq "") {
275 $$id3{'TRACKNUM'} = "-1";
277 #printf "Track num: %s\n", $$id3{'TRACKNUM'};
279 if (not defined $$id3{'BITRATE'} or $$id3{'BITRATE'} eq "") {
280 $$id3{'BITRATE'} = "-1";
282 #printf "Bitrate: %s\n", $$id3{'BITRATE'};
284 if (not defined $$id3{'SECS'} or $$id3{'SECS'} eq "") {
285 $$id3{'SECS'} = "-1";
287 #printf "Length: %s\n", $$id3{'SECS'};
289 $$id3{'PATH'} = $path;
290 $entries{$path} = $id3;
293 # extractdirs filters out only subdirectories from all given entries
294 my @d = extractdirs($dir, @a);
295 my $d;
297 for $d (sort @d) {
298 $dir =~ s|/$||;
299 dodir("$dir/$d");
303 dodir($dir);
304 print "\n";
306 sub dumpshort {
307 my ($num)=@_;
309 # print "int: $num\n";
311 if ($littleendian) {
312 print DB pack "v", $num;
314 else {
315 print DB pack "n", $num;
319 sub dumpint {
320 my ($num)=@_;
322 # print "int: $num\n";
324 if ($littleendian) {
325 print DB pack "V", $num;
327 else {
328 print DB pack "N", $num;
332 sub dump_tag_string {
333 my ($s, $index) = @_;
335 my $strlen = length($s)+1;
336 my $padding = $strlen%4;
337 if ($padding > 0) {
338 $padding = 4 - $padding;
339 $strlen += $padding;
342 dumpshort($strlen);
343 dumpshort($index);
344 print DB $s."\0";
346 for (my $i = 0; $i < $padding; $i++) {
347 print DB "X";
351 sub dump_tag_header {
352 my ($entry_count) = @_;
354 my $size = tell(DB) - 12;
355 seek(DB, 0, 0);
357 dumpint($dbver);
358 dumpint($size);
359 dumpint($entry_count);
362 sub openfile {
363 my ($f) = @_;
364 open(DB, "> $f") || die "couldn't open $f";
365 binmode(DB);
368 sub create_tagcache_index_file {
369 my ($index, $key, $unique) = @_;
371 my $num = 0;
372 my $prev = "";
373 my $offset = 12;
375 openfile $db ."_".$index.".tcd";
376 dump_tag_header(0);
378 for(sort {uc($entries{$a}->{$key}) cmp uc($entries{$b}->{$key})} keys %entries) {
379 if (!$unique || !($entries{$_}->{$key} eq $prev)) {
380 my $index;
382 $num++;
383 $prev = $entries{$_}->{$key};
384 $offset = tell(DB);
385 printf(" %s\n", $prev) if ($verbose);
387 if ($unique) {
388 $index = 0xFFFF;
390 else {
391 $index = $entries{$_}->{'INDEX'};
393 dump_tag_string($prev, $index);
395 $entries{$_}->{$key."_OFFSET"} = $offset;
398 dump_tag_header($num);
399 close(DB);
402 if (!scalar keys %entries) {
403 print "No songs found. Did you specify the right --path ?\n";
404 print "Use the --help parameter to see all options.\n";
405 exit;
408 my $i = 0;
409 for (sort keys %entries) {
410 $entries{$_}->{'INDEX'} = $i;
411 $i++;
414 if ($db) {
415 # tagcache index files
416 create_tagcache_index_file(0, 'ARTIST', 1);
417 create_tagcache_index_file(1, 'ALBUM', 1);
418 create_tagcache_index_file(2, 'GENRE', 1);
419 create_tagcache_index_file(3, 'TITLE', 0);
420 create_tagcache_index_file(4, 'PATH', 0);
421 create_tagcache_index_file(5, 'COMPOSER', 1);
423 # Master index file
424 openfile $db ."_idx.tcd";
425 dump_tag_header(0);
427 # current serial
428 dumpint(0);
430 for (sort keys %entries) {
431 dumpint($entries{$_}->{'ARTIST_OFFSET'});
432 dumpint($entries{$_}->{'ALBUM_OFFSET'});
433 dumpint($entries{$_}->{'GENRE_OFFSET'});
434 dumpint($entries{$_}->{'TITLE_OFFSET'});
435 dumpint($entries{$_}->{'PATH_OFFSET'});
436 dumpint($entries{$_}->{'COMPOSER_OFFSET'});
437 dumpint($entries{$_}->{'YEAR'});
438 dumpint($entries{$_}->{'TRACKNUM'});
439 dumpint($entries{$_}->{'BITRATE'});
440 dumpint($entries{$_}->{'SECS'});
441 # play count
442 dumpint(0);
443 # play time
444 dumpint(0);
445 # last played
446 dumpint(0);
447 # status flag
448 dumpint(0);
451 dump_tag_header(scalar keys %entries);
452 close(DB);