package XXV::MODULES::MUSIC;
use strict;

use Tools;
use MP3::Icecast;
use MP3::Info;
use CGI;
use Locale::gettext;
use File::Basename;
use LWP::Simple;
use File::Path;

$SIG{CHLD} = 'IGNORE';

# This module method must exist for XXV
# ------------------
sub module {
# ------------------
    my $obj = shift || return error ('No Object!' );
    my $args = {
        Name => 'MUSIC',
        Description => gettext('This module managed music files.'),
        Version => '0.45',
        Date => '04.09.2005',
        Author => 'xpix',
        Status => sub{ $obj->status(@_) },
        Preferences => {
            active => {
                description => gettext('Activate this service'),
                default     => 'y',
                type        => 'confirm',
                required    => gettext('This is required!'),
            },
            path => {
                description => gettext('Directory with the music files'),
                default     => '/music',
                type        => 'dir',
                required    => gettext('This is required!'),
            },
            host => {
                description => gettext('Used host of referred link inside playlist.'),
                default     => main::getModule('STATUS')->name,
                type        => 'string',
                required    => gettext('This is required!'),
            },
            port => {
                description => gettext('Port to listen for icecast clients.'),
                default     => 8100,
                type        => 'integer',
                required    => gettext('This is required!'),
            },
            proxy => {
                description => gettext('Proxy URL to music server. e.g. (http://vdr/xxv) Please remember you must write the Port to icecast server in your Proxy Konfiguration!'),
                default     => '',
                type        => 'string',
            },
            clients => {
                description => gettext('Maximum Clients to connect at the same time.'),
                default     => 5,
                type        => 'integer',
                required    => gettext('This is required!'),
            },
            coverimages => {
                description => gettext('common directory for cover images'),
                default     => '/var/cache/xxv/cover',
                type        => 'dir',
                required    => gettext('This is required!'),
            },
        },
        Commands => {
            mrefresh => {
                description => gettext('Rereading of the music directory.'),
                short       => 'mr',
                callback    => sub{ $obj->refresh(@_) },
                DenyClass   => 'mlist',
            },
            mcovers => {
                description => gettext('Download album covers.'),
                short       => 'mc',
                callback    => sub{ $obj->getcovers(@_) },
                DenyClass   => 'mlist',
            },
            mplay => {
                description => gettext("play music file 'fid'"),
                short       => 'mp',
                callback    => sub{ $obj->play(@_) },
                DenyClass   => 'mlist',
            },
            mplaylist => {
                description => gettext("get a m3u playlist for 'fid'"),
                short       => 'm3',
                callback    => sub{ $obj->playlist(@_) },
                DenyClass   => 'mlist',
            },
            mlist => {
                description => gettext("list music 'dir'"),
                short       => 'ml',
                callback    => sub{ $obj->list(@_) },
                DenyClass   => 'mlist',
            },
            msearch => {
                description => gettext("search music 'txt'"),
                short       => 'mf',
                callback    => sub{ $obj->search(@_) },
                DenyClass   => 'mlist',
            },
        },
    };
    return $args;
}

# ------------------
sub new {
# ------------------
	my($class, %attr) = @_;
	my $self = {};
	bless($self, $class);

    # paths
    $self->{paths} = delete $attr{'-paths'};

	# who am I
    $self->{MOD} = $self->module;

    # all configvalues to $self without parents (important for ConfigModule)
    map {
        $self->{$_} = $attr{'-config'}->{$self->{MOD}->{Name}}->{$_} || $self->{MOD}->{Preferences}->{$_}->{default}
    } keys %{$self->{MOD}->{Preferences}};

    # read the DB Handle
    $self->{dbh} = delete $attr{'-dbh'};

    # The Initprocess
    my $erg = $self->init or return error('Problem to initialze Module');

	return $self;
}

