chiark / gitweb /
bin/disorder-notify: Rewrite and take over the functionality of `media-keys'.
[profile] / bin / disorder-notify
index d36db9a1d57c4f70394fd3dc872e0a946f05fffa..e67668f49489d09dec9498cf76fe7f98ef527d91 100755 (executable)
-#! /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:\&:&amp;:g;
+  $body =~ s:\<:&lt;:g;
+  $body =~ s:\>:&gt;: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];
-      if ($st eq "disable_random") {
-       notify "DisOrder state", "Random play disabled";
-      } elsif ($st eq "enable_random") {
-       notify "DisOrder state", "Random play enabled";
-      } elsif ($st eq "disable_play") {
-       notify "DisOrder state", "Playing disabled";
-      } elsif ($st eq "enable_play") {
-       notify "DisOrder state", "Playing enabled";
-      } elsif ($st eq "pause") {
-       notify "DisOrder state", "Paused";
-      } elsif ($st eq "resume") {
-       notify "DisOrder state", "Resuming";
-      }
-    } 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) // "<unknown>");
+  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;
+};
+
+###--------------------------------------------------------------------------
+### 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 --------------------------------------------------