#!/usr/bin/perl # Filename: mpls # Author: David Ljung Madison # See License: http://MarginalHacks.com/License # Version: 1.04 # Description: Make a playlist, sorted and with extinf meta data for xmms # # Tries to find mp3 track number in notes or filename. # # Does anyone know an easy way to figure out mp3 song duration? # Does anyone know of a faster ogginfo? It sucks rocks! # use strict; use POSIX; # xmms rounds duration down instead of using int use MP3::Info; # Can get mp3 duration info! Thanks Ami Fischman [fischman.org] ################################################## # Setup the variables ################################################## my $PROGNAME = $0; $PROGNAME =~ s|.*/||; # Title format (should match xmms preferences) my $TITLE = "%p - %t"; # xmms default, I think my $OGGINFO_DURATION = "ogginfo"; my $OGGINFO = "vorbiscomment"; # Faster, but no duration info my $MP3INFO = "id3tool"; my @POSTFIXES = qw(ogg mp3); ################################################## # Usage ################################################## sub usage { foreach my $msg (@_) { print STDERR "ERROR: $msg\n"; } print STDERR < \tSearches directory for files with postfix [@POSTFIXES] \tOr reads file (or '-' for stdin) for list of mp3/oggs \tThen creates a sorted playlist \t-title \tTitle format (same as xmms preferences) \t-x\tAdd extinf for xmms (much slower) \t-q\tQuiet USAGE exit -1; } # Read songs from a playlist, or recurse if it's a directory, # looking for .ogg and .mp3 files sub get_songs { my ($path) = @_; my @ret; # Directory. Recurse. if (-d $path) { return () unless opendir(D,$path); my @dir = readdir(D); closedir(D); foreach my $d ( @dir ) { my $full = "$path/$d"; next if $d eq '.' || $d eq '..'; push(@ret, get_songs($full)) if -d $full; push(@ret, $full) if -f $full && grep($d =~ /\.$_$/, @POSTFIXES); } return @ret; } # Filelist open(IN,"<$path") || usage("Couldn't read file input [$path]"); while (<IN>) { chomp; s/^#.*//g; # Ignore commented lines s/^\s*//g; s/\s*$//g; # Ignore whitespace next if /^$/; # Ignore whitespace push(@ret,$_); } close IN; @ret; } sub parse_args { my (@songs,$saw_file,$extinfo); while (my $arg=shift(@ARGV)) { if ($arg =~ /^-h$/) { usage(); } if ($arg =~ /^-x$/) { $extinfo=1; next; } if ($arg =~ /^-t(itle)?$/) { $TITLE = shift(@ARGV); next; } if ($arg =~ /^-q$/) { $MAIN::QUIET=1; next; } if ($arg =~ /^-./) { usage("Unknown option: $arg"); } push(@songs,get_songs($arg)); $saw_file++; } usage("No songs specified") unless @songs; ($extinfo,@songs); } ###################################################################### # Hash code ###################################################################### my $HASHES = 50; my $_hashes_done = 0; my $_hashes_start; sub start_hashes { return if $MAIN::QUIET; my ($str) = @_; $_hashes_start = $str if $str; print STDERR "$_hashes_start ["," "x$HASHES,"]\b","\b"x$HASHES; } sub show_hashes { my ($done,$outof) = @_; return if $MAIN::QUIET; my $needed = int($HASHES*($done/$outof)); print STDERR "X"x($needed-$_hashes_done); $_hashes_done = $needed; } sub stop_hashes { return if $MAIN::QUIET; undef $_hashes_start; print STDERR "]\n"; } sub hash_warn { print STDERR "\n"; foreach my $msg (@_) { print STDERR "WARN: $msg\n"; } start_hashes; print STDERR "X"x$_hashes_done; undef; } ###################################################################### # Info ###################################################################### sub song_info { my ($song,$extinfo) = @_; my %song; $song{file} = $song; if (! -f $song) { hash_warn("Missing: $song"); } else { if ($song =~ /\.mp3$/) { my $info = get_mp3info($song); $song{length} = $info->{MM}*60+$info->{SS}; my $tag = get_mp3tag($song); foreach my $K (keys %$tag) { my ($k,$v) = (lc($K), $tag->{$K}); # Convert some tags to ogg equivalents ($k,$v) = ("tracknumber",$1) if ($k eq "note" && $v =~ /track\s*(\d+)/i); $k = "comment" if $k eq "note"; $k = "date" if $k eq "year"; $song{$k} = $v; } ## The old method, using id3tool # if (open(INF,"$MP3INFO \Q$song\E|")) { # while(<INF>) { # chomp; # next if /^\s*$/; # if (/No ID3 Tag/i) { # hash_warn("No ID3 tag:\n $song\n"); # last; # } # die("[$PROGNAME] Bad mp3info:\n File: $song\n Info: [$_]") # unless /(\S+):\s*(\S?.*)$/; # my ($k,$v) = (lc($1),$2); # $v =~ s/\s+$//g; $v =~ s/^\s+//g; # # Convert some tags to ogg equivalents # ($k,$v) = ("tracknumber",$1) if ($k eq "note" && $v =~ /track\s*(\d+)/i); # $v = $1 if ($k eq "genre" && $v =~ /(\S+)\s+\(0x[0-9a-f]+\)/); # $k = "comment" if $k eq "note"; # $k = "date" if $k eq "year"; # # And store it # $song{$k} = $v if $v; # } # close INF; # # # Hack for guessing tracknumber # $song{tracknumber} = $1 if (!defined $song{tracknumber} && $song =~ m|/^(\d+)[^\d/]+$|); # } else { # hash_warn("Can't $MP3INFO:\n $song"); # } # Hack for guessing tracknumber $song{tracknumber} = $1 if (!defined $song{tracknumber} && $song =~ m|/^(\d+)[^\d/]+$|); } elsif ($song =~ /\.ogg$/) { my $ogginfo = $extinfo ? $OGGINFO_DURATION : $OGGINFO; if (open(INF,"$ogginfo \Q$song\E|")) { while(<INF>) { chomp; next if /^\s*$/; next if $extinfo && !s/^\s+//; ($song{MM},$song{SS}) = ($1,$2) if /Playback length: (\d+)m:(\d+)(\.\d+)?s/; # ogginfo next if $extinfo && !/=/; die("\n[$PROGNAME] Bad ogginfo: [$_]\n") unless /^(\S+)=(.*)$/; $song{$1} = $2; } close INF; } else { hash_warn("Can't $ogginfo:\n $song"); } } else { $song{artist} = $song; $song{title} = $song; $song{album} = $song; #$song{unknown} = 1; # hash_warn("Unknown file type:\n $song"); } } \%song; } sub gather_info { my ($extinfo,@songs) = @_; my %songs; start_hashes("Reading song info: "); my $outof = $#songs+1; my $done = 0; foreach my $song ( @songs ) { $done++; $songs{$song} = song_info($song,$extinfo); show_hashes($done,$outof); } stop_hashes; %songs; } ################################################## # Playlist ################################################## # Fuzzier compares sub strip { my ($str) = @_; $str = lc($str); # Ignore case $str =~ s/[^a-z0-9]//g; # Ignore whitespace, punctuation, ... $str =~ s/^the\s*//ig; # Ignore leading 'The' $str; } sub my_compare { my ($c1,$c2) = @_; return 0 unless defined $c1 && defined $c2; return 1 unless defined $c1; return -1 unless defined $c2; strip($c1) cmp strip($c2); } sub my_compare_num { my ($c1,$c2) = @_; return 0 unless defined $c1 && defined $c2; return 1 unless defined $c1; return -1 unless defined $c2; ($c1+0) <=> ($c2+0); } sub song_sort { my ($s1,$s2) = @_; # First by artist, then album year, album, then track, if available, then song my_compare($s1->{artist},$s2->{artist}) || my_compare_num($s1->{date},$s2->{date}) || my_compare($s1->{album},$s2->{album}) || my_compare_num($s1->{tracknumber},$s2->{tracknumber}) || my_compare($s1->{title},$s2->{title}) || $s1->{file} cmp $s2->{file}; } sub get_title { my ($song) = @_; my $filename = $song->{file}; $filename =~ s|.*/||g; my $ext; $ext=$1 if $filename =~ /\.([^\.]+)$/; my $title = $TITLE; $title =~ s/%p/$song->{artist}/g; $title =~ s/%a/$song->{album}/g; $title =~ s/%g/$song->{genre}/g; $title =~ s/%f/$filename/g; $title =~ s/%F/$song->{file}/g; $title =~ s/%e/$ext/g; $title =~ s/%t/$song->{title}/g; $title =~ s/%n/$song->{tracknumber}/g; $title =~ s/%d/$song->{date}/g; # I don't see how these are different $title =~ s/%y/$song->{year}/g; $title =~ s/%c/$song->{comment}/g; $title; } sub playlist { my ($extinfo,%songs) = @_; print "#EXTM3U\n" if $extinfo; foreach my $file ( sort { song_sort($songs{$a},$songs{$b}) } keys %songs ) { if ($extinfo && !$songs{$file}{unknown} && $songs{$file}{title}) { my $seconds = POSIX::floor($songs{$file}{length}) || 60*3; my $title = get_title($songs{$file}); print "#EXTINF:$seconds,$title\n"; } print "$songs{$file}{file}\n"; } } ################################################## # Main code ################################################## sub main { my ($extinfo,@songs) = parse_args(); my %songs = gather_info($extinfo,@songs); playlist($extinfo,%songs); } main();