package Plugins::MOD;

# SlimServer Copyright (c) 2001-2004 Sean Adams, Slim Devices Inc.
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License,
# version 2.

# Created from many other scripts by Gerph (11 Oct 2005)
#
# Purpose:
#    Provides transcoding of ProTracker (and other variant) .mod files
#    to MP3, allowing playback.
#
# Limitations:
#    Tags are not set other than the comment.
#    Titles are not always set.
#    No artist or album details are given; possibly these should be forced
#      to '.Mod' or something as these files should not be in the main
#      collection.
#    No size or duration information is ever extracted.
#    We are limited by the fact that mikmod sleeps, rather than trying to
#      process as much of the file as it can, quickly.
#
# Requirements/Installation:
#    This source - copy it into the Plugins directory
#    Lame - download and build yourself a copy, or obtain it some other
#           way. Try http://lame.sf.net/download/download.html
#           There does not seem to be a debian package for it.
#    MikMod - copy it into the Bin directory or install it in the path.
#          You can get it from http://mikmod.raphnet.net/.
#          On debian, use 'apt-get install mikmod'.
#    Modifications to the custom-types.conf and slimserver-convert.conf files
#      - you can do this automatically by doing the following :
#          perl -MMOD -e Plugins::MOD::AddTypes
#    Restarting the server.
#
# Note: The checking and algorithms in here are not perfect by any means
#       and have been cut from that of the libmikmod sources - check that
#       out for more details. The extension and function names are taken
#       from the relevant files.

###############################################################################
# FILE: Plugins::MOD.pm
#
# DESCRIPTION:
#     Fake some information just so that mods play. Not entirely accurate(!)
#
###############################################################################

use strict;

our $config_artist = "[Trackers]";

sub getDisplayName { return 'PLUGIN_MOD' };
# Export the version to the server
use vars qw($VERSION);
$VERSION = "0.02";
# 0.01 (13 Oct 2005) - initial version created from a few other plugins
# 0.02 (17 Oct 2005) - added artist grouping

$Slim::Music::Info::tagFunctions{'mod'} = {
		'module' => 'Plugins::MOD',
		'loaded' => 1,
		'getTag' => \&Plugins::MOD::getTagMOD,
};

$Slim::Music::Info::tagFunctions{'s3m'} = {
		'module' => 'Plugins::MOD',
		'loaded' => 1,
		'getTag' => \&Plugins::MOD::getTagS3M,
};

$Slim::Music::Info::tagFunctions{'stm'} = {
		'module' => 'Plugins::MOD',
		'loaded' => 1,
		'getTag' => \&Plugins::MOD::getTagSTM,
};

$Slim::Music::Info::tagFunctions{'it'} = {
		'module' => 'Plugins::MOD',
		'loaded' => 1,
		'getTag' => \&Plugins::MOD::getTagIT,
};

$Slim::Music::Info::tagFunctions{'xm'} = {
		'module' => 'Plugins::MOD',
		'loaded' => 1,
		'getTag' => \&Plugins::MOD::getTagXM,
};

$Slim::Music::Info::tagFunctions{'med'} = {
		'module' => 'Plugins::MOD',
		'loaded' => 1,
		'getTag' => \&Plugins::MOD::getTagMED,
};

sub strings
{
    local $/ = undef;
    <DATA>;
}

