#!/pro/bin/perl

use v5.12;
use warnings;

our $VERSION = 0.011;	# See bottom for versions

sub usage
{
    my $err = shift and select STDERR;
    say "usage: $0 [--cd | file.m3u | file.mp3 | http://radio.com:7170 ]";
    exit $err;
    } # usage

use Getopt::Long qw(:config bundling nopermute passthrough);
my $cdplay  = 0;
my $opt_l   = 1;
my $mode    = "list";	# URL, list, Local, CD
GetOptions (
    "help|?"	=> sub { usage (0); },

    "c|cd"	=> sub { $mode = "cd"; },
    "l|log!"	=> \$opt_l,
    ) or usage (1);

{   my $pid = fork;
    if ($pid < 0) {
        print STDERR "Unable to run in the background, cannot fork: $!\n";
        exit $?;
        }
    $pid and exit 0;
    } # to_background

my @pref = map { "$ENV{HOME}/radio/$_.m3u" } qw( 05-prog-fm 00-sa );
my $pref = 0;

use Tk;
use Tk::Scale;
use Tk::ROText;
use Tk::BrowseEntry;
use Tk::Pixmap;
use Tk::FBox;

use Cwd qw(abs_path);
use Sys::Hostname;
use List::Util qw(max);
use LWP::Simple;
use HTML::TreeBuilder;
use Glib qw(TRUE FALSE);
use GStreamer;
use Linux::CDROM;
use Data::Peek;

my $mw = MainWindow->new;

my $w_info = 40;
if (hostname eq "pc09") {
    #$mw->update;
    $mw->geometry ("+3100+1065");
    #$mw->geometry ("500x134");
    $w_info = 60;
    #$mw->update;
    }

my %pack = (
    efl => [ qw( -expand 1 -fill both -side left  )],
    efr => [ qw( -expand 1 -fill both -side right )],
    eft => [ qw( -expand 1 -fill both -side top   )],
    e_l => [ qw( -expand 1 -fill none -side left  )],
    e_r => [ qw( -expand 1 -fill none -side right )],
    e_t => [ qw( -expand 1 -fill none -side top   )],
    _fl => [ qw( -expand 0 -fill both -side left  )],
    _fr => [ qw( -expand 0 -fill both -side right )],
    _ft => [ qw( -expand 0 -fill both -side top   )],
    __l => [ qw( -expand 0 -fill none -side left  )],
    __r => [ qw( -expand 0 -fill none -side right )],
    __t => [ qw( -expand 0 -fill none -side top   )],
    );
my %color = (
    bg		=> "Black",
    mode	=> "Green",
    info	=> "Yellow",
    button	=> "#007f00",
    xpm		=> "#007fef",
    volume	=> "#7f0000",
    vol_bg	=> "#001020",
    list	=> "#a0a0a0",
    play	=> "White",
    edge	=> "#600000",
    );
my %font = (
    mode	=> "{DejaVu Sans Mono} 12",
    info	=> "{DejaVu Sans} 8",
    list	=> "{DejaVu Sans} 6",
    button	=> "{DejaVu Sans} 7",
    );

# +-----------+---------------------------------------+
# | > 0:05:12 | Pink Floyd - The Wall                 | play mode    currently playing
# |           | http://radio.com:7170                 |  play time   URL, file, CD
# |           | 128kbps 44 kHz                        |  play time   audio mode
# +-----------+---------------------------------------+
# | [|<] [<<] [>] [>>] [>|] [^] [========------------]| prv rew play/pause/stop ff nxt eject volume
# +---------------------------------------------------+
# | 1. http://89.238.146.140:7170                    #| play list
# | 2. http://89.238.146.140:7200                    #|
# | 3.                                               #|
# | 4.                                               ||
# | 5.                                               ||
# +---------------------------------------------------+

my @fo = (
    -highlightcolor      => $color{bg},
    -highlightbackground => $color{bg},
    -highlightthickness  => 0,
    );
my $fi = $mw->Frame (@fo)->pack (@{$pack{eft}});	# Info
my $fb = $mw->Frame (@fo)->pack (@{$pack{eft}});	# Buttons
my $fl = $mw->Frame (@fo)->pack (@{$pack{eft}});	# List

