#!/usr/bin/perl -w

# This file is part of mpd-hits.
# Copyright (C) 2010 Dmitry Samoyloff.
#
# mpd-hits is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# mpd-hits is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with mpd-hits. If not, see <http://www.gnu.org/licenses/>.

use strict;

use Audio::MPD;
use Audio::MPD::Common; # Use it explicitly to make its $VERSION available.
use File::Basename 'basename';
use File::Path 'mkpath';
use Proc::Daemon;
use Proc::PID::File;
use Config::Simple;
use Getopt::Mixed 1.006; # Customization vars first appeared in this version
use Sys::Syslog;
use Sort::Versions;

# Install signal handlers to cleanup before termination
$SIG{'TERM'}=\&terminator;
$SIG{'INT'}=\&terminator;
$SIG{'__DIE__'}=sub { exitprog(1, "Dying: @_") };

my $run_as_user=''; # If empty, user wouldn't change
my $run_as_group=''; # If empty, group wouldn't change
my $progname=basename($0);
my $daemon=0;
my $started=0;
my $host='localhost';
my $password;
my $port=6600;
my $config_file="/etc/$progname.conf";
my $db_dir="/var/lib/$progname"; # Directory of statistics DB
my $lock_dir="/var/run/$progname";
my $lock_filename="$progname.pid"; # Just a guess of how module would call it
my $playing_file="$db_dir/playing";#ID of song played during last termination
my $field_sep=':::'; # Field separator in DB files
my $period=1; # Polling period, sec
my $current_id=-1; # ID of currently playing song. -1 if playback is stopped.
my $registered_id=-1; # ID of last registered song. -1 if playback is stopped.
my $elapsed=0; # Number of seconds the current song played
my ($state, $status, $song); # Make'em global to reach from undead_spell()
my $amc_too_old=(versioncmp($Audio::MPD::Common::VERSION, '0.1.3')==-1);

my $version='0.2.0'; # sed'ed before release

# Hide command line so MPD password wouldn't show up in "ps" output
$0=$progname;

# Parse command line arguments, use some of them
use vars qw($opt_check_running $opt_daemon $opt_db_directory $opt_group
            $opt_help $opt_host $opt_kill $opt_lock_directory $opt_password
            $opt_port $opt_user $opt_version);
Getopt::Mixed::init('check-running c>check-running daemon d>daemon '.
                    'db-directory=s group=s help h>help host=s kill '.
                    'k>kill lock-directory=s password=s port=s user=s '.
                    'version V>version');
$Getopt::Mixed::badOption=sub { usage(-1) };
Getopt::Mixed::getOptions();
usage(1) if scalar @ARGV; # Unknown stuff remained in command line
usage(0) if defined $opt_help; # Show usage info and quit
version() if defined $opt_version; # Show version and quit
if (defined $opt_daemon) { # Daemonize program
    $daemon=1;
    openlog $progname, 'pid', 'user'; # Setup logging for daemon
    Proc::Daemon::Init();
}

# Read configuration file(s)
my %config;
if (Config::Simple->import_from($config_file, \%config)) {
    foreach my $key (keys %config) {
        if ($key eq 'HOST') {
            $host=$config{$key};
        } elsif ($key eq 'PORT') {
            $port=$config{$key};
        } elsif ($key eq 'PASSWORD') {
            $password=$config{$key};
        } elsif ($key eq 'USER') {
            $run_as_user=$config{$key};
        } elsif ($key eq 'GROUP') {
            $run_as_group=$config{$key};
        } elsif ($key eq 'LOCK_DIRECTORY') {
            $lock_dir=$config{$key};
        } elsif ($key eq 'DB_DIRECTORY') {
            $db_dir=$config{$key};
        } else {
            log_message('err', "Ignored unknown key `$key' ".
                        "in configuration file $config_file");
        }
    }
} else {
    log_message('err', "Error reading configuration file $config_file: ".
                Config::Simple->error());
}

# Check environment variables.
my ($host_e, $passw_e)=reverse split '@', $ENV{'MPD_HOST'} if $ENV{'MPD_HOST'};
$host=$host_e if $host_e;
$password=$passw_e if $passw_e;
$port=$ENV{'MPD_PORT'} if $ENV{'MPD_PORT'};