# Given a file, return a hash of name value pairs,
# where each name is a tag name.
sub getTagMOD {
	my $tags = {};

	my $file = shift || "";
	my $id;
	my $title;
	
	if (!open(FILE, "< $file"))
	{ return $tags; }
	
	seek(FILE, 0x438, 0); # seek to end of header
	read(FILE, $id, 4);
	
	if ($id eq "M.K." || $id eq "M!K!")
	{ $tags->{'COMMENT'} = "Protracker"; 
	  $tags->{'CHANNELS'} = 4; }
	elsif ($id =~ /^FLT([0-9])/ || $id =~ /^EXO([0-9])/)
	{ $tags->{'COMMENT'} = "Star Tracker";  
	  $tags->{'CHANNELS'} = 0+$1; }
	elsif ($id eq "OCTA")
	{ $tags->{'COMMENT'} = "Oktalyzer (Amiga)";  
	  $tags->{'CHANNELS'} = 8; }
	elsif ($id eq "CD81")
	{ $tags->{'COMMENT'} = "Oktalyzer (Atari)";  
	  $tags->{'CHANNELS'} = 8; }
	elsif ($id =~ /^([0-9])CHN/)
	{ $tags->{'COMMENT'} = "Fasttracker";  
	  $tags->{'CHANNELS'} = 0+$1; }
	elsif ($id =~ /^([0-9][0-9])C(H)/ || $id =~ /^([0-9][0-9])C(N)/)
	{ $tags->{'COMMENT'} = ($2 eq "H") ? "Fasttracker" : "Taketracker";  
	  $tags->{'CHANNELS'} = 0+$1; }
	else
	{ 
	  close(FILE);
	  return &getTagM15($file); # Try the getTagM15 instead
	}

	seek(FILE, 0x0, 0); # seek to start of file
	read(FILE, $title, 20);
	$title=~ s/\0//;
	$tags->{'TITLE'} = $title;
	$tags->{'ARTIST'} = $config_artist;
	$tags->{'STEREO'} = 2;

	$tags->{'SECS'} = -1; # We're going to lie, just so that we have a value
	$tags->{'SIZE'} = -s $file;

	# stolen from MP3::Info
	$tags->{'MM'}	    = int $tags->{'SECS'} / 60;
	$tags->{'SS'}	    = int $tags->{'SECS'} % 60;
	$tags->{'MS'}	    = (($tags->{'SECS'} - ($tags->{'MM'} * 60) - $tags->{'SS'}) * 1000);
	$tags->{'TIME'}	    = sprintf "%.2d:%.2d", @{$tags}{'MM', 'SS'};

	close(FILE); return $tags;
}

# Given a file, return a hash of name value pairs,
# where each name is a tag name.
sub getTagM15 {
	my $tags = {};

	my $file = shift || "";
	my $id;
	my $title;
	
	if (!open(FILE, "< $file"))
	{ return $tags; }
	
	seek(FILE, 0x0, 0); # seek to start
	read(FILE, $title, 20);
	if ($title =~ /[\x01-\x1f]/)
	{ # Not a valid name 'cos it contains controls
	  close(FILE); return $tags; }
	if ($title =~ /\x00[^\x00]/)
	{ # Not a valid name 'cos it has a nul inside the string
	  close(FILE); return $tags; }
	# I'm ignoring the sample details checks because we /assume/ that
	# the extension is a reasonable discriminator.
	# Of course, mikmod will fail anything, so we don't lose that much.
	
	if ($title =~ /^CAKEWALK/ || $title =~ /^SZDD/)
	{ # This check is quick and easy - things that fool mikmod
	  close(FILE); return $tags; }

        $tags->{'COMMENT'} = "Soundtracker"; 
	$title=~ s/\0//;
	$title=~ s/ +$//;
	$tags->{'TITLE'} = $title;
	$tags->{'ARTIST'} = $config_artist;
	$tags->{'STEREO'} = 2;

	$tags->{'SECS'} = -1; # We're going to lie, just so that we have a value
	$tags->{'SIZE'} = -s $file;

	# stolen from MP3::Info
	$tags->{'MM'}	    = int $tags->{'SECS'} / 60;
	$tags->{'SS'}	    = int $tags->{'SECS'} % 60;
	$tags->{'MS'}	    = (($tags->{'SECS'} - ($tags->{'MM'} * 60) - $tags->{'SS'}) * 1000);
	$tags->{'TIME'}	    = sprintf "%.2d:%.2d", @{$tags}{'MM', 'SS'};

	close(FILE); return $tags;
}