my %xpm;
{   my $xpm;
    my $dta;
    sub xpm2xpm
    {
	$xpm && $dta or return;
	$xpm{$xpm} = $mw->Pixmap ($xpm, -data => join "\n",
	    qq(/* XPM */),
	    qq(static char *${xpm}[] = {),
	    qq("9 5 3 1",),	# col row colors chars/pixel
	    qq("  c none",),
	    qq("x c $color{xpm}",),
	    qq(". c #ff4500",),
	    (join ",\n" => map { sprintf qq{"%-9s"}, $_ } @$dta),
	    qq(};));
	$xpm = $dta = undef;
	} # xpm

    while (<DATA>) {
	m/^#/ and next;
	chomp;
	m/^\[/ and xpm2xpm;
	if (m/^\[image:(\S+)\]/) {
	    $xpm = $1;
	    $dta = [];
	    next;
	    }
	$xpm and push @$dta, $_;
	}
    xpm2xpm;
    }

my $width     = 800;
my $volume    = 20;
my $volume_x  = 2;
my $_playmode = "";
my $play_mode = "stop";		# stop / play / pause
my $play_time = "0:00:00";
my $lpay_time;
my $play_list = "";
my $pl_title  = "";
my $p_started;
my $trck_time = "";
my $t_started;

my @play_list;
my $play_idx;
my $playing   = "playing ...";
my $play_info = "please select";
my @audio_inf = ("-", "-");
my $audio_inf = "";
my $cdrom;

my $g_loop;
my $g_play;
my $g_volume;

sub dis_time
{
    use integer;
    my $secs = shift or return "0:00:00";
    my $mins = $secs / 60;
    if (my $hour = $mins / 60) {
	if ($hour >= 24) {
	    (my $ft = $font{mode}) =~ s/(\d+)/$1 - 2/e;
	    $lpay_time->configure (-font => $ft);
	    return sprintf "%d'%2d:%02d:%02d",
		$hour / 24, $hour % 24, $mins % 60, $secs % 60;
	    }
	(my $ft = $font{mode}) =~ s/(\d+)/$1 - 1/e;
	$lpay_time->configure (-font => $ft);
	return sprintf "%d:%02d:%02d", $hour, $mins % 60, $secs % 60;
	}
    return sprintf "%2d:%02d", $mins, $secs % 60;
    } # dis_time

sub update_times
{
    $p_started and $play_time = dis_time (time - $p_started);
    $t_started and $trck_time = dis_time (time - $t_started);
    $audio_inf = join "  " => @audio_inf;
    $mw->after (500, \&update_times);
    } # update_times

sub g_idle_callback
{
    $mw->update;
    #$mw->idletasks;
    return TRUE;
    } # g_idle_callback

sub g_play_callback
{
    state $last = "";
    state $prvt = "";
    state $pcnt = 0;

    my ($bus, $message, $loop) = @_;

    my $mt = $message->type;
    # Stringified:
    # [ unknown state-changed ]
    # [ unknown stream-status ]
    # [ unknown tag ]
    # [ unknown buffering ]
    # [ unknown duration ]
    # [ unknown async-done ]
    # [ unknown new-clock ]

    # print STDERR "$pcnt, $prvt, $mt\n";
    if ("$mt" eq $prvt) {
	$pcnt++;
	}
    else {
	($prvt, $pcnt) = ("$mt", 0);
	}

    if ($mt & "error") {
	($play_info = $message->error) =~ s/[\s\r\n]+/ /;
	$play_info =~ s/^\s*(.*?)\s*$/$1/;
	$play_info =~ s/ at $0 line [0-9]+//;
	#print STDERR "ERR: '$play_info'\n";
	$loop->quit ();
	stop ();
	return TRUE;
	}

    if ($mt & "eos") {
	$play_info = "Stopped";
	$loop->quit ();
	stop ();
	nxt ();
	play ();
	return TRUE;
	}

    # "Normal" is +86 buffering events
    if ($pcnt > 250 and $mt & "buffering") {
	$play_info = "Stopped";
	$loop->quit ();
	stop ();
	play ();
	return TRUE;
	}

    if ($mt & "tag") {
	my $tags = $message->tag_list;
	my %info;
	foreach (qw(artist title album track-number)) {
	    exists $tags->{$_} or next;
	    my $val = $tags->{$_}->[0] // "";
	    $val =~ s/[\r\n]+/ /;
	    $val =~ s/\s+/ /g;
	    $val =~ s/\s+$//;
	    $val =~ s/^\s+//;
	    $info{ucfirst GStreamer::Tag::get_nick ($_)} = $val;
	    }
	$playing = join "-" => grep { $_ } map { $info{$_} } qw( Artist Title );
	$play_info = ($info{Album} // $pl_title // $play_list[$play_idx]) || "-";
	$mw->title ("\x{266b} $playing");
	$t_started = time;
	if ($opt_l && $playing ne $last && -w "radio.log") {
	    open my $fh, ">>:utf8", "radio.log";
	    my @now = localtime;
	    printf $fh "%4d-%02d-%02d %02d:%02d:%02d %s%s\n",
		$now[5] + 1900, ++$now[4], @now[3, 2, 1, 0],
		$playing, $play_info ? " ($play_info)" : "";
	    close $fh;
	    }
	$last = $playing;
	return TRUE;
	}

    # remove message from the queue
    return TRUE;
    } # g_play_callback

my $w_pl = $fl->Scrolled ("ROText",
    -scrollbars             => "",	# No visible scrollbars, use mouse wheel
	-height             => 5,
	-width              => 35,
	-foreground         => $color{list},
	-background         => $color{bg},
	-highlightthickness => 0,
	-font               => $font{list},
	-setgrid            => 1,
	-relief             => "flat",
	)->pack (@{$pack{efl}})->Subwidget ("scrolled");
$w_pl->tag ("configure", "selected", -foreground => $color{play});
$w_pl->tag ("bind", "item", "<Double-1>" => sub {
    my $e = $w_pl->XEvent;
    my ($x, $y) = ($e->x, $e->y);
    my $line = $w_pl->index ("\@$x,$y linestart");
    --$line == $play_idx and return;
    set_index ($line) and play ();
    });

sub play_next
{
    } # play_next

my $f_run = $fi->Frame ()->pack (@{$pack{efl}});

$lpay_time = $f_run->Label (
    -textvariable	=> \$play_time,
    -width		=> 8,
    -foreground		=> $color{mode},
    -background		=> $color{bg},
    -font		=> $font{mode},
    -anchor		=> "ne",
    -relief		=> "flat",
    )->pack (@{$pack{eft}});
my $b_mode;
my $f_mode = $f_run->Frame ()->pack (@{$pack{eft}});
$f_mode->Label (
    -width		=> 2,
    -textvariable	=> \$play_idx,
    -foreground		=> $color{xpm},
    -background		=> $color{bg},
    -font		=> $font{button},
    -anchor		=> "e",
    -relief		=> "flat",
    )->pack (@{$pack{_fl}});
my $l_mode = $f_mode->Label (
    -image		=> $xpm{stop},
    -background		=> $color{bg},
    -anchor		=> "w",
    -relief		=> "flat",
    -padx		=> 5,
    )->pack (@{$pack{efl}});
$l_mode->bind ("<3>" => sub { stop (); exec $0 });
$f_mode->Label (
    -textvariable	=> \$trck_time,
    -foreground		=> $color{xpm},
    -background		=> $color{bg},
    -font		=> $font{button},
    -anchor		=> "e",
    -relief		=> "flat",
    )->pack (@{$pack{efr}});

my $f_inf = $fi->Frame ()->pack (@{$pack{efl}});

for (\$playing, \$play_info, \$audio_inf) {
    $f_inf->Label (
	-textvariable	=> \$_,
	-width		=> $w_info,
	-foreground	=> $color{info},
	-background	=> $color{bg},
	-font		=> $font{info},
	-anchor		=> "nw",
	-relief		=> "flat",
	)->pack (@{$pack{eft}});
    }

sub set_index
{
    my $idx = shift;
    $idx >= 0 && $idx < @play_list or return;
    my $pos = $idx + 1;
    $w_pl->tag ("remove", "selected", "1.0", "end");
    $w_pl->tag ("add", "selected", "$pos.0 linestart", "$pos.0 lineend");

    defined $play_idx && $play_idx == $idx and return 0;

    $play_idx = $idx;
    if ($play_mode eq "play") {
	stop ();
	play ();
	}
    return $pos;
    } # set_index

sub set_play_mode
{
    $play_mode = shift;
    $l_mode and $l_mode->configure (-image => $xpm{$play_mode});
    } # set_play_mode

sub stop
{
    set_play_mode "stop";
    $b_mode and $b_mode->configure (-image => $xpm{play});
    if ($g_play) {
	$g_play->set_state ("null");
	undef $g_play;
	}
    if ($g_loop) {
	$g_loop->quit ();
	undef $g_loop;
	}
    undef $p_started;
    undef $t_started;
    $play_time = dis_time (0);
    $trck_time = "";
    } # stop

sub play
{
    @play_list or return;
    my $uri = $play_list[$play_idx];
    if (-f $uri) {
	$uri = Glib::filename_to_uri abs_path ($uri), "localhost";
	}
    else {
	if ($uri =~ m/^(http:.*?):[0-9]+$/) {
	    # Check if the host returns data
	    my $content = get ($1) or return;
	    }
	}
    set_play_mode "play";
    $b_mode->configure (-image => $xpm{pause});
    $g_loop = Glib::MainLoop->new (undef, FALSE) or die "Cannot loop: $!\n";;
    $g_play = GStreamer::ElementFactory->make (playbin => "player") or
	die "Cannot create player: $!\n";
    $p_started = time;
    $g_play->set (uri => $uri);
    $g_play->get_bus ()->add_watch (\&g_play_callback, $g_loop);
    $g_play->set_state ("playing") or return stop;
    audio_volume ();
    $g_loop->run ();
    } # play

sub pause
{
    $play_mode eq "play" or return;
    set_play_mode "pause";
    $b_mode->configure (-image => $xpm{play});
    } # pause

sub pp
{
    return $play_mode eq "play" ? pause : play;
    } # pp

sub rew
{
    $play_mode eq "play" or return;
    } # rew

sub ff
{
    $play_mode eq "play" or return;
    } # ff

sub prv
{
    set_index ($play_idx - 1);
    } # prv

sub nxt
{
    set_index ($play_idx + 1);
    } # nxt

sub eject
{
    stop ();
    Linux::CDROM->new ("/dev/cdrom")->eject;
    } # eject

sub read_pl
{
    my $plist = shift or return;
    open my $fh, "<", $plist or return;
    my @pl;
    my @pt;
    my $pt;
    while (<$fh>) {
	chomp;
	s/\s+$//;
	s/^\s+//;
	if (s/^#\s*Title\s*:\s*//) {
	    $pt = $_;
	    next;
	    }
	if (s/^(?:Title[0-9]+=|#EXTINF:-?[0-9]+,)\s*//i) {
	    push @pt, $_;
	    next;
	    }
	m/^ ( [#] | Length[0-9]+ | \[ | numberofentries | version | \s*$ )/ix and next;
	s/^File[0-9]+=//;	# pls
	push @pl, $_;
	}
    @pl or return;

    # DDumper { pl => \@pl, pt => \@pt };
    $w_pl->delete ("1.0", "end");
    stop ();
    @play_list = @pl;
    my @lt = @pl == @pt ? @pt : ("") x scalar @pl;
    my $tl = max map { length } @lt;
    $tl and $tl += 1;
    for (0 .. $#play_list) {
	$w_pl->insert ("end", (sprintf "%02d. %-${tl}s%s\n", $_, $lt[$_], $play_list[$_]), [ "item", "tag$_" ]);
	}
    $plist =~ s{.*/}{};
    $play_list = $plist;
    $plist =~ s/\.(m3u|pls)$//i;
    $pl_title = join " - " => grep m/\S/ => map { $_ // "" } $plist, $pt // $pt[0] // "";
    set_index (0);
    $_playmode eq "play" and $mw->after (100, \&play);
    } # read_pl

# http://www.gstreamer.net/data/doc/gstreamer/head/gst-plugins-base-plugins/html/gst-plugins-base-plugins-volume.html
sub audio_volume ()
{
    $g_play and $g_play->set (volume => $volume / 100);
    } # audio_volume

sub volume_dec ()
{
    $volume -= $volume_x;
    $volume <   0 and $volume =   0;
    audio_volume ();
    } # volume_dec

sub volume_inc ()
{
    $volume += $volume_x;
    $volume > 100 and $volume = 100;
    audio_volume ();
    } # volume_dec

sub mute
{
    $g_play and
	$g_play->set (volume => $g_play->get ("volume") ? 0 : $volume / 100);
    } # mute

my @bo = (
    -foreground          => $color{button},
    -background          => $color{bg},
    -highlightbackground => $color{edge},
    -highlightcolor      => $color{edge},
    -highlightthickness  => 1,
    -anchor              => "c",
    -height              => 6,
    -relief              => "flat",
    );
$fb->Button (-image => $xpm{prv},   -command => \&prv,   @bo)->pack (@{$pack{efl}});
$fb->Button (-image => $xpm{rew},   -command => \&rew,   @bo)->pack (@{$pack{efl}});
$b_mode =
$fb->Button (-image => $xpm{play},  -command => \&pp,    @bo)->pack (@{$pack{efl}});
$fb->Button (-image => $xpm{stop},  -command => \&stop,  @bo)->pack (@{$pack{efl}});
$fb->Button (-image => $xpm{ff},    -command => \&ff ,   @bo)->pack (@{$pack{efl}});
$fb->Button (-image => $xpm{nxt},   -command => \&nxt,   @bo)->pack (@{$pack{efl}});
$fb->Button (-image => $xpm{eject}, -command => \&eject, @bo)->pack (@{$pack{efl}});

$fb->Scale (
    -orient              => "h",
    -borderwidth         =>   0,
    -length              => 100,
    -width               =>  12,
    -variable            => \$volume,
    -sliderlength        =>  10,
    -highlightthickness  => 1,
    -highlightbackground => $color{edge},
    -activebackground    => $color{edge},
    -background          => $color{bg},
    -foreground          => $color{button},
    -troughcolor         => $color{vol_bg},
    -showvalue           =>   0,
    -from                =>   0,
    -to                  => 100,
    -command             => \&audio_volume,
    )->pack (@{$pack{efl}});
update_times ();
$fb->Button (-image => $xpm{mute},  -command => \&mute,  @bo)->pack (@{$pack{efl}});

sub open_args
{
    for (@_) {
	$_ or next;
	if (m{^ cd               $}xi) {	# Play CD
	    $mode = "cd";
	    next;
	    }
	if (m{\.(?: pls | m3u ) $}xi) {		# Playlist
	    read_pl ($_);
	    $mode = "url";
	    next;
	    }
	if ( m{^ https?:// }xi ||		# URL
	     m{ \.(?: wav | mp3 )$}xi) {	# audio-file
	    @play_list = ($_);
	    $mode = "url";
	    # Mode "mp3" will play blocking the alarm clock
	    # xmms and friends deal with this ok
	    # Take care with UTF8 file names. Something fails somewhere
	    next;
	    }
	}
    } # open_args

# Open dialog for other music source
$mw->bind ("<Key-$_>", sub {
    my $types = [
	[ "Play lists",  [qw( .m3u .pls						)]],
	[ "Audio files", [qw( .wav .mp3 .ogg .mid .aiff .wma .wax .wvx .raw	)]],
	[ "All Files",  "*"							  ],
	];
    $_playmode = $play_mode;
    if (my $file = $mw->FBox (-filetypes => $types)->Show) {
	stop;
	open_args ($file);
	}
    else {
	$_playmode eq "play" and $mw->after (100, \&play);
	}
    }) for "l", "o";
# Switch to alternate station
$mw->bind ("<Key-a>", sub {
    $_playmode = $play_mode;
    stop;
    $pref ^= 1;
    read_pl ($pref[$pref]);
    });
# Volume control
$mw->bind ("<Down>", \&volume_dec);
$mw->bind ("<Up>",   \&volume_inc);

@ARGV and open_args (@ARGV);

GStreamer->init ();
#Glib::Idle->add (\&g_idle_callback, undef, 0);
Glib::Timeout->add (124, \&g_idle_callback, undef, 0);

if (@play_list) {
    play;
    }
else {
    if (open my $fh, "<", "$ENV{HOME}\/radio/radio.pref") {
	chomp (@pref = <$fh>);
	close $fh;
	}
    read_pl ($pref[$pref]);
    }

#$mw->protocol ("WM_DELETE_WINDOW" => sub {
#    stop;
#    exit;
#    });

MainLoop;

__END__
[image:prv]
xx     xx
xx   xxxx
xx xxxxxx
xx   xxxx
xx     xx
[image:rew]
   x   x
  xx  xx
 xxx xxx
  xx  xx
   x   x
[image:pause]
 xxx xxx
 xxx xxx
 xxx xxx
 xxx xxx
 xxx xxx
[image:stop]
 xxxxxxx
 xxxxxxx
 xxxxxxx
 xxxxxxx
 xxxxxxx
[image:play]
xxx
xxxxxx
xxxxxxxxx
xxxxxx
xxx
[image:ff]
 x   x
 xx  xx
 xxx xxx
 xx  xx
 x   x
[image:nxt]
xx     xx
xxxx   xx
xxxxxx xx
xxxx   xx
xx     xx
[image:eject]
    x
  xxxxx
xxxxxxxxx

xxxxxxxxx
[image:mute]
  .  x.xx
xxx.x.xxx
xxxx.xxxx
xxx.x.xxx
  .  x.xx
[end]
# 0.001 Initial
# 0.002 It all works - including opening of playlists
# 0.003 Show playlist used, file type selector
# 0.004 if switching play-list during play, start playing new playlist automatically
# 0.005 Key 'a' quickly switches to alternate station
# 0.006 GStreamer playing a *file* needs absolute path
# 0.007
# 0.008
# 0.009 Add logging to radio.log
# 0.010 Restart when overbuffering, Right-click on play-mode = restart
# 0.011 Compress whitespace in tag values reported in gstreamer
