summaryrefslogtreecommitdiff
path: root/tools/songdb.pl
diff options
context:
space:
mode:
Diffstat (limited to 'tools/songdb.pl')
-rwxr-xr-xtools/songdb.pl448
1 files changed, 448 insertions, 0 deletions
diff --git a/tools/songdb.pl b/tools/songdb.pl
new file mode 100755
index 0000000000..cba30492da
--- /dev/null
+++ b/tools/songdb.pl
@@ -0,0 +1,448 @@
1#!/usr/bin/perl
2#
3# Rockbox song database docs:
4# http://www.rockbox.org/twiki/bin/view/Main/TagCache
5#
6
7use mp3info;
8use vorbiscomm;
9
10# configuration settings
11my $db = "tagcache";
12my $dir;
13my $strip;
14my $add;
15my $verbose;
16my $help;
17my $dirisalbum;
18my $littleendian = 0;
19my $dbver = 0x54434804;
20
21# file data
22my %entries;
23
24while($ARGV[0]) {
25 if($ARGV[0] eq "--path") {
26 $dir = $ARGV[1];
27 shift @ARGV;
28 shift @ARGV;
29 }
30 elsif($ARGV[0] eq "--db") {
31 $db = $ARGV[1];
32 shift @ARGV;
33 shift @ARGV;
34 }
35 elsif($ARGV[0] eq "--strip") {
36 $strip = $ARGV[1];
37 shift @ARGV;
38 shift @ARGV;
39 }
40 elsif($ARGV[0] eq "--add") {
41 $add = $ARGV[1];
42 shift @ARGV;
43 shift @ARGV;
44 }
45 elsif($ARGV[0] eq "--dirisalbum") {
46 $dirisalbum = 1;
47 shift @ARGV;
48 }
49 elsif($ARGV[0] eq "--littleendian") {
50 $littleendian = 1;
51 shift @ARGV;
52 }
53 elsif($ARGV[0] eq "--verbose") {
54 $verbose = 1;
55 shift @ARGV;
56 }
57 elsif($ARGV[0] eq "--help" or ($ARGV[0] eq "-h")) {
58 $help = 1;
59 shift @ARGV;
60 }
61 else {
62 shift @ARGV;
63 }
64}
65
66if(! -d $dir or $help) {
67 print "'$dir' is not a directory\n" if ($dir ne "" and ! -d $dir);
68 print <<MOO
69
70songdb --path <dir> [--db <file>] [--strip <path>] [--add <path>] [--dirisalbum] [--littleendian] [--verbose] [--help]
71
72Options:
73
74 --path <dir> Where your music collection is found
75 --db <file> Prefix for output files. Defaults to tagcache.
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 simulator)
81 --verbose Shows more details while working
82 --help This text
83MOO
84;
85 exit;
86}
87
88sub get_oggtag {
89 my $fn = shift;
90 my %hash;
91
92 my $ogg = vorbiscomm->new($fn);
93
94 my $h= $ogg->load;
95
96 # Convert this format into the same format used by the id3 parser hash
97
98 foreach my $k ($ogg->comment_tags())
99 {
100 foreach my $cmmt ($ogg->comment($k))
101 {
102 my $n;
103 if($k =~ /^artist$/i) {
104 $n = 'ARTIST';
105 }
106 elsif($k =~ /^album$/i) {
107 $n = 'ALBUM';
108 }
109 elsif($k =~ /^title$/i) {
110 $n = 'TITLE';
111 }
112 $hash{$n}=$cmmt if($n);
113 }
114 }
115
116 return \%hash;
117}
118
119sub get_ogginfo {
120 my $fn = shift;
121 my %hash;
122
123 my $ogg = vorbiscomm->new($fn);
124
125 my $h= $ogg->load;
126
127 return $ogg->{'INFO'};
128}
129
130# return ALL directory entries in the given dir
131sub getdir {
132 my ($dir) = @_;
133
134 $dir =~ s|/$|| if ($dir ne "/");
135
136 if (opendir(DIR, $dir)) {
137 my @all = readdir(DIR);
138 closedir DIR;
139 return @all;
140 }
141 else {
142 warn "can't opendir $dir: $!\n";
143 }
144}
145
146sub extractmp3 {
147 my ($dir, @files) = @_;
148 my @mp3;
149 for(@files) {
150 if( (/\.mp[23]$/i || /\.ogg$/i) && -f "$dir/$_" ) {
151 push @mp3, $_;
152 }
153 }
154 return @mp3;
155}
156
157sub extractdirs {
158 my ($dir, @files) = @_;
159 $dir =~ s|/$||;
160 my @dirs;
161 for(@files) {
162 if( -d "$dir/$_" && ($_ !~ /^\.(|\.)$/)) {
163 push @dirs, $_;
164 }
165 }
166 return @dirs;
167}
168
169sub singlefile {
170 my ($file) = @_;
171 my $hash;
172 my $info;
173
174 if($file =~ /\.ogg$/i) {
175 $hash = get_oggtag($file);
176 $info = get_ogginfo($file);
177 }
178 else {
179 $hash = get_mp3tag($file);
180 $info = get_mp3info($file);
181 if (defined $$info{'BITRATE'}) {
182 $$hash{'BITRATE'} = $$info{'BITRATE'};
183 }
184
185 if (defined $$info{'SECS'}) {
186 $$hash{'SECS'} = $$info{'SECS'};
187 }
188 }
189
190 return $hash;
191}
192
193sub dodir {
194 my ($dir)=@_;
195
196 my %lcartists;
197 my %lcalbums;
198
199 print "$dir\n";
200
201 # getdir() returns all entries in the given dir
202 my @a = getdir($dir);
203
204 # extractmp3 filters out only the mp3 files from all given entries
205 my @m = extractmp3($dir, @a);
206
207 my $f;
208
209 for $f (sort @m) {
210
211 my $id3 = singlefile("$dir/$f");
212
213 if (not defined $$id3{'ARTIST'} or $$id3{'ARTIST'} eq "") {
214 $$id3{'ARTIST'} = "<Untagged>";
215 }
216
217 # Only use one case-variation of each artist
218 if (exists($lcartists{lc($$id3{'ARTIST'})})) {
219 $$id3{'ARTIST'} = $lcartists{lc($$id3{'ARTIST'})};
220 }
221 else {
222 $lcartists{lc($$id3{'ARTIST'})} = $$id3{'ARTIST'};
223 }
224 #printf "Artist: %s\n", $$id3{'ARTIST'};
225
226 if (not defined $$id3{'ALBUM'} or $$id3{'ALBUM'} eq "") {
227 $$id3{'ALBUM'} = "<Untagged>";
228 if ($dirisalbum) {
229 $$id3{'ALBUM'} = $dir;
230 }
231 }
232
233 # Only use one case-variation of each album
234 if (exists($lcalbums{lc($$id3{'ALBUM'})})) {
235 $$id3{'ALBUM'} = $lcalbums{lc($$id3{'ALBUM'})};
236 }
237 else {
238 $lcalbums{lc($$id3{'ALBUM'})} = $$id3{'ALBUM'};
239 }
240 #printf "Album: %s\n", $$id3{'ALBUM'};
241
242 if (not defined $$id3{'GENRE'} or $$id3{'GENRE'} eq "") {
243 $$id3{'GENRE'} = "<Untagged>";
244 }
245 #printf "Genre: %s\n", $$id3{'GENRE'};
246
247 if (not defined $$id3{'TITLE'} or $$id3{'TITLE'} eq "") {
248 # fall back on basename of the file if no title tag.
249 ($$id3{'TITLE'} = $f) =~ s/\.\w+$//;
250 }
251 #printf "Title: %s\n", $$id3{'TITLE'};
252
253 my $path = "$dir/$f";
254 if ($strip ne "" and $path =~ /^$strip(.*)/) {
255 $path = $1;
256 }
257
258 if ($add ne "") {
259 $path = $add . $path;
260 }
261 #printf "Path: %s\n", $path;
262
263 if (not defined $$id3{'COMPOSER'} or $$id3{'COMPOSER'} eq "") {
264 $$id3{'COMPOSER'} = "<Untagged>";
265 }
266 #printf "Composer: %s\n", $$id3{'COMPOSER'};
267
268 if (not defined $$id3{'YEAR'} or $$id3{'YEAR'} eq "") {
269 $$id3{'YEAR'} = "-1";
270 }
271 #printf "Year: %s\n", $$id3{'YEAR'};
272
273 if (not defined $$id3{'TRACKNUM'} or $$id3{'TRACKNUM'} eq "") {
274 $$id3{'TRACKNUM'} = "-1";
275 }
276 #printf "Track num: %s\n", $$id3{'TRACKNUM'};
277
278 if (not defined $$id3{'BITRATE'} or $$id3{'BITRATE'} eq "") {
279 $$id3{'BITRATE'} = "-1";
280 }
281 #printf "Bitrate: %s\n", $$id3{'BITRATE'};
282
283 if (not defined $$id3{'SECS'} or $$id3{'SECS'} eq "") {
284 $$id3{'SECS'} = "-1";
285 }
286 #printf "Length: %s\n", $$id3{'SECS'};
287
288 $$id3{'PATH'} = $path;
289 $entries{$path} = $id3;
290 }
291
292 # extractdirs filters out only subdirectories from all given entries
293 my @d = extractdirs($dir, @a);
294 my $d;
295
296 for $d (sort @d) {
297 $dir =~ s|/$||;
298 dodir("$dir/$d");
299 }
300}
301
302use_mp3_utf8(1);
303dodir($dir);
304print "\n";
305
306sub dumpshort {
307 my ($num)=@_;
308
309 # print "int: $num\n";
310
311 if ($littleendian) {
312 print DB pack "v", $num;
313 }
314 else {
315 print DB pack "n", $num;
316 }
317}
318
319sub dumpint {
320 my ($num)=@_;
321
322# print "int: $num\n";
323
324 if ($littleendian) {
325 print DB pack "V", $num;
326 }
327 else {
328 print DB pack "N", $num;
329 }
330}
331
332sub dump_tag_string {
333 my ($s, $index) = @_;
334
335 my $strlen = length($s)+1;
336 my $padding = $strlen%4;
337 if ($padding > 0) {
338 $padding = 4 - $padding;
339 $strlen += $padding;
340 }
341
342 dumpshort($strlen);
343 dumpshort($index);
344 print DB $s."\0";
345
346 for (my $i = 0; $i < $padding; $i++) {
347 print DB "X";
348 }
349}
350
351sub dump_tag_header {
352 my ($entry_count) = @_;
353
354 my $size = tell(DB) - 12;
355 seek(DB, 0, 0);
356
357 dumpint($dbver);
358 dumpint($size);
359 dumpint($entry_count);
360}
361
362sub openfile {
363 my ($f) = @_;
364 open(DB, "> $f") || die "couldn't open $f";
365 binmode(DB);
366}
367
368sub create_tagcache_index_file {
369 my ($index, $key, $unique) = @_;
370
371 my $num = 0;
372 my $prev = "";
373 my $offset = 12;
374
375 openfile $db ."_".$index.".tcd";
376 dump_tag_header(0);
377
378 for(sort {uc($entries{$a}->{$key}) cmp uc($entries{$b}->{$key})} keys %entries) {
379 if (!$unique || !($entries{$_}->{$key} eq $prev)) {
380 my $index;
381
382 $num++;
383 $prev = $entries{$_}->{$key};
384 $offset = tell(DB);
385 printf(" %s\n", $prev) if ($verbose);
386
387 if ($unique) {
388 $index = 0xFFFF;
389 }
390 else {
391 $index = $entries{$_}->{'INDEX'};
392 }
393 dump_tag_string($prev, $index);
394 }
395 $entries{$_}->{$key."_OFFSET"} = $offset;
396 }
397
398 dump_tag_header($num);
399 close(DB);
400}
401
402if (!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;
406}
407
408my $i = 0;
409for (sort keys %entries) {
410 $entries{$_}->{'INDEX'} = $i;
411 $i++;
412}
413
414if ($db) {
415 # Artists
416 create_tagcache_index_file(0, 'ARTIST', 1);
417 # Albums
418 create_tagcache_index_file(1, 'ALBUM', 1);
419 # Genres
420 create_tagcache_index_file(2, 'GENRE', 1);
421 # Titles
422 create_tagcache_index_file(3, 'TITLE', 0);
423 # Filenames
424 create_tagcache_index_file(4, 'PATH', 0);
425 # Composers
426 create_tagcache_index_file(5, 'COMPOSER', 1);
427
428 # Master index file
429 openfile $db ."_idx.tcd";
430 dump_tag_header(0);
431
432 for (sort keys %entries) {
433 dumpint($entries{$_}->{'ARTIST_OFFSET'});
434 dumpint($entries{$_}->{'ALBUM_OFFSET'});
435 dumpint($entries{$_}->{'GENRE_OFFSET'});
436 dumpint($entries{$_}->{'TITLE_OFFSET'});
437 dumpint($entries{$_}->{'PATH_OFFSET'});
438 dumpint($entries{$_}->{'COMPOSER_OFFSET'});
439 dumpint($entries{$_}->{'YEAR'});
440 dumpint($entries{$_}->{'TRACKNUM'});
441 dumpint($entries{$_}->{'BITRATE'});
442 dumpint($entries{$_}->{'SECS'});
443 dumpint(0);
444 }
445
446 dump_tag_header(scalar keys %entries);
447 close(DB);
448}