# Given a file, return a hash of name value pairs,
# where each name is a tag name.
sub getTagS3M {
	my $tags = {};

	my $file = shift || "";
	my $id;
	my $title;
	
	if (!open(FILE, "< $file"))
	{ return $tags; }
	
	seek(FILE, 0x2c, 0); # seek to end of header
	read(FILE, $id, 4);
	
	if ($id eq "SCRM")
	{ $tags->{'COMMENT'} = "Screamtracker 3"; 
	  # $tags->{'CHANNELS'} = 4; # Dunno this one
	}
	else
	{ close(FILE); return $tags; } # Not recognised

	seek(FILE, 0x0, 0); # seek to start of file
	read(FILE, $title, 28);
	$title=~ s/\0//;
	$tags->{'TITLE'} = $title;
	$tags->{'ARTIST'} = $config_artist;
	$tags->{'STEREO'} = 2;

	$tags->{'SECS'} = -1; # We're going to lie, just so that we have a value
	$tags->{'SIZE'} = -s $file;

	# stolen from MP3::Info
	$tags->{'MM'}	    = int $tags->{'SECS'} / 60;
	$tags->{'SS'}	    = int $tags->{'SECS'} % 60;
	$tags->{'MS'}	    = (($tags->{'SECS'} - ($tags->{'MM'} * 60) - $tags->{'SS'}) * 1000);
	$tags->{'TIME'}	    = sprintf "%.2d:%.2d", @{$tags}{'MM', 'SS'};

	close(FILE); return $tags;
}

# Given a file, return a hash of name value pairs,
# where each name is a tag name.
sub getTagSTM {
	my $tags = {};

	my $file = shift || "";
	my $id;
	my $title;
	
	if (!open(FILE, "< $file"))
	{ return $tags; }
	
	seek(FILE, 20, 0); # seek to end of header
	read(FILE, $id, 44);
	
	if ($id =~ /SCRM$/ &&
	    ($id =~ /^Screamtr/ || # 8 character limit - see libmikmod
	     $id =~ /^Converte/ ||
	     $id =~ /^Wuzamod /))
	{ $tags->{'COMMENT'} = "Screamtracker 2"; 
	  $tags->{'CHANNELS'} = 4; # Always 4 apparently
	}
	else
	{ close(FILE); return $tags; } # Not recognised

	seek(FILE, 0x0, 0); # seek to start of file
	read(FILE, $title, 20);
	$title=~ s/\0//;
	$title=~ s/ *$//;
	$tags->{'TITLE'} = $title;
	$tags->{'ARTIST'} = $config_artist;
	$tags->{'STEREO'} = 2;

	$tags->{'SECS'} = -1; # We're going to lie, just so that we have a value
	$tags->{'SIZE'} = -s $file;

	# stolen from MP3::Info
	$tags->{'MM'}	    = int $tags->{'SECS'} / 60;
	$tags->{'SS'}	    = int $tags->{'SECS'} % 60;
	$tags->{'MS'}	    = (($tags->{'SECS'} - ($tags->{'MM'} * 60) - $tags->{'SS'}) * 1000);
	$tags->{'TIME'}	    = sprintf "%.2d:%.2d", @{$tags}{'MM', 'SS'};

	close(FILE); return $tags;
}

# Given a file, return a hash of name value pairs,
# where each name is a tag name.
sub getTagIT {
	my $tags = {};

	my $file = shift || "";
	my $id;
	my $title;
	
	if (!open(FILE, "< $file"))
	{ return $tags; }
	
	seek(FILE, 0x0, 0); # seek to start
	read(FILE, $id, 4);
	
	if ($id eq "IMPM")
	{ $tags->{'COMMENT'} = "Impluse Tracker"; 
	  # $tags->{'CHANNELS'} = 4; # Dunno this one
	}
	else
	{ close(FILE); return $tags; } # Not recognised

	seek(FILE, 0x4, 0); # seek to after the header
	read(FILE, $title, 26);
	$title=~ s/\0//;
	$tags->{'TITLE'} = $title;
	$tags->{'ARTIST'} = $config_artist;
	$tags->{'STEREO'} = 2;

	$tags->{'SECS'} = -1; # We're going to lie, just so that we have a value
	$tags->{'SIZE'} = -s $file;

	# stolen from MP3::Info
	$tags->{'MM'}	    = int $tags->{'SECS'} / 60;
	$tags->{'SS'}	    = int $tags->{'SECS'} % 60;
	$tags->{'MS'}	    = (($tags->{'SECS'} - ($tags->{'MM'} * 60) - $tags->{'SS'}) * 1000);
	$tags->{'TIME'}	    = sprintf "%.2d:%.2d", @{$tags}{'MM', 'SS'};

	close(FILE); return $tags;
}