# ------------------
sub init {
# ------------------
    my $obj = shift || return error ('No Object!' );

    #create an instance to find all files below /usr/local/mp3
    $obj->{ICE} = MP3::Icecast->new();
    $obj->{ICE}->recursive(1);
    $obj->{ICE}->add_directory($obj->{path});

    $obj->{SOCK} = IO::Socket::INET->new(
        LocalPort => $obj->{port}, #standard Icecast port
        Listen    => $obj->{clients},
        Proto     => 'tcp',
        Reuse     => 1,
        Timeout   => 3600
    );

    my $channels;

    Event->io(
        fd => $obj->{SOCK},
        prio => -1,  # -1 very hard ... 6 very low
        cb => sub {
            # accept client
            my $client = $obj->{SOCK}->accept;
            warn "[Error] Cannot connect to new icecast client.\n" and return unless $client;
            $client->autoflush;

            # make "channel" number
            my $channel=++$channels;

            # install a communicator
            Event->io(
                fd => $client,
                prio => -1,  # -1 very hard ... 6 very low
                poll => 'r',
                cb => sub {
                    my $watcher = shift;
                    # report
                    warn "[Server] Talking on icecast channel $channel.\n";

                    # read new line and report it
                    my $handle=$watcher->w->fd;
                    my $data = $obj->parseRequest($handle);
                    my $files = $obj->handleInput($data);
                    unless(ref $files eq 'ARRAY') {
                        $watcher->w->cancel;
                        $client->close();
                        undef $watcher;
                        return 1;
                    }

                    $obj->stream($files, $client);

                    $watcher->w->cancel;
                    undef $watcher;
                    $client->close;
                },
            );

            # report
            warn "[Server] Opened new icecast channel $channel.\n";
        },
    ) if($obj->{active} eq 'y');


    # Look for table or create this table
    my $version = main::getVersion;
    $obj->{dbh}->do(qq|
      CREATE TABLE IF NOT EXISTS MUSIC (
          Id int(16) unsigned auto_increment NOT NULL,
          FILE text NOT NULL,
          ARTIST varchar(128) default 'unknown',
          ALBUM varchar(128) default 'unknown',
          TITLE varchar(128) default 'unknown',
          COMMENT varchar(128),
          TRACKNUM varchar(10) default '0',
          YEAR int(4) default 1970,
          GENRE varchar(128),
          BITRATE int(4),
          FREQUENCY varchar(4),
          SECS int (11) NOT NULL,
          PRIMARY KEY  (ID)
        ) COMMENT = '$version'
    |);

    $obj->{fields} = fields($obj->{dbh}, 'select * from MUSIC');

    # Read File to Database, if the DB empty
    $obj->refresh()
        unless($obj->{dbh}->selectall_arrayref("select count(*) from MUSIC")->[0][0]);
}

# ------------------
sub refresh {
# ------------------
    my $obj = shift || return error ('No Object!' );
    my $watcher = shift;
    my $console = shift;

    if( ref $console and not -d $obj->{path} ) {
        $console->err(sprintf(gettext("Directory of the music files '%s' not found"), $obj->{path}));
        $console->link({
            text => gettext("Back to music list"),
            url => "?cmd=mlist",
        }) if($console->typ eq 'HTML');
        return;
    }

    #create an instance to find all files below /usr/local/mp3
    $obj->{ICE} = MP3::Icecast->new();
    $obj->{ICE}->recursive(1);
    $obj->{ICE}->add_directory($obj->{path});

    $obj->{CACHE} = {};

    my $data = $obj->{dbh}->selectall_hashref("select ID, FILE from MUSIC", 'FILE');
    my @files = $obj->{ICE}->files;

    lg sprintf('Found %d music files !', scalar @files);


    if( ref $console and not scalar @files ) {
        $console->err(gettext("No music files found!"));
        $console->link({
            text => gettext("Back to music list"),
            url => "?cmd=mlist",
        }) if($console->typ eq 'HTML');
        return;
    }

    my $waiter;
    if(ref $console) {
        $waiter = $console->wait(gettext("Get information from music files ..."));
        $waiter->min(0);
        $waiter->max(scalar @files);
        $waiter->screen('yes'); # Muss angegeben werden!
    }

    my $c = 0;
    my $new = 0;
    foreach my $file (@files) {
        $waiter->next(++$c)
            if(ref $console);
        next if(delete $data->{$file});
        my $info = MP3::Info->new($file);
        $new++
            if($obj->insert($info));
    }

    # last call of waiter
    $waiter->next(scalar @files,1)
      if(ref $console && ref $waiter);

    # Packen des contents am Ende verhindern
    $console->{nopack} = 1 if(ref $console);

    foreach my $f (sort keys %$data) {
        unless(-e $f) {
            $obj->{dbh}->do(sprintf('DELETE FROM MUSIC WHERE ID = %lu', $data->{$f}->{ID}));
        }
    }

    if(ref $console) {
        $console->start();
        my $msg = sprintf(gettext("%d new music files in database saved and %d non exists entries deleted!"), $new, scalar keys %$data);
        $console->message($msg);
        lg $msg;
        $console->link({
            text => gettext("Back to music list"),
            url => "?cmd=mlist",
        }) if($console->typ eq 'HTML');
        $console->footer();
    }
}

