A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita
audio
rust
zig
deno
mpris
rockbox
mpd
1#!/usr/bin/perl
2#
3# Rockbox song database docs:
4# http://www.rockbox.org/wiki/DataBase
5#
6
7use mp3info;
8use vorbiscomm;
9
10# configuration settings
11my $db = "database";
12my $dir;
13my $strip;
14my $add;
15my $verbose;
16my $help;
17my $dirisalbum;
18my $littleendian = 0;
19my $dbver = 0x54434806;
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 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
84MOO
85;
86 exit;
87}
88
89sub get_oggtag {
90 my $fn = shift;
91 my %hash;
92
93 my $ogg = vorbiscomm->new($fn);
94
95 my $h= $ogg->load;
96
97 # Convert this format into the same format used by the id3 parser hash
98
99 foreach my $k ($ogg->comment_tags())
100 {
101 foreach my $cmmt ($ogg->comment($k))
102 {
103 my $n;
104 if($k =~ /^artist$/i) {
105 $n = 'ARTIST';
106 }
107 elsif($k =~ /^album$/i) {
108 $n = 'ALBUM';
109 }
110 elsif($k =~ /^title$/i) {
111 $n = 'TITLE';
112 }
113 $hash{$n}=$cmmt if($n);
114 }
115 }
116
117 return \%hash;
118}
119
120sub get_ogginfo {
121 my $fn = shift;
122 my %hash;
123
124 my $ogg = vorbiscomm->new($fn);
125
126 my $h= $ogg->load;
127
128 return $ogg->{'INFO'};
129}
130
131# return ALL directory entries in the given dir
132sub getdir {
133 my ($dir) = @_;
134
135 $dir =~ s|/$|| if ($dir ne "/");
136
137 if (opendir(DIR, $dir)) {
138 my @all = readdir(DIR);
139 closedir DIR;
140 return @all;
141 }
142 else {
143 warn "can't opendir $dir: $!\n";
144 }
145}
146
147sub extractmp3 {
148 my ($dir, @files) = @_;
149 my @mp3;
150 for(@files) {
151 if( (/\.mp[23]$/i || /\.ogg$/i) && -f "$dir/$_" ) {
152 push @mp3, $_;
153 }
154 }
155 return @mp3;
156}
157
158sub extractdirs {
159 my ($dir, @files) = @_;
160 $dir =~ s|/$||;
161 my @dirs;
162 for(@files) {
163 if( -d "$dir/$_" && ($_ !~ /^\.(|\.)$/)) {
164 push @dirs, $_;
165 }
166 }
167 return @dirs;
168}
169
170sub singlefile {
171 my ($file) = @_;
172 my $hash;
173 my $info;
174
175 if($file =~ /\.ogg$/i) {
176 $hash = get_oggtag($file);
177 $info = get_ogginfo($file);
178 }
179 else {
180 $hash = get_mp3tag($file);
181 $info = get_mp3info($file);
182 if (defined $$info{'BITRATE'}) {
183 $$hash{'BITRATE'} = $$info{'BITRATE'};
184 }
185
186 if (defined $$info{'SECS'}) {
187 $$hash{'SECS'} = $$info{'SECS'};
188 }
189 }
190
191 return $hash;
192}
193
194sub dodir {
195 my ($dir)=@_;
196
197 my %lcartists;
198 my %lcalbums;
199
200 print "$dir\n";
201
202 # getdir() returns all entries in the given dir
203 my @a = getdir($dir);
204
205 # extractmp3 filters out only the mp3 files from all given entries
206 my @m = extractmp3($dir, @a);
207
208 my $f;
209
210 for $f (sort @m) {
211
212 my $id3 = singlefile("$dir/$f");
213
214 if (not defined $$id3{'ARTIST'} or $$id3{'ARTIST'} eq "") {
215 $$id3{'ARTIST'} = "<Untagged>";
216 }
217
218 # Only use one case-variation of each artist
219 if (exists($lcartists{lc($$id3{'ARTIST'})})) {
220 $$id3{'ARTIST'} = $lcartists{lc($$id3{'ARTIST'})};
221 }
222 else {
223 $lcartists{lc($$id3{'ARTIST'})} = $$id3{'ARTIST'};
224 }
225 #printf "Artist: %s\n", $$id3{'ARTIST'};
226
227 if (not defined $$id3{'ALBUM'} or $$id3{'ALBUM'} eq "") {
228 $$id3{'ALBUM'} = "<Untagged>";
229 if ($dirisalbum) {
230 $$id3{'ALBUM'} = $dir;
231 }
232 }
233
234 # Only use one case-variation of each album
235 if (exists($lcalbums{lc($$id3{'ALBUM'})})) {
236 $$id3{'ALBUM'} = $lcalbums{lc($$id3{'ALBUM'})};
237 }
238 else {
239 $lcalbums{lc($$id3{'ALBUM'})} = $$id3{'ALBUM'};
240 }
241 #printf "Album: %s\n", $$id3{'ALBUM'};
242
243 if (not defined $$id3{'GENRE'} or $$id3{'GENRE'} eq "") {
244 $$id3{'GENRE'} = "<Untagged>";
245 }
246 #printf "Genre: %s\n", $$id3{'GENRE'};
247
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+$//;
251 }
252 #printf "Title: %s\n", $$id3{'TITLE'};
253
254 my $path = "$dir/$f";
255 if ($strip ne "" and $path =~ /^$strip(.*)/) {
256 $path = $1;
257 }
258
259 if ($add ne "") {
260 $path = $add . $path;
261 }
262 #printf "Path: %s\n", $path;
263
264 if (not defined $$id3{'COMPOSER'} or $$id3{'COMPOSER'} eq "") {
265 $$id3{'COMPOSER'} = "<Untagged>";
266 }
267 #printf "Composer: %s\n", $$id3{'COMPOSER'};
268
269 if (not defined $$id3{'YEAR'} or $$id3{'YEAR'} eq "") {
270 $$id3{'YEAR'} = "-1";
271 }
272 #printf "Year: %s\n", $$id3{'YEAR'};
273
274 if (not defined $$id3{'TRACKNUM'} or $$id3{'TRACKNUM'} eq "") {
275 $$id3{'TRACKNUM'} = "-1";
276 }
277 #printf "Track num: %s\n", $$id3{'TRACKNUM'};
278
279 if (not defined $$id3{'BITRATE'} or $$id3{'BITRATE'} eq "") {
280 $$id3{'BITRATE'} = "-1";
281 }
282 #printf "Bitrate: %s\n", $$id3{'BITRATE'};
283
284 if (not defined $$id3{'SECS'} or $$id3{'SECS'} eq "") {
285 $$id3{'SECS'} = "-1";
286 }
287 #printf "Length: %s\n", $$id3{'SECS'};
288
289 $$id3{'PATH'} = $path;
290 $entries{$path} = $id3;
291 }
292
293 # extractdirs filters out only subdirectories from all given entries
294 my @d = extractdirs($dir, @a);
295 my $d;
296
297 for $d (sort @d) {
298 $dir =~ s|/$||;
299 dodir("$dir/$d");
300 }
301}
302
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 # 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);
422
423 # Master index file
424 openfile $db ."_idx.tcd";
425 dump_tag_header(0);
426
427 # current serial
428 dumpint(0);
429
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);
449 }
450
451 dump_tag_header(scalar keys %entries);
452 close(DB);
453}