# Use more command line arguments
$host=$opt_host if defined $opt_host; # Get mpd host
$port=$opt_port if defined $opt_port; # Get mpd port
$password=$opt_password if defined $opt_password; # Get mpd password
$run_as_user=$opt_user if defined $opt_user;
$run_as_group=$opt_group if defined $opt_group;
$lock_dir=$opt_lock_directory if defined $opt_lock_directory;
$db_dir=$opt_db_directory if defined $opt_db_directory;

# Set real and effective user and group of this process
if ($run_as_group) {
    my $gid=getgrnam($run_as_group);
    exitprog(1, "No such group '$run_as_group'") unless defined $gid;
    ($(, $))=($gid, $gid);
    exitprog(1, "Can't set GID to '$run_as_group'") if $(!=$gid or $)!=$gid;
}
if ($run_as_user) {
    my $uid=getpwnam($run_as_user);
    exitprog(1, "No such user '$run_as_user'") unless defined $uid;
    ($<, $>)=($uid, $uid);
    exitprog(1, "Can't set UID to '$run_as_user'") if $<!=$uid or $>!=$uid;
}

# Check permissions of the lock and db directories
exitprog(1, "Can't access lock directory $lock_dir")
    if not -r $lock_dir or not -w $lock_dir;
exitprog(1, "Can't access db directory $db_dir")
    if not -r $db_dir or not -w $db_dir;

# Use remained command line arguments
if (defined $opt_check_running) {
    if (Proc::PID::File->running(dir=>$lock_dir, verify=>1)) {
        exitprog(0, 'check-running: Running');
    } else {
        exitprog(1, 'check-running: Not running');
    }
}
if (defined $opt_kill) {
    # Kill another currently running process of this program and quit
    if (open PIDFILE, '<', "$lock_dir/$lock_filename") {
        my $pid=<PIDFILE>;
        log_message('info', "Killing $pid\n");
        kill TERM=>$pid;
        close PIDFILE;
    }
    exitprog(0);
}

$started=1;
log_message('info', 'Started');

# Check if this program already running.
# Purposely doing this *after* daemonization!
exitprog(0, 'Already running')
    if Proc::PID::File->running(dir=>$lock_dir, verify=>1);

my $mpd;
log_message('info', "Trying to connect to MPD at $host:$port ...");
until (defined undead_spell('$mpd=Audio::MPD->new(host=>$host, port=>$port);'))
{ sleep $period };
log_message('info', 'Connected to MPD');
$mpd->password($password) if defined $password;

# Find out which song played during last termination and its playing time
if (open PLAYING, '<', $playing_file) {
    my $song_str=<PLAYING>;
    chomp $song_str;
    my $played=<PLAYING>;
    undead_spell('$state=$mpd->status()->state();');
    if (defined $state and
        ($state eq 'play' or
         $state eq 'pause')) {
        undead_spell('$song=$mpd->current();');
        if (defined $song) {
            my %song_info=get_song_info($song);
            if ($song_str eq make_song_string(\%song_info)) {
                $current_id=$song->id();
                $elapsed=$played;
            }
        }
    }
    close PLAYING;
    unlink $playing_file;
}

mkpath($db_dir);

while (1) {
    undead_spell('$status=$mpd->status();');
    next if not defined $status;

    $elapsed++ if $status->state() eq 'play';

    if ($status->state() eq 'play' or
        $status->state() eq 'pause') {
        undead_spell('$song=$mpd->current();');
        next if not defined $song;
        if ($song->id()!=$current_id) {
            # Another song playing now.
            $current_id=$song->id();
            $elapsed=0;
            print "\n";
        }
        my $length=$song->time();
        $|=1;
        print "Played $elapsed of $length sec of ".
            $song->artist().' / '.$song->title()."\r";
        if ($song->file()!~m@\w+://@ and # This is not a stream.
            $registered_id!=$current_id and # Song is not registered yet.
            $elapsed>=$length/2) # More than a half of song played.
        {
            $registered_id=$current_id;
            my %song_info=get_song_info($song);
            append_record(\%song_info);
        }
    } else {
        print "\nNot playing." if ($current_id!=-1);
        $current_id=-1;
        $registered_id=-1;
        $elapsed=0;
    }
} continue {
    sleep $period;
}

