A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd
at master 453 lines 10 kB view raw
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}