# Given a file, return a hash of name value pairs,
# where each name is a tag name.
sub getTagXM {
	my $tags = {};

	my $file = shift || "";
	my $id;
	my $title;
	
	if (!open(FILE, "< $file"))
	{ return $tags; }
	
	seek(FILE, 0, 0); # seek to start
	read(FILE, $id, 38);
	
	if ($id =~ /Extended Module: (.{20})\x1a/)
	{ $tags->{'COMMENT'} = "XM (FastTracker 2)"; 
	  # $tags->{'CHANNELS'} = 4; # Dunno this one
	}
	else
	{ close(FILE); return $tags; } # Not recognised
	$title = $1;
	$title =~ s/\0//;
	$title =~ s/ *$//;
	$tags->{'TITLE'} = $title;
	$tags->{'ARTIST'} = $config_artist;
	$tags->{'STEREO'} = 2;

	$tags->{'SECS'} = -1; # We're going to lie, just so that we have a value
	$tags->{'SIZE'} = -s $file;

	# stolen from MP3::Info
	$tags->{'MM'}	    = int $tags->{'SECS'} / 60;
	$tags->{'SS'}	    = int $tags->{'SECS'} % 60;
	$tags->{'MS'}	    = (($tags->{'SECS'} - ($tags->{'MM'} * 60) - $tags->{'SS'}) * 1000);
	$tags->{'TIME'}	    = sprintf "%.2d:%.2d", @{$tags}{'MM', 'SS'};

	close(FILE); return $tags;
}

# Given a file, return a hash of name value pairs,
# where each name is a tag name.
sub getTagMED {
	my $tags = {};

	my $file = shift || "";
	my $id;
	my $title;
	my $pos;
	my $len;
	
	if (!open(FILE, "< $file"))
	{ return $tags; }
	
	seek(FILE, 0, 0); # seek to start
	read(FILE, $id, 4);
	
	if ($id =~ /MMD[01]/)
	{ $tags->{'COMMENT'} = "Amiga MED"; 
	  # $tags->{'CHANNELS'} = 4; # Dunno this one
	}
	else
	{ close(FILE); return $tags; } # Not recognised
	
	seek(FILE, 0x20, 0); # seek to start
	read(FILE, $pos, 4);
	$pos = unpack "v", $pos;
	if ($pos)
	{
          seek(FILE, $pos + 0x2c, 0);
          read(FILE, $title, 4);
          ($pos, $len) = unpack "vv", $title;
  
          seek(FILE, $pos, 0);
	  read(FILE, $title, $len);
	  $tags->{'TITLE'} = $title;
	}
	$tags->{'ARTIST'} = $config_artist;
	
	$tags->{'STEREO'} = 2;

	$tags->{'SECS'} = -1; # We're going to lie, just so that we have a value
	$tags->{'SIZE'} = -s $file;

	# stolen from MP3::Info
	$tags->{'MM'}	    = int $tags->{'SECS'} / 60;
	$tags->{'SS'}	    = int $tags->{'SECS'} % 60;
	$tags->{'MS'}	    = (($tags->{'SECS'} - ($tags->{'MM'} * 60) - $tags->{'SS'}) * 1000);
	$tags->{'TIME'}	    = sprintf "%.2d:%.2d", @{$tags}{'MM', 'SS'};

	close(FILE); return $tags;
}

