#!/usr/bin/perl
# Filename:	mpls
# Author:	David Ljung Madison <DaveSource.com>
# 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 <<USAGE;

Usage:\t$PROGNAME [-xq] <file | directory>

\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 <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*$/;
          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;
}

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();
