bin/mdw-sbuild, bin/mdw-sbuild-server: Update `usage' messages.
[profile] / bin / disorder-notify
CommitLineData
a1b30762
MW
1#! /usr/bin/perl -w
2
3use autodie qw{:all};
4use strict;
5
6use DisOrder;
7use File::FcntlLock;
21ec4bc8
MW
8use Getopt::Long qw{:config gnu_compat bundling
9 require_order no_getopt_compat};
a1b30762
MW
10use POSIX qw{:errno_h :fcntl_h};
11
12###--------------------------------------------------------------------------
13### Configuration.
14
15my %C = (config => "$ENV{HOME}/.disorder/passwd",
16 lockdir => "$ENV{HOME}/.disorder/",
17 mixer => "Master,0");
18
21ec4bc8 19(my $PROG = $0) =~ s:^.*/::;
a1b30762
MW
20my $TITLE = "DisOrder";
21my $VARIANT = "default";
22if (-l $C{config} && (my $t = readlink $C{config}) =~ /^passwd\.(.*)$/)
23 { $VARIANT = $1; $TITLE .= " ($1)"; }
24
25###--------------------------------------------------------------------------
26### Random utilities.
27
28sub run_discard_output (@) {
29 my $kid = fork();
30 if (!$kid) {
31 open STDOUT, ">/dev/null" or die "open /dev/null: $!";
32 exec @_;
33 }
34 waitpid $kid, 0;
35 if ($?) {
36 my $st;
37 if ($? >= 256) { $st = sprintf "rc = %d", $? >> 8; }
38 else { $st = sprintf "signal %d", $?; }
39 die "$_[0] failed ($st)";
40 }
41}
6bdf3aad
MW
42
43sub notify ($$) {
44 my ($head, $body) = @_;
45
0452eefc
MW
46 $body =~ s:\&:&:g;
47 $body =~ s:\<:&lt;:g;
48 $body =~ s:\>:&gt;:g;
a1b30762
MW
49
50 ##print "****************\n$head\n\n$body\n"; return;
51
52 run_discard_output "notify-send",
53 "-c", "DisOrder", "-i", "audio-volume-high", "-t", "5000",
54 $head, $body;
55}
56
57sub try_unlink ($) {
58 my ($f) = @_;
59 eval { unlink $f; };
60 die $@ if $@ and $@->errno != ENOENT;
61}
62
63###--------------------------------------------------------------------------
64### Locking protocol.
65
66my $LKFILE = "$C{lockdir}/disorder-notify-$VARIANT.lock";
67my $LKFH;
68
69sub locked_by () {
70
71 ## Try to open the lock file. If it's not there, then obviously it's not
72 ## locked.
73 my $fh;
74 eval { open $fh, "<", $LKFILE; };
75 if ($@) {
76 return undef if $@->errno == ENOENT;
77 die $@;
6bdf3aad 78 }
a1b30762
MW
79
80 ## Take out a non-exclusive lock on the lock file.
81 my $lk = new File::FcntlLock;
82 $lk->l_type(F_RDLCK); $lk->l_whence(SEEK_SET);
83 $lk->l_start(0); $lk->l_len(0);
84 if ($lk->lock($fh, F_SETLK)) { close $fh; return undef; }
85
86 ## Read the pid of the current lock-holder.
87 chomp (my $pid = (readline $fh) // "<unknown>");
88 close $fh;
89 return $pid;
6bdf3aad
MW
90}
91
a1b30762
MW
92sub claim_lock () {
93 sysopen my $fh, $LKFILE, O_CREAT | O_WRONLY;
94
95 my $lk = new File::FcntlLock;
96 $lk->l_type(F_WRLCK); $lk->l_whence(SEEK_SET);
97 $lk->l_start(0); $lk->l_len(0);
98 if (!$lk->lock($fh, F_SETLK)) {
99 return undef if $! == EAGAIN;
100 die "failed to lock `$LKFILE': $!";
101 }
102
103 truncate $fh, 0;
104 print $fh "$$\n";
105 flush $fh;
106 $LKFH = $fh;
107 1;
994838b7
MW
108}
109
a1b30762
MW
110###--------------------------------------------------------------------------
111### DisOrder utilities.
112
113sub get_state0 ($) {
114 my ($sk) = @_;
115 my %st = ();
116
117 LINE: for (;;) {
118 my @f = split_fields readline $sk;
119 if ($f[1] ne "state") { last LINE; }
120 elsif ($f[2] eq "enable_random") { $st{random} = 1; }
121 elsif ($f[2] eq "disable_random") { $st{random} = 0; }
122 elsif ($f[2] eq "enable_play") { $st{play} = 1; }
123 elsif ($f[2] eq "disable_play") { $st{play} = 0; }
124 elsif ($f[2] eq "resume") { $st{pause} = 0; }
125 elsif ($f[2] eq "pause") { $st{pause} = 1; }
a367c2fe 126 }
a1b30762 127 return \%st;
f1b1fa59
MW
128}
129
be626dd5
MW
130my $CONF = undef;
131
132sub configured_connection (;$) {
133 my ($quietp) = @_;
134 $CONF //= load_config $C{config};
135 return connect_to_server %$CONF, $quietp // 0;
136}
137
a1b30762 138sub get_state () {
be626dd5 139 my $sk = configured_connection;
a1b30762
MW
140 send_command0 $sk, "log";
141 my $st = get_state0 $sk;
142 close $sk;
143 return $st;
144}
145
146sub decode_track_name ($\%) {
147 my ($sk, $info) = @_;
148 return unless exists $info->{track};
149 my $track = $info->{track};
150 for my $i ("artist", "album", "title") {
151 my @f = split_fields send_command $sk, "part", $track, "display", "$i";
152 $info->{$i} = $f[0];
153 }
154}
155
16e1b76d
MW
156sub fmt_duration ($) {
157 my ($n) = @_;
158 return sprintf "%d:%02d", int $n/60, $n%60;
159}
160
a1b30762
MW
161sub format_now_playing (\%) {
162 my ($info) = @_;
163 exists $info->{track} or return "Nothing.";
164 my $r = "$info->{artist}: ‘$info->{title}’";
165 $r .= ", from ‘$info->{album}’" if $info->{album};
16e1b76d
MW
166 exists $info->{sofar} && exists $info->{length} and
167 $r .= sprintf " (%s/%s)",
168 fmt_duration $info->{sofar}, fmt_duration $info->{length};
a1b30762
MW
169 $r .= "\n(chosen by $info->{submitter})" if exists $info->{submitter};
170 return $r;
171}
172
173sub get_now_playing ($) {
174 my ($sk) = @_;
175 my $r = send_command $sk, "playing";
176 defined $r or return {};
177 my %info = split_fields $r;
178 decode_track_name $sk, %info;
16e1b76d
MW
179 exists $info{sofar} and
180 $info{length} = send_command $sk, "length", $info{track};
a1b30762
MW
181 return \%info;
182}
183
184sub watch_and_notify0 ($) {
185 my ($now_playing) = @_;
186
be626dd5
MW
187 my $sk = configured_connection 1;
188 my $sk_log = configured_connection 1;
a1b30762
MW
189
190 send_command0 $sk_log, "log";
191 my $st = get_state0 $sk_log;
192 my $msg = "playing " . ($st->{play} ? "enabled" : "disabled");
193 $msg .= "; random play " . ($st->{random} ? "enabled" : "disabled");
194 $msg .= "; " . ($st->{pause} ? "paused" : "playing");
195 notify "$TITLE state", "Connected: $msg";
196 if ($st->{play} && $now_playing) {
197 my $info = get_now_playing $sk;
198 notify "$TITLE: Now playing", format_now_playing %$info;
199 }
200
f6ef7584
MW
201 fcntl $sk_log, F_SETFL, (fcntl $sk_log, F_GETFL, 0) | O_NONBLOCK;
202 my $buffer = "";
203 my @lines = ();
204 my $rdin = ""; vec($rdin, (fileno $sk_log), 1) = 1;
c7eee684 205 my $loss;
f6ef7584
MW
206
207 WATCH: for (;;) {
208 for my $line (@lines) {
209 my @f = split_fields $line;
210 if ($f[1] eq "state") {
211 my $msg = undef;
212 if ($f[2] eq "disable_random") { $msg = "Random play disabled"; }
213 elsif ($f[2] eq "enable_random") { $msg = "Random play enabled"; }
214 elsif ($f[2] eq "disable_play") { $msg = "Playing disabled"; }
215 elsif ($f[2] eq "enable_play") { $msg = "Playing enabled"; }
216 elsif ($f[2] eq "pause") { $msg = "Paused"; }
217 elsif ($f[2] eq "resume") { $msg = "Playing"; }
218 notify "$TITLE state", $msg if defined $msg;
219 } elsif ($f[1] eq "playing") {
220 my %info;
221 $info{track} = $f[2];
222 $info{submitter} = $f[3] if @f > 3;
223 decode_track_name $sk, %info;
224 notify "$TITLE: Now playing", format_now_playing %info;
225 } elsif ($f[1] eq "scratched") {
226 my %info;
227 $info{track} = $f[2];
228 decode_track_name $sk, %info;
229 notify "$TITLE: Scratched by $f[3]", format_now_playing %info;
230 }
6bdf3aad 231 }
f6ef7584 232
c7eee684 233 if (!$sk_log) { $loss = "EOF from server"; last WATCH; }
50d7331d
MW
234 my $nfd = select my $rdout = $rdin, undef, undef, 60;
235 if (!$nfd) {
236 eval { print $sk_log "."; flush $sk_log; };
237 if ($@) { $loss = "error from write: " . $@->errno; last WATCH; }
238 @lines = ();
239 } else {
240 READ: for (;;) {
241 my ($b, $n);
242 eval { $n = sysread $sk_log, $b, 4096; };
243 if ($@ && $@->errno == EAGAIN) { last READ; }
244 elsif ($@) { $loss = "error from read: " . $@->errno; last WATCH; }
ba19b1ea 245 elsif (!$n) { close $sk_log; $sk_log = undef; last READ; }
50d7331d
MW
246 else { $buffer .= $b; }
247 }
f6ef7584 248
50d7331d
MW
249 @lines = split /\n/, $buffer, -1;
250 $buffer = pop @lines;
251 }
6bdf3aad 252 }
a1b30762 253
c7eee684 254 notify "$TITLE state", "Lost connection: $loss";
a1b30762
MW
255
256 close $sk;
f6ef7584 257 close $sk_log if defined $sk_log;
6bdf3aad 258}
a1b30762
MW
259
260sub watch_and_notify ($) {
261 my ($now_playing) = @_;
262
a1b30762
MW
263 claim_lock or exit 1;
264
265 for (;;) {
266 eval { watch_and_notify0 $now_playing; };
267 $now_playing = 1;
268 sleep 5;
269 }
270}
271
272###--------------------------------------------------------------------------
273### User-facing operations.
274
275my %OP;
276
277$OP{"volume-up"} =
278 sub { run_discard_output "amixer", "sset", $C{mixer}, "5\%+"; };
279$OP{"volume-down"} =
280 sub { run_discard_output "amixer", "sset", $C{mixer}, "5\%-"; };
281
282$OP{"scratch"} = sub {
be626dd5 283 my $sk = configured_connection;
a1b30762
MW
284 send_command $sk, "scratch";
285 close $sk;
286};
287
288$OP{"enable/disable"} = sub {
289 my $st = get_state;
be626dd5 290 my $sk =configured_connection;
a1b30762
MW
291 if ($st->{play}) { send_command $sk, "disable"; }
292 else { send_command $sk, "enable"; }
293 close $sk;
294};
295
296$OP{"play/pause"} = sub {
297 my $st = get_state;
be626dd5 298 my $sk = configured_connection;
a1b30762
MW
299 if (!$st->{play}) {
300 send_command $sk, "enable";
301 if ($st->{pause}) { send_command $sk, "resume"; }
302 } else {
303 if ($st->{pause}) { send_command $sk, "resume"; }
304 else { send_command $sk, "pause"; }
305 }
306 close $sk;
307};
308
309$OP{"watch"} = sub {
310 if (defined (my $lkpid = locked_by)) {
311 print STDERR "$0: already watched by pid $lkpid\n";
312 exit 2;
313 }
314 watch_and_notify 1;
315};
316
317$OP{"now-playing"} = sub {
be626dd5 318 my $sk = configured_connection;
a1b30762
MW
319 my $info = get_now_playing $sk;
320 close $sk;
321 print format_now_playing %$info;
322 print "\n";
323};
324
325$OP{"notify-now-playing"} = sub {
be626dd5 326 my $sk = configured_connection;
a1b30762
MW
327 my $info = get_now_playing $sk;
328 close $sk;
329 notify "$TITLE: Now playing", format_now_playing %$info;
a9e2532b
MW
330 unless (defined locked_by) {
331 fork and exit 0;
332 watch_and_notify 0;
333 }
a1b30762
MW
334};
335
70291d8d
MW
336$OP{"next-config"} = sub {
337 (my $dir = $C{config}) =~ s:/[^/]*$::;
338 my (@conf, $curr, $conf, $min);
339
340 if (-l $C{config} && (my $t = readlink $C{config}) =~ /^passwd\.(.*)$/)
341 { $curr = $1; }
342
343 opendir my $dh, +$dir;
344 FILE: while (my $f = readdir $dh)
345 { push @conf, $1 if $f =~ /^passwd\.(.*[^~])$/; }
346
347 for (my $i = 0; $i < @conf; $i++) {
348 $min = $conf[$i] if (!defined $min) || $conf[$i] lt $min;
349 $conf = $conf[$i]
350 if ((!defined $curr) || $curr lt $conf[$i]) &&
351 ((!defined $conf) || $conf[$i] lt $conf);
352 }
353 $conf = $min unless defined $conf;
354
355 try_unlink "$dir/passwd.new";
356 symlink "passwd.$conf", "$dir/passwd.new";
357 rename "$dir/passwd.new", "$dir/passwd";
358 notify "DisOrder configuration", "Switched to `$conf'";
359};
360
a1b30762
MW
361###--------------------------------------------------------------------------
362### Main program.
363
21ec4bc8
MW
364sub usage (\*) {
365 my ($fh) = @_;
366 print $fh "usage: $PROG [-u CONFIG] COMMAND\n";
367}
368
369sub help () {
370 usage *STDOUT;
371 print <<EOF;
372
373Command-line options:
374 -h, --help Show this help text
375 -u, --user-config Set user configuration file
376
377Commands:
378 volume-up
379 volume-down
380 scratch
381 enable/disable
382 play/pause
383 watch
384 now-playing
385 notify-now-playing
386 next-config
387EOF
388}
389
390my $bad = 0;
391GetOptions
392 "h|help" => sub { help; exit 0; },
393 "u|user-config=s" => \$C{config}
394 or $bad = 1;
395@ARGV == 1 or $bad = 1;
396if ($bad) { usage *STDERR; exit 2; }
a1b30762
MW
397my $op = $ARGV[0];
398if (!exists $OP{$op}) { print STDERR "$0: unknown op `$op'\n"; exit 2; }
399$OP{$op}();
400
401###----- That's all, folks --------------------------------------------------