#!/usr/bin/perl

# This file is part of mpd-hits.
# Copyright (C) 2011, 2012, 2013, 2015 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 warnings 'all';

use File::Basename qw(basename fileparse);
# Customization vars first appeared in this version.
use Getopt::Mixed 1.006 "nextOption";
use DateTime::Format::Strptime;
use File::Copy;
use Audio::FLAC::Header;
use Ogg::Vorbis::Header::PurePerl;
use POSIX;
use Config::Simple;

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 $version='0.3.0'; # sed'ed before release
my $db_dir="/var/lib/mpd-hits"; # Directory of statistics DB
# Contains references to hashes, consisting of each song info.
my @songs;
# Only files ending with these suffixes will be considered as sound files.
my @suffixes=qw(flac ogg);
my $start; # DateTime object of when the listening session started
my $config_file="/etc/mpd-hits.conf";
my $was_root=0;

my $strp=new DateTime::Format::Strptime(
    pattern => '%FT%T',
    time_zone => 'local',
    on_error => 'undef',
    );

# Parse command line arguments, prepare list of songs.
usage(1) if not scalar @ARGV; # No args specified
Getopt::Mixed::init('start=s s>start add=s a>add
                     db-directory=s d>db-directory group=s
                     user=s help h>help version V>version');
$Getopt::Mixed::badOption=sub { usage(1) };
while ((my $option, my $value, my $pretty)=nextOption()) {
    if ($option eq 's' or $option eq 'start') {
        $start=$strp->parse_datetime($value);
        exitprog(1, "Can't parse TIMESPEC") if (not defined $start);
    } elsif ($option eq 'a' or $option eq 'add') {
        if (-d $value and -r $value) {
            opendir(DIR, $value) or die "Can't open directory $value: $!";
            foreach my $file (sort readdir(DIR)) {
                accept_if_song("$value/$file");
            }
            closedir(DIR);
        } elsif (-r $value) {
            accept_if_song($value);
        } else {
            exitprog(1, "Can't read file or directory \"$value\"");
        }
    } elsif ($option eq 'd' or $option eq 'db-directory') {
        $db_dir=$value;
    }
    elsif ($option eq 'user') {
        $run_as_user=$value;
    } elsif ($option eq 'group') {
        $run_as_group=$value;
    }
    usage(0) if $option eq 'h' or $option eq 'help'; # Show usage info and quit
    version() if $option eq 'v' or $option eq 'version';# Show version and quit
}
Getopt::Mixed::cleanup();
usage(1) if scalar @ARGV; # Unknown stuff remained in command line
if (!$start) {
    exitprog(1, "Use --start option to specify the time of first playback.");
}

# Read configuration file(s)
my %config;
if (Config::Simple->import_from($config_file, \%config)) {
    foreach my $key (keys %config) {
        if ($key eq 'USER' and !$run_as_user) {
            $run_as_user=$config{$key};
        } elsif ($key eq 'GROUP' and !$run_as_group) {
            $run_as_group=$config{$key};
        } elsif ($key eq 'DB_DIRECTORY' and !$db_dir) {
            $db_dir=$config{$key};
        }
    }
} else {
    print "Error reading configuration file $config_file: ".
        Config::Simple->error()."\n";
}

# Set real and effective user and group of this process
if ($>==0) {
    if ($run_as_group) {
        my $gid=getgrnam($run_as_group);
        exitprog(1, "No such group '$run_as_group'") unless defined $gid;
        ($(, $))=($gid, $gid);
        if ($(!=$gid or $)!=$gid) {
            exitprog(1, "Can't set GID to '$run_as_group'");
        }
    }
    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;
    }
    $was_root=1;
}

# Fetch all needed tags from audio files and calculate times when each song
# was played.
my $first_song=1;
my $prev_sec=0;
my $prev_dt;
foreach my $file (@songs) {
    if ($first_song) {
        $file->{DT}=$start->clone();
    } else {
        $file->{DT}=$prev_dt->clone();
    }
    $first_song=0;
    my $path=$file->{Path};
    my $sec;

    my ($f, $d, $suffix)=fileparse($path, '\.[^.]*$');
    if ($suffix eq '.flac') {
        my $flac=Audio::FLAC::Header->new($path);

        $sec=ceil($flac->{trackTotalLengthSeconds});

        $file->{Timesec}=$sec;
        $file->{Performer}=$flac->{tags}->{ARTIST};
        $file->{Album}=$flac->{tags}->{ALBUM};
        $file->{Recordeddate}=$flac->{tags}->{DATE};
        $file->{Trackname}=$flac->{tags}->{TITLE};
        $file->{Genre}=$flac->{tags}->{GENRE};

    } elsif ($suffix eq '.ogg') {
        my $ogg=Ogg::Vorbis::Header::PurePerl->new($path);

        $sec=ceil($ogg->info->{length});

        $file->{Timesec}=$sec;
        foreach my $com ($ogg->comment_tags) {
            ($file->{Performer})=$ogg->comment($com) if ($com eq 'artist');
            ($file->{Album})=$ogg->comment($com) if ($com eq 'album');
            ($file->{Recordeddate})=$ogg->comment($com) if ($com eq 'date');
            ($file->{Trackname})=$ogg->comment($com) if ($com eq 'title');
            ($file->{Genre})=$ogg->comment($com) if ($com eq 'genre');
        }
    } else {
        exitprog(1, "Unsupported file type \"$suffix\". ".
                 "Currently supported: @suffixes");
    }

    $file->{DT}->add(seconds => $sec/2+1);
    if ($prev_sec) {
        $file->{DT}->add(seconds => $prev_sec/2+1);
    }
    $prev_sec=$sec;
    $prev_dt=$file->{DT}->clone();
}