sub get_song_info {
    my $song=shift @_;

    my %song_info;
    $song_info{'artist'}=$song->artist() || 'Unknown artist';
    $song_info{'title'}=$song->title() || 'Unknown title';
    $song_info{'album'}=$song->album() || 'Unknown album';
    $song_info{'date'}=
        $amc_too_old ? 'Unknown date' : ($song->date() || 'Unknown date');
    $song_info{'genre'}=
        $amc_too_old ? 'Unknown genre' : ($song->genre() || 'Unknown genre');
    $song_info{'length'}=
        $song->time() || 0; # Length (sec) of currently playing song
    return %song_info;
}

sub append_record {
    my $song_info=shift @_;

    # Construct file name
    my @lt=localtime(time);
    my $filename=sprintf("$db_dir/%04d.%02d", $lt[5]+1900, $lt[4]+1);

    # Add record
    my $day=sprintf("%04d.%02d.%02d", $lt[5]+1900, $lt[4]+1, $lt[3]);
    my $time=sprintf("%02d:%02d:%02d", $lt[2], $lt[1], $lt[0]);
    open DBFILE, '>>', $filename or
        exitprog(1, "Can't write statistics into $filename: $!");
    chmod 0644, $filename;
    print DBFILE "$day$field_sep$time$field_sep".
        make_song_string($song_info)."\n";
    close DBFILE;

    # Print to stdout also
    print "\n------- Registered: -------\n";
    print "Artist: ".$song_info->{'artist'}."\n";
    print "Title:  ".$song_info->{'title'}."\n";
    print "Album:  ".$song_info->{'album'}."\n";
    print "Date:   ".$song_info->{'date'}."\n";
    print "Genre:  ".$song_info->{'genre'}."\n";
    print "Length: ".$song_info->{'length'}."\n";
    print "---------------------------\n";
}

sub make_song_string {
    my $song_info=shift @_;
    return
        "$song_info->{'artist'}$field_sep".
        "$song_info->{'title'}$field_sep".
        "$song_info->{'album'}$field_sep".
        "$song_info->{'date'}$field_sep".
        "$song_info->{'genre'}$field_sep".
        "$song_info->{'length'}";
}

sub log_message {
    my $priority=shift @_;
    my $message=shift @_;

    if ($daemon) {
        syslog $priority, $message;
    } else {
        print "[$$]: $message\n";
    }
}

sub undead_spell {
    my $r=eval '{ local $SIG{__DIE__};'.$_[0].'}';
# Commented out to prevent system log flooding
#    log_message('err', $@) if $@;
    return $r;
}

sub exitprog {
    my $code=shift @_;
    my $message=shift @_ if defined $_[0];

    # Put last messages into the log
    my $priority=$code ? 'err' :'info';
    log_message($priority, $message) if $message;
    log_message('info', 'Terminated') if $started;

    # Store currently playing song info in file.
    if (defined $mpd) {
        undead_spell('$state=$mpd->status()->state();');
        if (defined $state and
            ($state eq 'play' or $state eq 'pause')) {
            undead_spell('$song=$mpd->current();');
            if (defined $song) {
                my %song_info=get_song_info($song);
                if (open PLAYING, '>', $playing_file) {
                    chmod 0644, $playing_file;
                    print PLAYING make_song_string(\%song_info)."\n";
                    print PLAYING $elapsed."\n";
                    close PLAYING;
                }
            }
        }
    }

    closelog() if ($daemon);
    exit $code;
}

sub usage {
    my $code=shift @_;

    print "Usage: $progname [OPTION]...\n";
    print "\n";
    print "Options:\n";
    print "  -d, --daemon\n";
    print "  -c, --check-running\n";
    print "  -k, --kill\n";
    print "      --host=MPDHOST\n";
    print "      --port=MPDPORT\n";
    print "      --password=MPDPASSW\n";
    print "      --user=USERNAME\n";
    print "      --group=GROUPNAME\n";
    print "      --lock-directory=LOCKDIR\n";
    print "      --db-directory=DBDIR\n";
    print "  -h, --help\n";
    print "  -V, --version\n";

    exitprog($code);
}

sub version {
    exitprog(0, "$progname $version");
}

sub terminator {
    exitprog(0);
}