# ------------------
sub play {
# ------------------
    my $obj = shift || return error ('No Object!' );
    my $watcher = shift || return error ('No Watcher!');
    my $console = shift || return error ('No Console');
    my $data = shift || return error ('No data');

    lg sprintf('Call play from User %s',
        $console->{USER}->{Name}
        );

    $console->player("?cmd=mplaylist&data=${data}&binary=1");
}

# ------------------
sub playlist {
# ------------------
    my $obj = shift || return error ('No Object!' );
    my $watcher = shift || return error ('No Watcher!');
    my $console = shift || return error ('No Console');
    my $data = shift || return error ('No data');

    my $host = $obj->{host} || main::getModule('STATUS')->IP;
    my $output = "#EXTM3U\r\n";

    foreach my $id (split('_', $data)) {
        my $data = $obj->{dbh}->selectrow_hashref("select * from MUSIC where ID = '$id'");
        my $file;
        if($obj->{proxy}) {
            $file = sprintf('%s/?cmd=play&data=%s&field=ID', $obj->{proxy}, $id);
        } else {
            $file = sprintf('http://%s:%lu/?cmd=play&data=%s&field=ID', $host, $obj->{port}, $id);
        }
        $output .= sprintf("#EXTINF:%d,%s - %s (%s)\r\n",$data->{SECS},$data->{TITLE},$data->{ARTIST},$data->{ALBUM});
        $output .= sprintf("%s\r\n", $file);
    }
    if($console->typ eq 'HTML') {
        $console->out($output, "audio/x-mpegurl");
    }
}

# ------------------
sub search {
# ------------------
    my $obj = shift || return error ('No Object!' );
    my $watcher = shift || return error ('No Watcher!');
    my $console = shift || return error ('No Console');
    my $text   = shift || return $console->message(gettext("No Text to search! Please use msearch 'text'"));

    return $obj->list($watcher,$console,"search:".$text);
}

# ------------------
sub list {
# ------------------
    my $obj = shift || return error ('No Object!' );
    my $watcher = shift || return error ('No Watcher!');
    my $console = shift || return error ('No Console');
    my $search  = shift || 'album:'.$obj->{dbh}->selectall_arrayref("select ALBUM from MUSIC limit 1")->[0][0];

    my @field = split(':',$search);
    my $typ = $field[0];
    shift @field;
    my $text = join(':',@field);

    my $where;
    if($typ eq 'search') {
        $where = buildsearch("ALBUM,ARTIST,TITLE,COMMENT",$text);
    } else {
        $text =~ s/\'/\\'/sg;#'
        $where = sprintf("%s = '%s'", uc($typ), $text);
    }

    my $sql = qq|
SELECT
	ID,
	ARTIST,
	ALBUM,
	TITLE,
	TRACKNUM,
	YEAR,
	DATE_FORMAT(FROM_UNIXTIME(SECS), '%i:%s') as Time,
	GENRE as __GENRE,
	COMMENT as __COMMENT
FROM
	MUSIC
WHERE
	( $where )
ORDER BY
	FILE
|;
    my $fields = fields($obj->{dbh}, $sql);

    my $erg = $obj->{dbh}->selectall_arrayref($sql);
    unshift(@$erg, $fields);

    my $params = {
        albums => $obj->GroupArray('ALBUM'),
        artists => $obj->GroupArray('ARTIST'),
        genres => $obj->GroupArray('GENRE'),
        getCover => sub{ return $obj->getCoverFile(@_) },
    };

    $console->table($erg, $params);
}

# ------------------
sub handleInput {
# ------------------
    my $obj     = shift || return error ('No Object!' );
    my $data    = shift || return error ('No Request!' );
    my $cgi = CGI->new( $data->{Query} );

    my $ucmd = $cgi->param('cmd')   || 'play';
    my $ufield = $cgi->param('field') || 'ID';
    my $udata = $cgi->param('data') || '*';

    my $files;
    if($ucmd eq 'play' and $ufield and my @search = split(',',$udata)) {
        $files = $obj->field2path($ufield, \@search);
    } else {
        return error "I don't understand this command '$ucmd'";
    }
    return $files;
}

