X-Git-Url: https://git.distorted.org.uk/~mdw/profile/blobdiff_plain/30068e6d43270e37b0f5c3620cb1ec43d54d1d61..70291d8de55c7625327e1bac438ab588ed26e158:/bin/disorder-notify?ds=sidebyside diff --git a/bin/disorder-notify b/bin/disorder-notify index 757e12f..d26a565 100755 --- a/bin/disorder-notify +++ b/bin/disorder-notify @@ -1,92 +1,319 @@ -#! /usr/bin/perl +#! /usr/bin/perl -w -sub notify ($$) { - my ($head, $body) = @_; +use autodie qw{:all}; +use strict; + +use DisOrder; +use File::FcntlLock; +use POSIX qw{:errno_h :fcntl_h}; + +###-------------------------------------------------------------------------- +### Configuration. + +my %C = (config => "$ENV{HOME}/.disorder/passwd", + lockdir => "$ENV{HOME}/.disorder/", + mixer => "Master,0"); - my $kid = fork; - defined $kid or return; +my $TITLE = "DisOrder"; +my $VARIANT = "default"; +if (-l $C{config} && (my $t = readlink $C{config}) =~ /^passwd\.(.*)$/) + { $VARIANT = $1; $TITLE .= " ($1)"; } + +###-------------------------------------------------------------------------- +### Random utilities. + +sub run_discard_output (@) { + my $kid = fork(); if (!$kid) { - open STDOUT, ">", "/dev/null"; - exec "gdbus", "call", "-e", - "-d", "org.freedesktop.Notifications", - "-o", "/org/freedesktop/Notifications", - "-m", "org.freedesktop.Notifications.Notify", "--", - "DisOrder", "0", "audio-volume-high", - $head, $body, "[]", "{}", "5000"; + open STDOUT, ">/dev/null" or die "open /dev/null: $!"; + exec @_; } waitpid $kid, 0; + if ($?) { + my $st; + if ($? >= 256) { $st = sprintf "rc = %d", $? >> 8; } + else { $st = sprintf "signal %d", $?; } + die "$_[0] failed ($st)"; + } } -sub cmd (@) { - my @args = @_; - open my $f, "-|", "disorder", @args; - chomp (my @r = <$f>); - close $f; - if (wantarray) { return @r; } - elsif (@r == 1) { return $r[0]; } - else { return "??? multiple lines"; } +sub notify ($$) { + my ($head, $body) = @_; + + $body =~ s:\&:&:g; + $body =~ s:\<:<:g; + $body =~ s:\>:>:g; + + ##print "****************\n$head\n\n$body\n"; return; + + run_discard_output "notify-send", + "-c", "DisOrder", "-i", "audio-volume-high", "-t", "5000", + $head, $body; } -sub now_playing ($) { - my ($track) = @_; - my %p; - for my $p ("artist", "album", "title") - { $p{$p} = cmd "part", $track, "display", $p; } - if ($p{artist} =~ /^[A-Z]$/) - { $p{artist} = $p{album}; $p{album} = undef; } - elsif ($p{artist} eq "share" && $p{album} eq "disorder") - { next LINE; } - my $r = "$p{artist}: ‘$p{title}’"; - if (defined $p{album}) { $r .= ", from ‘$p{album}’"; } - notify "DisOrder: now playing", $r; +sub try_unlink ($) { + my ($f) = @_; + eval { unlink $f; }; + die $@ if $@ and $@->errno != ENOENT; } -for (;;) { - open my $log, "-|", "disorder", "log"; - LINE: while (<$log>) { - chomp; - my @f = (); - my $q = my $t = undef; - my $e = 0; - my $j = -1; - for (my $i = 0; $i < length $_; $i++) { - my $ch = substr($_, $i, 1); - if ($e) { - if ($ch eq "n") { $ch = "\n"; } - $t .= $ch; $e = 0; - } elsif ($ch eq $q) { - push @f, $t; $q = $t = undef; - } elsif (defined $q) { - if ($ch eq "\\") { $e = 1; } - else { $t .= $ch; } - } elsif ($ch eq " ") { - push @f, $t if defined $t; $t = undef; - } elsif (!defined $t && ($ch eq '"' || $ch eq "'")) { - $t //= ""; $q = $ch; $j = $i; - } else { - $t //= ""; $t .= $ch; - } - } - defined $q and die "unmatched $q (pos $j) in: $_"; - push @f, $t if defined $t; - - my $what = $f[1]; - if ($what eq "state") { - my $st = $f[2]; - my $msg; - if ($st eq "disable_random") { $msg = "random play disabled"; } - elsif ($st eq "enable_random") { $msg = "random play enabled"; } - elsif ($st eq "disable_play") { $msg = "playing disabled"; } - elsif ($st eq "enable_play") { $msg = "playing enabled"; } - elsif ($st eq "pause") { $msg = "paused"; } - elsif ($st eq "resume") { $msg = "playing"; } - else { next LINE; } - notify "DisOrder state", ucfirst $msg; - } - } elsif ($what eq "playing") { - now_playing $f[2]; +###-------------------------------------------------------------------------- +### Locking protocol. + +my $LKFILE = "$C{lockdir}/disorder-notify-$VARIANT.lock"; +my $LKFH; + +sub locked_by () { + + ## Try to open the lock file. If it's not there, then obviously it's not + ## locked. + my $fh; + eval { open $fh, "<", $LKFILE; }; + if ($@) { + return undef if $@->errno == ENOENT; + die $@; + } + + ## Take out a non-exclusive lock on the lock file. + my $lk = new File::FcntlLock; + $lk->l_type(F_RDLCK); $lk->l_whence(SEEK_SET); + $lk->l_start(0); $lk->l_len(0); + if ($lk->lock($fh, F_SETLK)) { close $fh; return undef; } + + ## Read the pid of the current lock-holder. + chomp (my $pid = (readline $fh) // ""); + close $fh; + return $pid; +} + +sub claim_lock () { + sysopen my $fh, $LKFILE, O_CREAT | O_WRONLY; + + my $lk = new File::FcntlLock; + $lk->l_type(F_WRLCK); $lk->l_whence(SEEK_SET); + $lk->l_start(0); $lk->l_len(0); + if (!$lk->lock($fh, F_SETLK)) { + return undef if $! == EAGAIN; + die "failed to lock `$LKFILE': $!"; + } + + truncate $fh, 0; + print $fh "$$\n"; + flush $fh; + $LKFH = $fh; + 1; +} + +###-------------------------------------------------------------------------- +### DisOrder utilities. + +sub get_state0 ($) { + my ($sk) = @_; + my %st = (); + + LINE: for (;;) { + my @f = split_fields readline $sk; + if ($f[1] ne "state") { last LINE; } + elsif ($f[2] eq "enable_random") { $st{random} = 1; } + elsif ($f[2] eq "disable_random") { $st{random} = 0; } + elsif ($f[2] eq "enable_play") { $st{play} = 1; } + elsif ($f[2] eq "disable_play") { $st{play} = 0; } + elsif ($f[2] eq "resume") { $st{pause} = 0; } + elsif ($f[2] eq "pause") { $st{pause} = 1; } + } + return \%st; +} + +sub get_state () { + my $sk = connect_to_server $C{config}; + send_command0 $sk, "log"; + my $st = get_state0 $sk; + close $sk; + return $st; +} + +sub decode_track_name ($\%) { + my ($sk, $info) = @_; + return unless exists $info->{track}; + my $track = $info->{track}; + for my $i ("artist", "album", "title") { + my @f = split_fields send_command $sk, "part", $track, "display", "$i"; + $info->{$i} = $f[0]; + } +} + +sub format_now_playing (\%) { + my ($info) = @_; + exists $info->{track} or return "Nothing."; + my $r = "$info->{artist}: ‘$info->{title}’"; + $r .= ", from ‘$info->{album}’" if $info->{album}; + $r .= "\n(chosen by $info->{submitter})" if exists $info->{submitter}; + return $r; +} + +sub get_now_playing ($) { + my ($sk) = @_; + my $r = send_command $sk, "playing"; + defined $r or return {}; + my %info = split_fields $r; + decode_track_name $sk, %info; + return \%info; +} + +sub watch_and_notify0 ($) { + my ($now_playing) = @_; + + my $sk = connect_to_server $C{config}, 1; + my $sk_log = connect_to_server $C{config}, 1; + + send_command0 $sk_log, "log"; + my $st = get_state0 $sk_log; + my $msg = "playing " . ($st->{play} ? "enabled" : "disabled"); + $msg .= "; random play " . ($st->{random} ? "enabled" : "disabled"); + $msg .= "; " . ($st->{pause} ? "paused" : "playing"); + notify "$TITLE state", "Connected: $msg"; + if ($st->{play} && $now_playing) { + my $info = get_now_playing $sk; + notify "$TITLE: Now playing", format_now_playing %$info; + } + + while (my $line = readline $sk_log) { + my @f = split_fields $line; + + if ($f[1] eq "state") { + my $msg = undef; + if ($f[2] eq "disable_random") { $msg = "Random play disabled"; } + elsif ($f[2] eq "enable_random") { $msg = "Random play enabled"; } + elsif ($f[2] eq "disable_play") { $msg = "Playing disabled"; } + elsif ($f[2] eq "enable_play") { $msg = "Playing enabled"; } + elsif ($f[2] eq "pause") { $msg = "Paused"; } + elsif ($f[2] eq "resume") { $msg = "Playing"; } + notify "$TITLE state", $msg if defined $msg; + } elsif ($f[1] eq "playing") { + my %info; + $info{track} = $f[2]; + $info{submitter} = $f[3] if @f > 3; + decode_track_name $sk, %info; + notify "$TITLE: Now playing", format_now_playing %info; + } elsif ($f[1] eq "scratched") { + my %info; + $info{track} = $f[2]; + decode_track_name $sk, %info; + notify "$TITLE: Scratched by $f[3]", format_now_playing %info; } } - close $log; - sleep 5; + + notify "$TITLE state", "Lost connection"; + + close $sk; + close $sk_log; } + +sub watch_and_notify ($) { + my ($now_playing) = @_; + + fork and exit 0; + claim_lock or exit 1; + + for (;;) { + eval { watch_and_notify0 $now_playing; }; + $now_playing = 1; + sleep 5; + } +} + +###-------------------------------------------------------------------------- +### User-facing operations. + +my %OP; + +$OP{"volume-up"} = + sub { run_discard_output "amixer", "sset", $C{mixer}, "5\%+"; }; +$OP{"volume-down"} = + sub { run_discard_output "amixer", "sset", $C{mixer}, "5\%-"; }; + +$OP{"scratch"} = sub { + my $sk = connect_to_server $C{config}; + send_command $sk, "scratch"; + close $sk; +}; + +$OP{"enable/disable"} = sub { + my $st = get_state; + my $sk = connect_to_server $C{config}; + if ($st->{play}) { send_command $sk, "disable"; } + else { send_command $sk, "enable"; } + close $sk; +}; + +$OP{"play/pause"} = sub { + my $st = get_state; + my $sk = connect_to_server $C{config}; + if (!$st->{play}) { + send_command $sk, "enable"; + if ($st->{pause}) { send_command $sk, "resume"; } + } else { + if ($st->{pause}) { send_command $sk, "resume"; } + else { send_command $sk, "pause"; } + } + close $sk; +}; + +$OP{"watch"} = sub { + if (defined (my $lkpid = locked_by)) { + print STDERR "$0: already watched by pid $lkpid\n"; + exit 2; + } + watch_and_notify 1; +}; + +$OP{"now-playing"} = sub { + my $sk = connect_to_server $C{config}; + my $info = get_now_playing $sk; + close $sk; + print format_now_playing %$info; + print "\n"; +}; + +$OP{"notify-now-playing"} = sub { + my $sk = connect_to_server $C{config}; + my $info = get_now_playing $sk; + close $sk; + notify "$TITLE: Now playing", format_now_playing %$info; + defined locked_by or watch_and_notify 0; +}; + +$OP{"next-config"} = sub { + (my $dir = $C{config}) =~ s:/[^/]*$::; + my (@conf, $curr, $conf, $min); + + if (-l $C{config} && (my $t = readlink $C{config}) =~ /^passwd\.(.*)$/) + { $curr = $1; } + + opendir my $dh, +$dir; + FILE: while (my $f = readdir $dh) + { push @conf, $1 if $f =~ /^passwd\.(.*[^~])$/; } + + for (my $i = 0; $i < @conf; $i++) { + $min = $conf[$i] if (!defined $min) || $conf[$i] lt $min; + $conf = $conf[$i] + if ((!defined $curr) || $curr lt $conf[$i]) && + ((!defined $conf) || $conf[$i] lt $conf); + } + $conf = $min unless defined $conf; + + try_unlink "$dir/passwd.new"; + symlink "passwd.$conf", "$dir/passwd.new"; + rename "$dir/passwd.new", "$dir/passwd"; + notify "DisOrder configuration", "Switched to `$conf'"; +}; + +###-------------------------------------------------------------------------- +### Main program. + +if (@ARGV != 1) { print STDERR "usage: $0 OP\n"; exit 2; } +my $op = $ARGV[0]; +if (!exists $OP{$op}) { print STDERR "$0: unknown op `$op'\n"; exit 2; } +$OP{$op}(); + +###----- That's all, folks --------------------------------------------------