# Check if all tags are present.
foreach my $song (@songs) {
    check_tag($song, 'Performer');
    check_tag($song, 'Album');
    check_tag($song, 'Recordeddate');
    check_tag($song, 'Trackname');
    check_tag($song, 'Genre');
}

# Stream needed DB files, inserting new tracks where appropriate.
foreach my $song (@songs) {
    # Read appropriate DB file into array.
    my @db_file;
    my $db_file_name=$db_dir.'/'.$song->{DT}->strftime('%Y.%m');
    open DBFILE, '<', $db_file_name and
        @db_file=<DBFILE> and
        close DBFILE;

    my $song_dt=$song->{DT};
    my $song_line=sprintf("%04d.%02d.%02d:::%02d:%02d:%02d:::".
                          "%s:::%s:::%s:::%s:::%s:::%s\n",
                          $song_dt->year, $song_dt->month, $song_dt->day,
                          $song_dt->hour, $song_dt->minute, $song_dt->second,
                          $song->{Performer}, $song->{Trackname},
                          $song->{Album}, $song->{Recordeddate},
                          $song->{Genre}, $song->{Timesec});

    # Insert song line into array
    my @new_db_file;
    my $inserted=0;
    if (@db_file) {
        foreach my $line (@db_file) {
            my ($year, $month, $day, $hour, $minute, $second)=
                $line=~/^(\d{4}).(\d{2}).(\d{2}):::(\d{2}):(\d{2}):(\d{2})/;
            my $db_song_dt=DateTime->new(year => $year, month => $month,
                                         day => $day, hour => $hour,
                                         minute => $minute, second => $second);
            if (!$inserted and DateTime->compare($db_song_dt, $song_dt) == 1) {
                push @new_db_file, $song_line;
                $inserted=1;
            }
            push @new_db_file, $line;
        }
        push(@new_db_file, $song_line) if !$inserted;
    } else {
        push @new_db_file, $song_line;
    }

    # Make some feedback on the screen.
    my $print_line=
        sprintf("%04d.%02d.%02d %02d:%02d:%02d Added %s - %s - %s\n",
        $song_dt->year, $song_dt->month, $song_dt->day,
                $song_dt->hour, $song_dt->minute, $song_dt->second,
                $song->{Performer}, $song->{Album},
                $song->{Trackname});
    print $print_line;

    # Save modified DB file.
    my $db_file_name_new=$db_file_name.'.new';
    open DBFILE, '>', "$db_file_name_new" or
        exitprog(1, qq(Can't create file "$db_file_name_new".\n).
                 "Try adding yourself to the mpd-hits group.");
    print DBFILE @new_db_file;
    close DBFILE;

    # Copy then unlink to preserve original file attrs.
    copy($db_file_name_new, $db_file_name);
    unlink($db_file_name_new);

    # chown and chmod the file if possible.
    my $uid=(stat $db_file_name)[4];
    my $gid=getgrnam($run_as_group);
    chown $uid, $gid, $db_file_name if defined $gid;
    chmod 0664, $db_file_name;
}




exit 0;

## Functions.

sub accept_if_song {
    my $file=shift @_;

    (my $suff)=($file=~/.*\.(.*)/);
    if ($suff and grep(/^$suff$/, @suffixes)) {
        push @songs, {Path => $file};
    }
}

sub check_tag {
    my $song=shift @_;
    my $prop=shift @_;

    if (!$song->{$prop}) {
        print qq(Property "$prop" is missing in the file ").
            basename($song->{Path}).qq("\nPlease enter it: );
        $song->{$prop}=<>;
        print "\n";
    }
}

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

    print "$message\n" if $message;
    exit $code;
}

sub usage {
    my $code=shift @_;

    my $p_spaces=' ' x length $progname;
    print "Usage: $progname [OPTION]...                     \n";
    print "                                                 \n";
    print "Options:                                         \n";
    print "  -s, --start=TIMESPEC                           \n";
    print "  -a, --add=MUSIC_PATH                           \n";
    print "                                                 \n";
    print "      --user=USERNAME                            \n";
    print "      --group=GROUPNAME                          \n";
    print "  -d, --db-directory=DBDIR                       \n";
    print "                                                 \n";
    print "  -h, --help                                     \n";
    print "  -V, --version                                  \n";
    print "                                                 \n";
    print "The --add option may be specified more than once.\n";
    print "                                                 \n";
    print "TIMESPEC is a date and time in a format ".
          "YYYY-MM-DDTHH:MM:SS\n(T in between is a ".
          "separator).\n\n";
    print "MUSIC_PATH is a path to some audio file or a ".
          "directory containing audio files.           \n";

    exitprog($code);
}

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