# ------------------
sub field2path {
# ------------------
    my $obj = shift || return error ('No Object!' );
    my $field = shift || return error ('No Field!' );
    my $data = shift || return error ('No ids!' );

    map {$_ = $obj->{dbh}->quote($_)} @$data;
    my $sql = sprintf "select FILE, %s from MUSIC", $field;
    $sql .= sprintf " where %s in (%s)", $field, join(',', @$data)
        if($data->[0] ne '*');
    my $ret = $obj->{dbh}->selectall_hashref($sql, 'FILE');
    my @files = sort keys %$ret;
    return \@files;
}

# ------------------
sub groupfields {
# ------------------
    my $obj = shift || return error ('No Object!' );
    my $field = shift || return error ('No Field!' );
    return $obj->{CACHE}->{GROUPS}->{$field}
        if(exists $obj->{CACHE}->{GROUPS}->{$field});

    my $sql = sprintf "select %s from MUSIC group by %s", $field, $field;
    my $ret = $obj->{dbh}->selectall_hashref($sql, $field);
    my @names = sort keys %$ret;
    @{$obj->{CACHE}->{GROUPS}->{$field}} = @names;

    return \@names;
}


# ------------------
sub insert {
# ------------------
    my $obj = shift || return error ('No Object!' );
    my $data = shift || return 0;

    my @setdata;
    foreach my $name (keys %$data) {
        next unless(grep($name eq $_, @{$obj->{fields}}));
        push(@setdata, sprintf("%s=%s", $name, $obj->{dbh}->quote($data->{$name})));
    }

    # MD5(File) as ID
    my $sql = sprintf('INSERT INTO MUSIC SET %s', join(', ', @setdata));
# dumper($sql);
    $obj->{dbh}->do( $sql );
    return 1;
}

# ------------------
sub stream {
# ------------------
    my $obj = shift || return error ('No Object!' );
    my $files = shift || return error ('No Files!' );
    my $client = shift || return error ('No Client!' );


    defined(my $child = fork()) or die "Can't fork: $!";
    if($child == 0) {
        $obj->{SOCK}->close;
        $obj->{dbh}->{InactiveDestroy} = 1;

        while(@$files) {
            my $file = shift @$files;
            lg sprintf('Stream file "%s" to Ip: %s',
                $file,
                getip($client)
                );
            my $erg = $obj->{ICE}->stream($file,0,$client)
                || last;
        }
        exit 0;
    }
}