##########
##########
# We now try to be clever, modifying the existing configuration files if
# possible.

sub AddTypes
{
  my $file;
  my $mikmod;
  
  if (!-e "../slimserver.pl")
  { die "I don't know what you're doing. Copy this file to Plugins. Then try this again, from there\n"; }
  
  print "Attempting to add types to custom.conf\n";
  
  $file = "";
  if (open (IN, "< ../custom-types.conf"))
  {
    # The file exists; we need to read and extract the bits we have
    # so that we can update the file with our new support
    while (<IN>)
    { 
      $file .= $_; # Process $_ if necessary
    } # get the lot
    $file =~ s/\n# MikMod bits\n(.*)# MikMod end\n/\n# MikMod bits\n/s;
  }
  
  if ($file !~ /\n# MikMod bits\n/)
  { $file .= "\n# MikMod bits\n"; }
  
  $mikmod =<<EOM;
mod	mod		audio/x-protracker-mod	audio
s3m	s3m		audio/x-protracker-mod	audio
stm	stm		audio/x-protracker-mod	audio
it	it		audio/x-protracker-mod	audio
xm	xm		audio/x-protracker-mod	audio
med	med		audio/x-protracker-mod	audio
EOM
  $file =~ s/\n# MikMod bits\n/\n# MikMod bits\n$mikmod# MikMod end\n/s;
  open(OUT, "> ../custom-types.conf") || die "Couldn't update custom-types.conf\n";
  print OUT $file;
  close(OUT);
  
  
  
  ########
  # Now we do the same thing with slimserver-convert.conf
  print "Attempting to add types to slimserver-convert.conf\n";
  
  $file = "";
  if (open (IN, "< ../slimserver-convert.conf"))
  {
    # The file exists; we need to read and extract the bits we have
    # so that we can update the file with our new support
    while (<IN>)
    { 
      $file .= $_; # Process $_ if necessary
    } # get the lot
    $file =~ s/\n# MikMod bits\n(.*)# MikMod end\n/\n# MikMod bits\n/s;
  }
  
  if ($file !~ /\n# MikMod bits\n/)
  { $file .= "\n# MikMod bits\n"; }
  
  $mikmod =<<EOM;
mod mp3 * *
	[mikmod] -norc -X -d stdout -q -o 16s \$FILE\$ | [lame] --quiet -x -r -s 44.1 -
s3m mp3 * *
	[mikmod] -norc -X -d stdout -q -o 16s \$FILE\$ | [lame] --quiet -x -r -s 44.1 -
stm mp3 * *
	[mikmod] -norc -X -d stdout -q -o 16s \$FILE\$ | [lame] --quiet -x -r -s 44.1 -
it mp3 * *
	[mikmod] -norc -X -d stdout -q -o 16s \$FILE\$ | [lame] --quiet -x -r -s 44.1 -
xm mp3 * *
	[mikmod] -norc -X -d stdout -q -o 16s \$FILE\$ | [lame] --quiet -x -r -s 44.1 -
med mp3 * *
	[mikmod] -norc -X -d stdout -q -o 16s \$FILE\$ | [lame] --quiet -x -r -s 44.1 -
EOM
  $file =~ s/\n# MikMod bits\n/\n# MikMod bits\n$mikmod# MikMod end\n/s;
  open(OUT, "> ../slimserver-convert.conf") || die "Couldn't update slimserver-convert.conf\n";
  print OUT $file;
  close(OUT);
  
  print "\nFiles updated.\n";
  print "\nYou will need to restart the server for this plugin to take effect.\n";
}

1;

__DATA__

PLUGIN_MOD
	EN	MOD file processor

MOD
	EN	SoundTracker/ProTracker module

STM
	EN	ScreamTracker 2 module

S3M
	EN	ScreamTracker 3 module

IT
	EN	Impluse Tracker module

XM
	EN	FastTracker 2 module

MED
	EN	Amiga Music Editor module