# ------------------
sub parseRequest {
# ------------------
    my $obj = shift || return error ('No Object!' );
    my $hdl = shift || return error ('No Request!' );

    my ($Req, $size) = getFromSocket($hdl);

	if(ref $Req eq 'ARRAY' and $Req->[0] =~ /^GET (\/[\w\.\/-\:]*)([\?[\w=&\.\+\%-\:\!]*]*)[\#\d ]+HTTP\/1.\d$/) {
        my $data = {};
		($data->{Request}, $data->{Query}) = ($1, $2 ? substr($2, 1, length($2)) : undef);

    	# parse header
    	foreach my $line (@$Req) {
    		if($line =~ /Referer: (.*)/) {
    			$data->{Referer} = $1;
    		}
    		if($line =~ /Host: (.*)/) {
    			$data->{HOST} = $1;
    		}
    		if($line =~ /Authorization: basic (.*)/i) {
    			($data->{username}, $data->{password}) = split(":", MIME::Base64::decode_base64($1), 2);
    		}
    		if($line =~ /User-Agent: (.*)/i) {
    			$data->{http_useragent} = $1;
    		}
    	}
        return $data;
	} else {
        warn "Unknown Request: <%s>\n", join("\n", @$Req);
		return;
	}
}

# ------------------
sub GroupArray {
# ------------------
    my $obj = shift || return error ('No Object!' );
    my $field = shift || return undef;

    my $sql = sprintf('select %s, ID from MUSIC group by %s order by %s', $field, $field, $field);
    my $erg = $obj->{dbh}->selectall_arrayref($sql);
    return $erg;
}

# ------------------
sub status {
# ------------------
    my $obj = shift || return error ('No Object!' );
    my $watcher = shift;
    my $console = shift;
    my $lastReportTime = shift || 0;

    my $report = {};
    foreach my $field (qw/FILE ALBUM ARTIST GENRE/) {
        my $data = $obj->GroupArray($field);
        $report->{$field} = scalar @$data;
    }

    return {
        message => sprintf(gettext('Music database contains %d entries with %d albums from %d artists in %d genres'),
            $report->{FILE}, $report->{ALBUM},$report->{ARTIST}, $report->{GENRE}),
    };
}

# ------------------
sub getcovers {
# ------------------
    my $obj = shift || return error ('No Object!' );
    my $watcher = shift;
    my $console = shift;
    my $force = shift;

    lg sprintf('Call getcovers from User: %s',
            $console->{USER}->{Name}
        );

    my $waiter = $console->wait(gettext("Please wait, search for new covers ..."))
        if(ref $console);

    unless(-d $obj->{coverimages}) {
        mkpath($obj->{coverimages}) or warn "Can't mkpath $obj->{coverimages} : $!";
        lg sprintf('mkdir path "%s" from User: %s',
                $obj->{coverimages},
                $console->{USER}->{Name}
            );
    }

    my $rob = main::getModule('ROBOT')
        or return error('No ROBOT Module installed!');

    $rob->saveRobot('coverimage', sub{
        my $agent = shift || return;
        my $formfiller = shift || return;
        my $album = shift || return;
        my $year = shift || 0;
        my $target = shift || return;
        my $current = shift || 0;

        # Anzeige der ProcessBar
        $waiter->next($current) if(ref $waiter);

        $agent->get('http://search.allofmp3.com/search.shtml');
        $agent->form(1) if $agent->forms and scalar @{$agent->forms};
        $agent->form_number(2);
        $formfiller->fill_form($agent->current_form);
        $formfiller->add_filler( 'search' => Fixed => $album );
        $formfiller->add_filler( 'range' => Fixed => 'album' );
        $formfiller->add_filler( 'ybeg' => Fixed => $year ) if($year);
        $formfiller->add_filler( 'yend' => Fixed => $year ) if($year);
        $formfiller->fill_form($agent->current_form);
        $agent->submit();

        if($agent->find_link( text_regex => qr/$album/i )) {
            $agent->follow_link( text_regex => qr/$album/i );
        } else {
            return 0, "Nothing found: $album";
        }

        $album =~ s/\s/_/sig;
        my $img = $agent->find_image(url_regex => qr/$album/i)
            || return 0, "Image not found for $album";
        getstore($img->url, $target);
        return 1;
    });

    my $sql = 'select * from MUSIC group by ALBUM';
    my $erg = $obj->{dbh}->selectall_hashref($sql, 'FILE');

    my $current = 0;
    foreach my $file (sort keys %$erg) {
        my $e = $erg->{$file};

        my $album = $e->{ALBUM};
        $album =~ s/\s/_/sig;
        my $target = sprintf('%s/%s.jpg', $obj->{coverimages}, $album);
        next if(-e $target and not $force);

        $rob->register('coverimage', $e->{ALBUM}, $e->{YEAR}, $target, ++$current);
    }

    if(ref $waiter) {
        $waiter->min(0);
        $waiter->max($current || 1);
        $waiter->screen('yes');
    }

    if(ref $waiter and $current) {
        $waiter->endcallback(
            sub{
                if(ref $console) {
                    $console->start();
                    $console->message(my $msg = gettext("New covers search was successfully!"));
                    lg sprintf($msg);

                    $console->link({
                        text => gettext("Back to music list"),
                        url => "?cmd=mlist",
                    }) if($console->typ eq 'HTML');
                    $console->footer();
                }
            }
        );
    }

    if(ref $waiter and not $current) {
        $waiter->endcallback(
            sub{
                if(ref $console) {
                    $console->start();
                    $console->message(gettext("It is not necessary to look for new covers because already all albums possess cover!"));

                    $console->link({
                        text => gettext("Back to music list"),
                        url => "?cmd=mlist",
                    }) if($console->typ eq 'HTML');
                    $console->footer();
                }
            }
        );
        lg sprintf('All covers exists!');
    }

    # Start Robots
    $rob->start( 'coverimage', $watcher, $console, sub{ $waiter->end if(ref $waiter and $current); } );

    return $erg;
}

# ------------------
sub getCoverFile {
# ------------------
    my $obj = shift  || return error ('No Object!' );
    my $album = shift || return error ('No Album!' );

    $album =~ s/\s/_/sg;

    # look for pictures
    if(my @covers = glob("$obj->{coverimages}/$album.[jJpPgG][pPnNiI][gGfF]")) {
        # Limit to first image
        splice(@covers,1,scalar(@covers));
        map {
            $_ =~ s/^$obj->{coverimages}\//coverimages\//
        } @covers;
        return \@covers;
    } else {
        return undef;
    }
}


1;
