summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoey Hess <joeyh@debian.org>2011-01-03 09:49:13 -0800
committerJoey Hess <joeyh@debian.org>2011-01-03 09:49:13 -0800
commit44a3d2d1b9f97def795cbe8ab846d2a1593cfb64 (patch)
tree83d4ca187fc68a697952de2d599ccb4c4260e6e1
mpdtoys (0.24) unstable; urgency=low
* mprandomwalk: New toy to play random bits of all queued songs. * mpinsert: Add -p option to begin playing inserted songs. Closes: #608690 # imported from the archive
-rw-r--r--Makefile23
-rw-r--r--MpdToys.pm62
-rw-r--r--TODO4
-rw-r--r--debian/changelog175
-rw-r--r--debian/compat1
-rw-r--r--debian/control24
-rw-r--r--debian/copyright5
-rw-r--r--debian/docs1
-rwxr-xr-xdebian/rules7
l---------mpcp1
-rwxr-xr-xmpfade118
-rwxr-xr-xmpgenplaylists84
-rwxr-xr-xmpinsert123
-rwxr-xr-xmplength44
l---------mpload1
l---------mpmv1
-rwxr-xr-xmprand38
-rwxr-xr-xmprandomwalk62
-rwxr-xr-xmprev56
-rwxr-xr-xmprompt370
-rwxr-xr-xmpskip90
-rwxr-xr-xmpstore272
l---------mpswap1
-rwxr-xr-xmptoggle65
-rwxr-xr-xsats89
-rwxr-xr-xvipl161
26 files changed, 1878 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..bd5148f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,23 @@
+progs=mpstore mprev mpgenplaylists mpfade sats mpskip mptoggle mprand \
+ mpinsert vipl mprompt mplength mprandomwalk
+
+PERLLIBDIR=$(shell perl -e 'use Config; print $$Config{installvendorlib}')
+
+build:
+
+install:
+ install -d $(DESTDIR)/usr/bin/
+ install $(progs) $(DESTDIR)/usr/bin/
+
+ install -d $(DESTDIR)/usr/share/man/man1/
+ for prog in $(progs); do \
+ pod2man -c $$prog $$prog > $(DESTDIR)/usr/share/man/man1/$$prog.1; \
+ done
+
+ for link in mpload mpcp mpmv mpswap; do \
+ ln -sf mpstore $(DESTDIR)/usr/bin/$$link; \
+ ln -sf mpstore.1 $(DESTDIR)/usr/share/man/man1/$$link.1; \
+ done
+
+ install -d $(DESTDIR)/$(PERLLIBDIR)
+ install -m 0644 MpdToys.pm $(DESTDIR)/$(PERLLIBDIR)
diff --git a/MpdToys.pm b/MpdToys.pm
new file mode 100644
index 0000000..4290b80
--- /dev/null
+++ b/MpdToys.pm
@@ -0,0 +1,62 @@
+#!/usr/bin/perl
+package MpdToys;
+
+sub findmatchingsongs {
+ my $term=shift;
+ my $mpd=shift;
+
+ # pass urls through
+ if ($term=~/^(\w+):\/\//) {
+ return Audio::MPD::Common::Item->new(file => $term);
+ }
+
+ my $coll=$mpd->collection;
+
+ my $exactmatch=$coll->song($term);
+ if ($exactmatch) {
+ return $exactmatch;
+ }
+
+ my @matches = (
+ $coll->songs_from_album_partial($term),
+ $coll->songs_by_artist_partial($term),
+ $coll->songs_with_title_partial($term),
+ );
+
+ return dedupsongs(@matches);
+}
+
+sub canmatch_fuzzy {
+ eval q{use String::Approx 'amatch'};
+ return ! $@;
+}
+
+sub findmatchingsongs_fuzzy {
+ my $term=shift;
+ my $mpd=shift;
+
+ my $coll=$mpd->collection;
+ my @matches = (
+ (map { $coll->songs_from_album($_) }
+ amatch($term, ['i'], $coll->all_albums())),
+ (map { $coll->songs_by_artist($_) }
+ amatch($term, ['i'], $coll->all_artists()))
+ );
+
+ # very slow, only try if nothing else matched
+ @matches = amatch($term, ['i'], $coll->all_songs())
+ if ! @matches;
+
+ return dedupsongs(@matches);
+}
+
+sub dedupsongs {
+ my %seen;
+ my @ret;
+ foreach my $song (@_) {
+ push @ret, $song unless $seen{$song->file}++;
+ }
+ return @ret;
+}
+
+1
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..5171bcf
--- /dev/null
+++ b/TODO
@@ -0,0 +1,4 @@
+mprm: delete the currently playing file, once it finishes playing
+sats: If mpd has one song in the playlist, and has it on repeat, sats will
+ never stop it playing, since it waits for mpd to move to the next
+ song.
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..fe3573c
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,175 @@
+mpdtoys (0.24) unstable; urgency=low
+
+ * mprandomwalk: New toy to play random bits of all queued songs.
+ * mpinsert: Add -p option to begin playing inserted songs.
+ Closes: #608690
+
+ -- Joey Hess <joeyh@debian.org> Mon, 03 Jan 2011 13:49:13 -0400
+
+mpdtoys (0.23) unstable; urgency=low
+
+ * mplength: New toy to print length of current playlist.
+ * Updated to support change to how conntype is specified in
+ Audio::MPD 1.092950
+
+ -- Joey Hess <joeyh@debian.org> Sat, 06 Mar 2010 22:23:07 -0500
+
+mpdtoys (0.22) unstable; urgency=low
+
+ * mpswap, mpstore, etc: avoid crash when playing song time is not available.
+ Closes: 559182
+
+ -- Joey Hess <joeyh@debian.org> Wed, 02 Dec 2009 23:38:43 -0500
+
+mpdtoys (0.21) unstable; urgency=low
+
+ * mpinsert: Fix another utf-8 encoding problem. Closes: #549986
+ * mpinsert: Avoid printing uninitialized value warning when
+ it fails to add any songs and -n is used.
+
+ -- Joey Hess <joeyh@debian.org> Wed, 25 Nov 2009 06:43:25 -0500
+
+mpdtoys (0.20) unstable; urgency=low
+
+ * Work around bug #548305.
+
+ -- Joey Hess <joeyh@debian.org> Sun, 27 Sep 2009 22:14:06 -0400
+
+mpdtoys (0.19) unstable; urgency=low
+
+ * Factored out matching code from mprompt and vipl.
+ * mprompt: Enable fuzzy matching by default, if the String::Approx module
+ is available. The -f flag is deprecated.
+ * vipl: Also enable fuzzy matching by default.
+ * vipl: Fix inserting/preservation of streaming urls in the playlist.
+ * mprompt, vipl: If the entered value exactly matches a file in the
+ mpd collection, use it without doing further matching.
+ * mpinsert: Enable partial/whole album/artist/fuzzy matching using
+ same code used for mprompt and vipl.
+
+ -- Joey Hess <joeyh@debian.org> Tue, 22 Sep 2009 21:40:06 -0400
+
+mpdtoys (0.18) unstable; urgency=low
+
+ * mprompt: Configure getopt to not ignore case.
+
+ -- Joey Hess <joeyh@debian.org> Wed, 03 Jun 2009 17:52:32 -0400
+
+mpdtoys (0.17) unstable; urgency=low
+
+ * mprompt: Rename terse switch to -T, -t was already taken.
+
+ -- Joey Hess <joeyh@debian.org> Mon, 18 May 2009 18:31:48 -0400
+
+mpdtoys (0.16) unstable; urgency=low
+
+ * mpinsert: Don't insert multiple items in reverse order when
+ not playing.
+ * mpinsert: Only display number of first item added with -n.
+
+ -- Joey Hess <joeyh@debian.org> Sat, 09 May 2009 20:23:17 -0400
+
+mpdtoys (0.15) unstable; urgency=low
+
+ * mprompt: Add -t switch, enabling a terse output mode where the output
+ is intended to be piped to a speech synth such as esound.
+ * mpinsert: Add -n switch, which prints the playlist position of
+ added items. This is useful if you want to insert an item and then jump
+ to it.
+
+ -- Joey Hess <joeyh@debian.org> Sat, 09 May 2009 17:18:08 -0400
+
+mpdtoys (0.14) unstable; urgency=low
+
+ * mprompt: Add -f parameter that enables fuzzy matching of entered
+ search terms. (Needs String::Approx perl module.)
+
+ -- Joey Hess <joeyh@debian.org> Sat, 11 Apr 2009 14:23:34 -0400
+
+mpdtoys (0.13) unstable; urgency=low
+
+ * mprompt: Add -t parameter, to allow clearing partially entered
+ lines after a timeout.
+
+ -- Joey Hess <joeyh@debian.org> Thu, 09 Apr 2009 20:15:06 -0400
+
+mpdtoys (0.12) unstable; urgency=low
+
+ * mprompt: Support exit on EOF/ctrl-d.
+ * vipl, mprompt: Smarter searching, in particular:
+ - Remove duplicates from result list.
+ - Search for whole albums or artists before individual songs,
+ so that songs with the same name do not appear out of order.
+
+ -- Joey Hess <joeyh@debian.org> Mon, 02 Mar 2009 18:43:55 -0500
+
+mpdtoys (0.11) unstable; urgency=low
+
+ * mprompt: prompt-based mpd client, designed for headless machines
+
+ -- Joey Hess <joeyh@debian.org> Sat, 28 Feb 2009 19:35:18 -0500
+
+mpdtoys (0.10) unstable; urgency=low
+
+ * Fix some bad man page synopses. Closes: #515304
+
+ -- Joey Hess <joeyh@debian.org> Sun, 15 Feb 2009 13:35:39 -0500
+
+mpdtoys (0.9) unstable; urgency=low
+
+ * vipl: Fix handling of utf-8 in song info. Closes: #514119
+
+ -- Joey Hess <joeyh@debian.org> Wed, 04 Feb 2009 17:57:19 -0500
+
+mpdtoys (0.8) unstable; urgency=low
+
+ * vipl: New program, allows editing the mpd playlist in your text editor.
+ * Use debhelper v7; rules file minimisation.
+ * Use DESTDIR rather than DEST.
+
+ -- Joey Hess <joeyh@debian.org> Wed, 30 Apr 2008 02:04:49 -0400
+
+mpdtoys (0.7) unstable; urgency=low
+
+ * sats: Add -n option allowing to stop after more than one song has played.
+
+ -- Joey Hess <joeyh@debian.org> Wed, 30 Jan 2008 02:52:50 -0500
+
+mpdtoys (0.6) unstable; urgency=low
+
+ * mpswap: Fix handling when two hosts are specified on the command line.
+
+ -- Joey Hess <joeyh@debian.org> Sat, 19 Jan 2008 17:28:02 -0500
+
+mpdtoys (0.5) unstable; urgency=low
+
+ * mpskip: Fix mispaste in synopsis. Closes: #457553
+
+ -- Joey Hess <joeyh@debian.org> Sun, 23 Dec 2007 11:19:50 -0500
+
+mpdtoys (0.4) unstable; urgency=low
+
+ * mptoggle: Allow the playlists to use to be specified at the command line.
+
+ -- Joey Hess <joeyh@debian.org> Thu, 20 Dec 2007 15:39:06 -0500
+
+mpdtoys (0.3) unstable; urgency=low
+
+ * mpswap, mpcp, mpmv: Fix handling of only one host being specified on the
+ command line.
+
+ -- Joey Hess <joeyh@debian.org> Sat, 15 Dec 2007 00:03:48 -0500
+
+mpdtoys (0.2) unstable; urgency=low
+
+ * mpgenplaylists: Sort, don't rely on find order.
+ * mprev: Don't crash if the playlist is empty.
+ * Add mpinsert.
+
+ -- Joey Hess <joeyh@debian.org> Fri, 14 Dec 2007 23:39:54 -0500
+
+mpdtoys (0.1) unstable; urgency=low
+
+ * First release.
+
+ -- Joey Hess <joeyh@debian.org> Sat, 01 Dec 2007 16:51:04 -0500
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..7f8f011
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+7
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..aa1008a
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,24 @@
+Source: mpdtoys
+Section: sound
+Priority: optional
+Build-Depends: debhelper (>= 7), dpkg-dev (>= 1.9.0)
+Maintainer: Joey Hess <joeyh@debian.org>
+Standards-Version: 3.8.4
+Homepage: http://kitenet.net/~joey/code/mpdtoys/
+Vcs-Git: git://git.kitenet.net/mpdtoys
+
+Package: mpdtoys
+Architecture: all
+Depends: perl, libaudio-mpd-perl (>= 1.100430-1), ${misc:Depends}
+Suggests: mpd, libproc-daemon-perl, libterm-readkey-perl, libstring-approx-perl
+Description: small command line tools and toys for MPD
+ This is a collection of small toys and tools for doing various things
+ to MPD (Music Player Daemon) from the command line. Some of them are
+ very useful, while others are only amusing.
+ .
+ Some examples of things the mpdtoys can do include moving the playing
+ song between different mpd daemons on different machines, storing
+ the state of a mpd daemon and loading it back later, reversing the
+ playlist, slowly fading volume up or down, stopping playback after the
+ current song finishes, emulating a skipping record, and editing the
+ playlist in a text editor.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..24c49f0
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,5 @@
+Files: *
+Copyright: (c) 2007 Joey Hess <joeyh@debian.org>
+License: GPL-2+
+ On Debian systems, the complete text of the GPL can be found in
+ /usr/share/common-licenses/GPL.
diff --git a/debian/docs b/debian/docs
new file mode 100644
index 0000000..1333ed7
--- /dev/null
+++ b/debian/docs
@@ -0,0 +1 @@
+TODO
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..6e8f2cc
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,7 @@
+#!/usr/bin/make -f
+%:
+ dh $@
+
+# Not intended for use by anyone except the author.
+announcedir:
+ @echo ${HOME}/src/joeywiki/code/mpdtoys/news
diff --git a/mpcp b/mpcp
new file mode 120000
index 0000000..5f7d564
--- /dev/null
+++ b/mpcp
@@ -0,0 +1 @@
+mpstore \ No newline at end of file
diff --git a/mpfade b/mpfade
new file mode 100755
index 0000000..49adf17
--- /dev/null
+++ b/mpfade
@@ -0,0 +1,118 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use Audio::MPD q{0.19.0};
+
+=head1 NAME
+
+mpfade - fade mpd volume in or out
+
+=head1 SYNOPSIS
+
+mpfade [minutes] [max|min] [host]
+
+=head1 DESCRIPTION
+
+B<mpfade> behaves differently depending on whether mpd is playing or not.
+
+If mpd is not playing (or is paused), it starts it playing at a low volume,
+and gradually cranks the volume up to the specified B<max> value (default
+50) over the specified number of minutes (default 10).
+
+If mpd is already playing, it works in reverse, reducing the volume over
+time until it's a the specified B<min> (defaults to a tenth of what it was
+at the start). Then it stops playing.
+
+B<mpfade> tries to interact well with manual volume changes you make. If
+the volume is fading up, and you change the volume manually, it stops. This
+is intended to be useful when used as an alarm clock. If the volume is
+fading down, changes you make to the volume will be noticed, and the fade
+down will continue from that point.
+
+The B<minutes> value can be a floating point value. If the hostname is
+omitted, the MPD_HOST environment variable will be used.
+
+=head1 AUTHOR
+
+Copyright 2007 Joey Hess <joey@kitenet.net>
+
+Licensed under the GNU GPL version 2 or higher.
+
+http://kitenet.net/~joey/code/mpdtoys
+
+=cut
+
+my $seconds=60 * 10;
+if (@ARGV && $ARGV[0] =~ /^[0-9.]+$/) {
+ $seconds=60 * shift;
+}
+my $endpoint;
+if (@ARGV && $ARGV[0] =~ /^[0-9.]+$/) {
+ $endpoint=shift;
+}
+
+if (@ARGV) {
+ $ENV{MPD_HOST}=shift;
+}
+
+my $mpd=Audio::MPD->new(conntype => "reuse");
+my $vol=$mpd->status->volume;
+if ($mpd->status->state eq 'play') {
+ if (defined $endpoint) {
+ if ($endpoint >= $vol) {
+ die "error: min ($endpoint) is not less than current volume ($vol)\n";
+ }
+ }
+ else {
+ $endpoint=int($vol / 10);
+ }
+ print "fading down from $vol to $endpoint over $seconds seconds\n";
+ fade($endpoint, $seconds);
+ $mpd->stop;
+}
+else {
+ if (! defined $endpoint) {
+ $endpoint=50;
+ }
+ $mpd->volume($vol=0);
+ print "fading up from $vol to $endpoint over $seconds seconds\n";
+ $mpd->play;
+ fade($endpoint, $seconds);
+}
+
+sub fade {
+ my $endpoint=shift;
+ my $seconds=shift;
+
+ if ($seconds < 1) {
+ $mpd->volume($endpoint);
+ return;
+ }
+
+ my $span=$endpoint - $vol;
+ my $oldvol=$vol;
+ my $curvol;
+
+ # TODO calculate how long to optimally sleep
+
+ while (sleep 1) {
+ $curvol=$mpd->status->volume;
+ if ($curvol != $oldvol) {
+ if ($span > 0) {
+ print "manual volume change, aborting fade up\n";
+ return;
+ }
+ else {
+ print "manual volume change\n";
+ $vol=$curvol;
+ }
+ }
+
+ $vol=$vol + ($span / $seconds);
+ last if abs($vol - $endpoint) <= 1;
+ if (abs(int($vol - $oldvol)) > 1) {
+ $mpd->volume(int($vol));
+ $oldvol=int($vol);
+ }
+ }
+}
diff --git a/mpgenplaylists b/mpgenplaylists
new file mode 100755
index 0000000..881bbec
--- /dev/null
+++ b/mpgenplaylists
@@ -0,0 +1,84 @@
+#!/bin/sh
+set -e
+
+conf=/etc/mpd.conf
+if [ -e $HOME/.mpdconf ]; then
+ conf=$HOME/.mpdconf
+fi
+if [ ! -e $conf ]; then
+ echo "error: $conf does not exist" >&2
+ exit 1
+fi
+
+music_directory=$(grep "^music_directory" $conf | cut -d '"' -f 2 | sed -e "s!^\~!$HOME!")
+playlist_directory=$(grep "^playlist_directory" $conf | cut -d '"' -f 2 | sed -e "s!^\~!$HOME!")
+
+if [ -z "$music_directory" ] || [ -z "$playlist_directory" ]; then
+ echo "error: failed to parse $conf" >&2
+ exit 1
+fi
+
+if [ ! -d "$music_directory" ] || [ ! -d "$playlist_directory" ]; then
+ echo "error: both $music_directory and $playlist_directory need to exist" >&2
+ exit 1
+fi
+
+rm $playlist_directory/\ *.m3u 2>/dev/null || true
+IFS="
+"
+for dir in $(cd "$music_directory"; find -type d -follow | sed 's!^./!!'); do
+ playlist="$(echo "$dir" | tr "_" " " | sed 's!/! - !g')"
+ if [ "$playlist" = . ]; then
+ playlist=all
+ fi
+ find "$music_directory/$dir" -type f -follow | sort \
+ > "$playlist_directory/ $playlist".m3u
+done
+
+exit
+
+<<POD
+
+=head1 NAME
+
+mpgenplaylists - generate mpd playlists for each subdirectory of music
+
+=head1 SYNOPSIS
+
+mpgenplaylists
+
+=head1 DESCRIPTION
+
+B<mpgenplaylists> generates mpd playlists.
+
+It reads your ~/.mpdconf or /etc/mpd.conf to figure out where mpd keeps its
+music directory and playlist directory.
+
+For each subdirectory of the music directory, a playlist is generated in the
+playlist directory. The playlists created by this tool always start with a
+space to avoid conflicts with your manually created playlists.
+
+So if you keep your sound in Artist/Album/ directories, you'll get playlists
+named like " Artist - Album", and also playlists named just " Artist" that
+contain all music by that artist. An " all" playlist is also created, that
+contains all your music.
+
+Each time it's run it updates the playlists, and removes any obsolete ones
+that it created before.
+
+=head1 LIMITATIONS
+
+It does not currently sort songs in an album by track number, but instead
+sorts by filename.
+
+=head1 AUTHOR
+
+Copyright 2007 Joey Hess <joey@kitenet.net>
+
+Licensed under the GNU GPL version 2 or higher.
+
+http://kitenet.net/~joey/code/mpdtoys
+
+=cut
+
+POD
diff --git a/mpinsert b/mpinsert
new file mode 100755
index 0000000..db25c84
--- /dev/null
+++ b/mpinsert
@@ -0,0 +1,123 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use Audio::MPD q{0.19.0};
+use MpdToys;
+use Encode;
+
+=head1 NAME
+
+mpinsert - insert song after currently playing song
+
+=head1 SYNOPSIS
+
+mpinsert [-n] [song ...]
+
+=head1 DESCRIPTION
+
+B<mpinsert> inserts a song (or songs) into the playlist directly after
+the currently playing song.
+
+If no songs are specified on the command line, it will read a list from
+stdin.
+
+Songs may be specified the same as they would be to mpc add: As paths to
+files in the music database, or urls to stream.
+
+You can also enter the name of a playlist, or part of the name of an
+album, artist, or song. Matching items will be added to the playlist.
+
+(If the perl String::Approx module is available, it will be used to handle
+typos, etc in the names you enter.)
+
+=head1 OPTIONS
+
+=over 4
+
+=item -n
+
+Print the playlist position number that the first song was inserted at.
+
+=back
+
+=item -p
+
+Play the song after inserting it.
+
+=back
+
+=head1 AUTHOR
+
+Copyright 2007-2009 Joey Hess <joey@kitenet.net>
+
+Licensed under the GNU GPL version 2 or higher.
+
+http://kitenet.net/~joey/code/mpdtoys
+
+=cut
+
+use Getopt::Long;
+my $shownum=0;
+my $playnow=0;
+GetOptions(
+ "n" => \$shownum,
+ "p" => \$playnow,
+) || usage();
+
+sub usage {
+ die "Usage: mpinsert [-n] [song ...]\n";
+}
+
+my @list=@ARGV;
+if (! @list) {
+ while (<>) {
+ chomp;
+ push @list, $_;
+ }
+}
+@list=map { decode_utf8($_) } @list;
+
+my $mpd=Audio::MPD->new(conntype => "reuse");
+my $pl=$mpd->playlist;
+
+@list=reverse @list if $mpd->current;
+
+my @out;
+my $firstsong;
+foreach my $item (@list) {
+ if (! length $item) {
+ die "no item specified to insert\n";
+ }
+
+ my @matches=MpdToys::findmatchingsongs($item, $mpd);
+ if (! @matches && MpdToys::canmatch_fuzzy()) {
+ @matches=MpdToys::findmatchingsongs_fuzzy($item, $mpd);
+ }
+
+ foreach my $song (@matches) {
+ $pl->add($song->file);
+ my @items=$pl->as_items;
+ if (! @items) {
+ die "failed!";
+ }
+ my $pos=$#items;
+
+ # move from end to just after current
+ my $current=$mpd->current;
+ if ($current) {
+ $pos=$current->pos+1;
+ $pl->move($#items, $pos);
+ }
+
+ $firstsong=$pos unless defined $firstsong;
+ push @out, ($pos+1) if $shownum;
+ }
+}
+
+if ($shownum) {
+ @out=reverse @out if $mpd->current;
+ print $out[0]."\n" if @out;
+}
+if ($playnow && defined $firstsong) {
+ $mpd->play($firstsong);
+}
diff --git a/mplength b/mplength
new file mode 100755
index 0000000..621891b
--- /dev/null
+++ b/mplength
@@ -0,0 +1,44 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use Audio::MPD q{0.19.0};
+
+=head1 NAME
+
+mplength - calculates length of mpd playlist
+
+=head1 SYNOPSIS
+
+mplength [host]
+
+=head1 DESCRIPTION
+
+B<mplength> calculates the total length of the currently selected
+playlist in mpd. The length is output to standard output in mm:ss format.
+
+=head1 SEE ALSO
+
+mpd(1)
+
+=head1 AUTHOR
+
+Copyright 2009 Joey Hess <joey@kitenet.net>
+
+Licensed under the GNU GPL version 2 or higher.
+
+http://kitenet.net/~joey/code/mpdtoys
+
+=cut
+
+if (@ARGV) {
+ $ENV{MPD_HOST}=shift;
+}
+my $mpd=Audio::MPD->new(conntype => "reuse");
+my $pl=$mpd->playlist;
+my @list=$pl->as_items;
+my $secs=0;
+foreach my $item (@list) {
+ $secs+=$item->time;
+}
+my $mins=int($secs/60);
+printf "%i:%02i\n", $mins, ($secs-$mins*60);
diff --git a/mpload b/mpload
new file mode 120000
index 0000000..5f7d564
--- /dev/null
+++ b/mpload
@@ -0,0 +1 @@
+mpstore \ No newline at end of file
diff --git a/mpmv b/mpmv
new file mode 120000
index 0000000..5f7d564
--- /dev/null
+++ b/mpmv
@@ -0,0 +1 @@
+mpstore \ No newline at end of file
diff --git a/mprand b/mprand
new file mode 100755
index 0000000..e140928
--- /dev/null
+++ b/mprand
@@ -0,0 +1,38 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use Audio::MPD q{0.19.0};
+
+=head1 NAME
+
+mprand - play a random playlist
+
+=head1 SYNOPSIS
+
+mprand[host]
+
+=head1 DESCRIPTION
+
+B<mprand> picks a playlist at random and tells mpd to play it.
+
+If the hostname is omitted, the MPD_HOST environment variable will be used.
+
+=head1 AUTHOR
+
+Copyright 2007 Joey Hess <joey@kitenet.net>
+
+Licensed under the GNU GPL version 2 or higher.
+
+http://kitenet.net/~joey/code/mpdtoys
+
+=cut
+
+if (@ARGV) {
+ $ENV{MPD_HOST}=shift;
+}
+my $mpd=Audio::MPD->new(conntype => "reuse");
+
+$mpd->playlist->clear;
+my @playlists=$mpd->collection->all_playlists;
+$mpd->playlist->load($playlists[rand @playlists]);
+$mpd->play;
diff --git a/mprandomwalk b/mprandomwalk
new file mode 100755
index 0000000..88fe5e0
--- /dev/null
+++ b/mprandomwalk
@@ -0,0 +1,62 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use Audio::MPD q{0.19.0};
+
+=head1 NAME
+
+mprandomwalk - play random bits of all queued songs
+
+=head1 SYNOPSIS
+
+mprandomwalk [maxduration] [host]
+
+=head1 DESCRIPTION
+
+B<mprandomwalk> plays random segments of randomly chosen songs in the
+playlist.
+
+The B<maxduration> can control the maximum number of seconds of a song to
+play at a time.
+
+If the hostname is omitted, the MPD_HOST environment variable will be used.
+
+=head1 AUTHOR
+
+Copyright 2010 Joey Hess <joey@kitenet.net>
+
+Licensed under the GNU GPL version 2 or higher.
+
+http://kitenet.net/~joey/code/mpdtoys
+
+=cut
+
+my $maxduration=10;
+if (@ARGV && $ARGV[0] =~ /^[0-9.]+$/) {
+ $maxduration=shift;
+}
+
+if (@ARGV) {
+ $ENV{MPD_HOST}=shift;
+}
+
+my $mpd=Audio::MPD->new(conntype => "reuse");
+
+$mpd->play;
+
+my $pl=$mpd->playlist;
+my @list=$pl->as_items;
+
+if (! @list) {
+ die "error: no songs queued\n";
+}
+
+while (1) {
+ my $song=$list[rand($#list)];
+ my $length=$song->time;
+ next unless $length;
+ my $duration=rand($maxduration);
+ my $pos=rand($length-$duration);
+ $mpd->seekid($pos, $song->id);
+ sleep $duration;
+}
diff --git a/mprev b/mprev
new file mode 100755
index 0000000..cb1f944
--- /dev/null
+++ b/mprev
@@ -0,0 +1,56 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use Audio::MPD q{0.19.0};
+
+=head1 NAME
+
+mprev - reverse the mpd playlist
+
+=head1 SYNOPSIS
+
+mprev [host]
+
+=head1 DESCRIPTION
+
+B<mprev> reverses mpd's playlist. That's all. The currently playing
+song doesn't change. The song you heard last will be the next song to
+play.
+
+If the hostname is omitted, the MPD_HOST environment variable will be used.
+
+=head1 AUTHOR
+
+Copyright 2007 Joey Hess <joey@kitenet.net>
+
+Licensed under the GNU GPL version 2 or higher.
+
+http://kitenet.net/~joey/code/mpdtoys
+
+=cut
+
+if (@ARGV) {
+ $ENV{MPD_HOST}=shift;
+}
+
+my $mpd=Audio::MPD->new(conntype => "reuse");
+my $pl=$mpd->playlist;
+my $current=$mpd->current;
+if (! $current) {
+ die "playlist seems to be empty\n";
+}
+my $pos=$mpd->current->pos;
+my $id=$mpd->current->id;
+my $past=0;
+my @list=$pl->as_items;
+foreach my $song (reverse @list) {
+ if ($past) {
+ $pl->moveid($song->id, $#list);
+ }
+ elsif ($id eq $song->id) {
+ $past=1;
+ }
+ else {
+ $pl->moveid($song->id, $pos++);
+ }
+}
diff --git a/mprompt b/mprompt
new file mode 100755
index 0000000..c58cc75
--- /dev/null
+++ b/mprompt
@@ -0,0 +1,370 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use Audio::MPD q{0.19.0};
+use MpdToys;
+use Getopt::Long;
+use Term::ReadKey;
+use Encode;
+
+=head1 NAME
+
+mprompt - simple prompt-based control for mpd
+
+=head1 SYNOPSIS
+
+mpompt [-s] [-m key=key] [-t n] [-f] [tty] [-T] [host]
+
+=cut
+
+sub usage {
+ die "Usage: mprompt [-s] [-m key=key] [-t n] [-f] [-t] [tty] [host]\n";
+}
+
+=head1 DESCRIPTION
+
+B<mprompt> is a mpd client with a prompt-based interface. It is
+designed to be usable on a headless machine.
+
+At the prompt, enter the name of a playlist, or part of the name of an
+album, artist, or song. Matching items will start playing. You can also
+paste in urls to stream.
+
+(If the perl String::Approx module is available, it will be used to handle
+typos, etc in the names you enter.)
+
+Use the left and right arrow keys to adjust volume, and the up and down
+arrow keys to move through the playlist.
+
+The Tab and Enter keys can both be used to pause and unpause playback.
+(Enter toggles pause only if nothing has been entered at the prompt.)
+
+Example of how to run mprompt in /etc/inittab:
+
+ 1:2345:respawn:/usr/bin/mprompt /dev/tty1
+
+=head1 OPTIONS
+
+=over 4
+
+=item -s
+
+This option allows shell commands to be typed in to mprompt, to be
+run by whatever user it is running as. (Typically root if it is run from
+/etc/inittab).
+
+To enter a shell command, type a "!", followed by the command to run,
+followed by Enter.
+
+=item -m key=key
+
+This option allows remapping keys. Any key can be remapped to any other
+key, which is useful to support keyboard with unusual key layouts, or
+missing keys.
+
+For alphanumeric and punctuation keys, individual symbols can be remapped.
+For example, "-m a=b" will turn each entered "a" into "b".
+
+For other keys, use the following names:
+
+=over 4
+
+=item <return>
+
+=item <tab>
+
+=item <space>
+
+=item <up>
+
+=item <down>
+
+=item <left>
+
+=item <right>
+
+=item <backspace>
+
+=back
+
+For example, -m "n=<down>" will map the "n" key to the down arrow, causing
+that key to change to the next track; -m "<space>=<return>" will make the space
+bar act as a pause.
+
+It's possible to swap keys too. For example, -m "<down>=<up>" -m "<up>=<down>"
+
+A single key can also be bound to a series of keystrokes. For example,
+-m "1=Mule Variations<return>" will cause the "1" key to play the "Mule
+Variations" album, a nice choice.
+
+=item -t n
+
+Adds a timeout, a specified number of seconds after which the entry
+on the command line will be cleared. Useful for headless systems, to avoid
+cat-on-keyboard confusing your later commands.
+
+=item -T
+
+Enables terse output mode. This mode tries to avoid displaying excessive
+or complex things, with the intent that mprompt's output can be piped into
+a speech synthesiser, such as espeak.
+
+=back
+
+=head1 SEE ALSO
+
+vipl(1) mptoggle(1) mpd(1)
+
+=head1 AUTHOR
+
+Copyright 2009 Joey Hess <joey@kitenet.net>
+
+Licensed under the GNU GPL version 2 or higher.
+
+http://kitenet.net/~joey/code/mpdtoys
+
+=cut
+
+my $tty;
+my $shell=0;
+my $timeout=0;
+my $terse=0;
+my %controlchars = GetControlChars;
+my %keysyms = (
+ "\n" => '<return>',
+ "\t" => '<tab>',
+ " " => '<space>',
+ "\e[A" => '<up>',
+ "\e[3~" => '<up>', # delete on some terminals, raw on others
+ "\e[B" => '<down>',
+ "\e[D" => '<left>',
+ "\e[C" => '<right>',
+ $controlchars{ERASE} => "<backspace>",
+);
+my %keymap;
+
+Getopt::Long::Configure("no_ignore_case");
+GetOptions(
+ "s" => \$shell,
+ "m=s" => sub {
+ my ($old, $new)=split(/=/, $_[1], 2);
+ $keymap{$old}=$new;
+ },
+ "t=i" => \$timeout,
+ "f" => sub { print STDERR "the -f option is now enabled by default\n" },
+ "T" => \$terse,
+) || usage();
+
+if (@ARGV) {
+ $tty=shift;
+ close STDIN;
+ close STDOUT;
+ open(STDIN, "<", $tty) || die "open $tty: $!";
+ open(STDOUT, ">", $tty) || die "open $tty: $!";
+}
+if (@ARGV) {
+ $ENV{MPD_HOST}=shift;
+}
+my $mpd=Audio::MPD->new(conntype => "reuse");
+
+sub quit {
+ ReadMode("restore");
+ exit(0);
+};
+$SIG{INT}=$SIG{TERM}=\&quit;
+ReadMode("raw");
+
+$|=1;
+
+my $line="";
+my $sequence;
+my $laststroke=time;
+
+showprompt();
+
+KEY: while (my $key = ReadKey(0)) {
+ if ($timeout) {
+ if (length $line && time - $laststroke > $timeout) {
+ $line="";
+ print " <timeout>\n" unless $terse;
+ showprompt();
+ }
+ $laststroke=time;
+ }
+
+ if ($key eq $controlchars{INTERRUPT} ||
+ $key eq $controlchars{EOF}) {
+ quit();
+ }
+
+ # Sequences are started with escape, and accumulated
+ # until a recognised sequence is seen, or until it becomes clear
+ # that it is not part of a recognised sequence.
+ if (defined $sequence) {
+ $sequence.=$key;
+ if (exists $keysyms{$sequence}) {
+ $key=$sequence;
+ $sequence=undef;
+ }
+ else {
+ foreach my $sym (keys %keysyms) {
+ if ($sym=~/^\Q$sequence\E/) {
+ next KEY; # unfinished sequence
+ }
+ }
+ $key=$sequence;
+ $sequence=undef;
+ }
+ }
+
+ $key = $keysyms{$key} if exists $keysyms{$key};
+ $key = $keymap{$key} if exists $keymap{$key};
+
+ # The key may be mapped to a multiple letter sequence.
+ while (length $key) {
+ if ($key=~s/^(<[^>]+>)//) {
+ handle($1);
+ }
+ elsif ($key=~s/(.)//) {
+ handle($1);
+ }
+ }
+}
+
+sub handle {
+ my $key=shift;
+
+ if ($key eq "\e") {
+ $sequence=$key;
+ }
+ elsif ($key eq '<return>') {
+ if ($shell && $line =~ /^\!(.*)/) {
+ print "\nrunning $1\n";
+ system($1);
+ }
+ elsif (length $line && $line !~ /^\s*$/) {
+ queue($line);
+ }
+ else {
+ toggle();
+ }
+ $line="";
+ showprompt();
+ }
+ elsif ($key eq '<backspace>') {
+ if (length $line) {
+ chop $line;
+ print "\b \b";
+ }
+ }
+ elsif ($key eq '<space>') {
+ print " ";
+ $line.=" ";
+ }
+ elsif ($key eq '<tab>') {
+ toggle();
+ showprompt();
+ }
+ elsif ($key eq '<left>') {
+ adjustvolume(-5);
+ }
+ elsif ($key eq '<right>') {
+ adjustvolume(+5);
+ }
+ elsif ($key eq '<up>') {
+ $mpd->prev;
+ $mpd->play;
+ showplaying();
+ showprompt();
+ }
+ elsif ($key eq '<down>') {
+ $mpd->next;
+ $mpd->play;
+ showplaying();
+ showprompt();
+ }
+ else {
+ print "$key";
+ $line.=$key;
+ }
+}
+
+sub adjustvolume {
+ my $amount=shift;
+ my $vol=$mpd->status->volume;
+
+ $vol+=$amount;
+ if ($vol > 100) {
+ $vol=100;
+ }
+ elsif ($vol < 0) {
+ $vol=0;
+ }
+
+ if (! $terse) {
+ print "\nvolume: $vol%\n";
+ }
+ $mpd->volume($vol);
+ showprompt();
+}
+
+sub showprompt {
+ print "> $line";
+}
+
+sub showplaying {
+ print "\n";
+ my $song=$mpd->current;
+ if (! defined $song) {
+ print "nothing queued\n";
+ }
+ else {
+ if (! $terse) {
+ print encode_utf8($song->as_string)."\n";
+ }
+ }
+}
+
+sub toggle {
+ $mpd->pause;
+ my $state=$mpd->status->state;
+ print "\n";
+ print "$state\n" if ! $terse || $state ne "play";
+}
+
+sub queue {
+ my $line=shift;
+
+ print "\n";
+
+ my $pl=$mpd->playlist;
+
+ eval q{$pl->load($line)};
+ if (! $@) {
+ $pl->clear;
+ $pl->load($line);
+ $mpd->play;
+
+ print "added $line playlist";
+ showplaying();
+ return;
+ }
+
+ my @matches=MpdToys::findmatchingsongs($line, $mpd);
+ if (! @matches && MpdToys::canmatch_fuzzy()) {
+ print "trying fuzzy match..\n";
+ @matches=MpdToys::findmatchingsongs_fuzzy($line, $mpd);
+ }
+ if (@matches) {
+ $pl->clear;
+ foreach (@matches) {
+ $pl->add($_->file);
+ }
+ $mpd->play;
+ print "added ".int(@matches)." songs";
+ showplaying();
+ }
+ else {
+ print "no matches found for \"$line\"\n";
+ }
+}
diff --git a/mpskip b/mpskip
new file mode 100755
index 0000000..5caa8d8
--- /dev/null
+++ b/mpskip
@@ -0,0 +1,90 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use Audio::MPD q{0.19.0};
+use Time::HiRes qw(usleep);
+
+=head1 NAME
+
+mpskip - emulate a skipping record
+
+=head1 SYNOPSIS
+
+mpskip [num_skips] [duration] [skippos] [host]
+
+=head1 DESCRIPTION
+
+B<mpskip> makes mpd emulate a skipping record.
+
+If B<num_skips> is not specified (or is 0), it will skip forever (or until
+someone manually seeks past the "bad" part of the song, or changes songs).
+
+The B<duration> is how long the skip should be, in seconds. Floating point
+values can be used.
+
+The B<skippos> is the number of seconds into the song to place the skip point.
+Default is the current play position.
+
+If the hostname is omitted, the MPD_HOST environment variable will be used.
+
+=head1 LIMITATIONS
+
+Doesn't insert the pop you hear on a real record player when the needle
+skips.
+
+If run against a remote host, it may not skip at exactly the same place
+each time due to network issues.
+
+=head1 AUTHOR
+
+Copyright 2007 Joey Hess <joey@kitenet.net>
+
+Licensed under the GNU GPL version 2 or higher.
+
+http://kitenet.net/~joey/code/mpdtoys
+
+=cut
+
+my $num_skips=0;
+if (@ARGV && $ARGV[0] =~ /^[0-9]+$/) {
+ $num_skips=shift;
+}
+
+my $duration=1;
+if (@ARGV && $ARGV[0] =~ /^[0-9.]+$/) {
+ $duration=shift;
+}
+
+my $skippos;
+if (@ARGV && $ARGV[0] =~ /^[0-9]+$/) {
+ $skippos=shift;
+}
+
+if (@ARGV) {
+ $ENV{MPD_HOST}=shift;
+}
+
+my $mpd=Audio::MPD->new(conntype => "reuse");
+if ($mpd->status->state ne 'play') {
+ die "error: mpd has to be playing first\n";
+}
+
+my $songid=$mpd->current->id;
+$skippos=$mpd->status->time->seconds_sofar unless defined $skippos;
+
+print "skipping".($num_skips ? " $num_skips times" : "").
+ " at position $skippos for $duration seconds\n";
+
+while (usleep $duration * 1000000 / 2 ) {
+ my $status=$mpd->status;
+ my $pos=$status->time->seconds_sofar;
+ exit if $status->state ne 'play';
+ exit if $mpd->current->id ne $songid;
+ exit if $pos > $skippos + $duration + 1;
+ next if $pos <= $skippos;
+ $mpd->seek($skippos);
+ if ($num_skips) {
+ $num_skips--;
+ exit unless $num_skips;
+ }
+}
diff --git a/mpstore b/mpstore
new file mode 100755
index 0000000..a482fab
--- /dev/null
+++ b/mpstore
@@ -0,0 +1,272 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use Audio::MPD q{0.19.0};
+use Data::Dumper;
+
+=head1 NAME
+
+mpstore - store and transfer mpd state between daemons
+
+=head1 SYNOPSIS
+
+mpstore [host] > file
+
+mpload [host] < file
+
+mpcp [src] dest
+
+mpmv [src] dest
+
+mpswap [A] B
+
+=head1 DESCRIPTION
+
+These commands allow saving, loading, and transferring state between mpd
+daemons running on different hosts.
+
+B<mpstore> dumps a daemon's state to stdout.
+
+B<mpload> loads a state dump from stdin and sends it to a daemon.
+
+B<mpcp> copies the state from the src daemon to the dest daemon, causing it
+to begin to play the same song as the src daemon, at the same position.
+
+B<mpmv> moves the state, so the dest daemon is left playing what the src
+daemon was playing, and the src daemon is paused.
+
+B<mpswap> exchanges the state of daemons A and B, swapping what they're
+playing.
+
+The first hostname passed to each command can be omitted, if it is then
+the MPD_HOST environment variable will be used. Like the MPD_HOST variable,
+the hostname can be of the form "password@hostname" to specify a password.
+If any hostname is "-", the MPD_HOST setting will be used.
+
+The full list of state that is handled is:
+
+=over
+
+=item the contents of the playlist
+
+=item the playback state (playing, paused, stopped)
+
+=item the currently playing song
+
+=item the position within the playing song
+
+=item the volume control
+
+=item the repeat, random, and cross fade settings
+
+=back
+
+=head1 LIMITATIONS
+
+The host that state is transferred to must have the playing song available
+in its library, with the same filename. It's ok if some other songs in the
+playlist are not available; such songs will be skipped.
+
+B<mpcp> cannot perfectly synchronise playback between the two daemons.
+Network latency and timing prevent this. It should manage better than 0.5
+second accuracy. If you need better accuracy of synchronised playback,
+you should probably use Pulse Audio.
+
+=head1 BUGS
+
+The file format is not the same that mpd uses for saving its own state,
+which would be nice.
+
+=head1 AUTHOR
+
+Copyright 2007 Joey Hess <joey@kitenet.net>
+
+Licensed under the GNU GPL version 2 or higher.
+
+http://kitenet.net/~joey/code/mpdtoys
+
+=cut
+
+# Allow "-" to be specified as a host, it will use MPD_HOST then.
+my $real_MPD_HOST=$ENV{MPD_HOST};
+my $prev_host;
+sub sethost {
+ my $host=shift;
+ if ($host eq "-") {
+ $host=$real_MPD_HOST;
+ if (! defined $host) {
+ die "error: MPD_HOST is not set, cannot use '-'\n";
+ }
+ }
+ if (defined $prev_host && $host eq $prev_host) {
+ die "error: src and dest hosts cannot be the same\n";
+ }
+
+ $ENV{MPD_HOST}=$prev_host=$host;
+}
+
+if ($0=~/mpswap/) {
+ if (@ARGV == 2) {
+ sethost(shift);
+ }
+ if (! @ARGV) {
+ die "error: not enough hosts specified\n";
+ }
+ my $a=Audio::MPD->new(conntype => "reuse");
+ sethost(shift);
+ my $b=Audio::MPD->new(conntype => "reuse");
+
+ my $asnap=snapshot($a, 1);
+ my $bsnap=snapshot($b, 1);
+ transfer($asnap, $b);
+ transfer($bsnap, $a);
+}
+elsif ($0=~/mpstore/) {
+ if (@ARGV) {
+ sethost(shift);
+ }
+
+ my $src=Audio::MPD->new(conntype => "reuse");
+
+ my $snap=snapshot($src, 0);
+ delete $snap->{src}; # don't dump this object
+ $Data::Dumper::Terse=1;
+ $Data::Dumper::Indent=1;
+ print Dumper($snap);
+}
+elsif ($0=~/mpload/) {
+ if (@ARGV == 2) {
+ sethost(shift);
+ }
+
+ my $dest=Audio::MPD->new(conntype => "reuse");
+ my $code;
+ {
+ local $/=undef;
+ $code=<>;
+ }
+ my $snap=eval $code;
+ if ($@ || ! ref $snap) {
+ die "error: failed to parse stdin ($@)\n";
+ }
+ transfer($snap, $dest);
+}
+else {
+ if (@ARGV == 2) {
+ sethost(shift);
+ }
+
+ if (! @ARGV) {
+ die "error: not enough hosts specified\n";
+ }
+ my $src=Audio::MPD->new(conntype => "reuse");
+ sethost(shift);
+ my $dest=Audio::MPD->new(conntype => "reuse");
+
+ my $snap=snapshot($src, 1);
+ transfer($snap, $dest);
+}
+
+sub snapshot {
+ my $mpd=shift;
+ my $pause=shift;
+
+ my $status=$mpd->status;
+ my $state=$status->state;
+ my $current=$mpd->current;
+ if ($pause && $state eq 'play') {
+ $mpd->pause;
+ }
+
+ my @playlist;
+ foreach my $song ($mpd->playlist->as_items) {
+ push @playlist, $song->file;
+ }
+
+ return {
+ src => $mpd,
+ state => $state,
+ repeat => $status->repeat,
+ volume => $status->volume,
+ pos => $status->time ? $status->time->seconds_sofar : 0,
+ xfade => $status->xfade,
+ current => defined $current ? $mpd->current->file : undef,
+ playlist => \@playlist,
+ };
+}
+
+sub transfer {
+ my $snap=shift;
+ my $dest=shift;
+
+ if (ref $snap->{playlist} eq 'ARRAY') {
+ # Feed playlist to dest.
+ $dest->playlist->clear;
+ eval {
+ $dest->playlist->add(@{$snap->{playlist}});
+ };
+ if ($@) {
+ # Try doing it a song at a time, in case only some
+ # songs are available.
+ foreach my $song (@{$snap->{playlist}}) {
+ eval {
+ $dest->playlist->add($song);
+ };
+ if ($@) {
+ print STDERR "warning: failed to add song to playlist ($song)\n";
+ }
+ }
+ }
+ }
+
+ # Set misc settings.
+ $dest->repeat($snap->{repeat}) if exists $snap->{repeat};
+ $dest->random($snap->{random}) if exists $snap->{random};
+ $dest->volume($snap->{volume}) if exists $snap->{volume};
+ $dest->fade($snap->{xfade}) if exists $snap->{xfade};
+
+ # Figure out the id of the song to play on dest.
+ my $id;
+ if (exists $snap->{current} && defined $snap->{current}) {
+ foreach my $song ($dest->playlist->as_items) {
+ if ($song->file eq $snap->{current}) {
+ $id=$song->id;
+ }
+ }
+ if (! defined $id) {
+ print STDERR "error: cannot find currently playing song (".
+ $snap->{current}.") on dest playlist\n";
+ return;
+ }
+ }
+
+ # Seek and set play state.
+ $dest->seekid($snap->{pos}, $id) if exists $snap->{pos} && defined $id;
+ if (exists $snap->{state}) {
+ if ($snap->{state} eq 'play') {
+ if ($0 =~ /mpcp/) {
+ # mpd only provides second accuracy, so src
+ # and dest are probably a fraction of a second
+ # off. For a more accurate copy of the state,
+ # seek the *src* back to the start of the
+ # current second too.
+ # Of course, this isn't perfect, due to
+ # network latency, etc.
+ $snap->{src}->seek($snap->{pos});
+ $snap->{src}->play;
+ $dest->play;
+ }
+ else {
+ $dest->play;
+ }
+ }
+ elsif ($snap->{state} eq 'pause') {
+ $dest->pause;
+ }
+ elsif ($snap->{state} eq 'stop') {
+ $dest->stop;
+ }
+ }
+
+ return 1;
+}
diff --git a/mpswap b/mpswap
new file mode 120000
index 0000000..5f7d564
--- /dev/null
+++ b/mpswap
@@ -0,0 +1 @@
+mpstore \ No newline at end of file
diff --git a/mptoggle b/mptoggle
new file mode 100755
index 0000000..60888e2
--- /dev/null
+++ b/mptoggle
@@ -0,0 +1,65 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use Audio::MPD q{0.19.0};
+
+=head1 NAME
+
+mptoggle - single button control for mpd
+
+=head1 SYNOPSIS
+
+mptoggle [host] [playlist ...]
+
+=head1 DESCRIPTION
+
+B<mptoggle> allows mpd to be controlled by a single physical button. It
+was designed for a linksys nslu2 running mpd, for which the only available
+physical control is a power button that can be remapped to run an arbitrary
+program.
+
+B<mptoggle> toggles playback each time it's run. So press the button once
+to start, and a second time to stop. Each time it starts playing. By
+default it selects a new different playlist, at random, to play. If
+playlist names are specified at the command line, it will choose between
+those at random.
+
+Example of how to make the nslu2's power button run mptoggle, in
+/etc/inittab:
+
+ ca:12345:ctrlaltdel:/usr/bin/mptoggle localhost
+
+=head1 SEE ALSO
+
+mprompt(1) mpd(1)
+
+=head1 AUTHOR
+
+Copyright 2007 Joey Hess <joey@kitenet.net>
+
+Licensed under the GNU GPL version 2 or higher.
+
+http://kitenet.net/~joey/code/mpdtoys
+
+=cut
+
+if (@ARGV) {
+ $ENV{MPD_HOST}=shift;
+}
+my $mpd=Audio::MPD->new(conntype => "reuse");
+
+if ($mpd->status->state ne 'play') {
+ $mpd->playlist->clear;
+ my @playlists;
+ if (@ARGV) {
+ @playlists=@ARGV;
+ }
+ else {
+ @playlists=$mpd->collection->all_playlists;
+ }
+ $mpd->playlist->load($playlists[rand @playlists]);
+ $mpd->play;
+}
+else {
+ $mpd->pause;
+}
diff --git a/sats b/sats
new file mode 100755
index 0000000..67c2ed2
--- /dev/null
+++ b/sats
@@ -0,0 +1,89 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use Audio::MPD q{0.19.0};
+use Getopt::Long;
+
+=head1 NAME
+
+sats - mpd stop after this song
+
+=head1 SYNOPSIS
+
+sats [-d] [-n num] [host]
+
+=head1 DESCRIPTION
+
+B<sats> is an acronym for Stop After This Song. It will wait for playback
+of the currently playing song to finish, and then tell mpd to stop playing.
+
+If the hostname is omitted, the MPD_HOST environment variable will be used.
+
+=head1 OPTIONS
+
+=over 4
+
+=item -d
+
+Daemonize rather than waiting in the foreground for the song to stop playing.
+
+=item -n num
+
+Stop after B<num> songs (default is 1).
+
+=back
+
+=head1 AUTHOR
+
+Copyright 2007 Joey Hess <joey@kitenet.net>
+
+Licensed under the GNU GPL version 2 or higher.
+
+http://kitenet.net/~joey/code/mpdtoys
+
+=cut
+
+my $num=1;
+my $daemon=0;
+GetOptions(
+ "n=i" => \$num,
+ "d" => \$daemon,
+) || usage();
+
+sub usage {
+ die "Usage: sats [-d] [-n num] [host]\n";
+}
+
+if (@ARGV) {
+ $ENV{MPD_HOST}=shift;
+}
+
+my $mpd=Audio::MPD->new(conntype => "reuse");
+
+my $song=$mpd->current;
+if ($mpd->status->state ne 'play') {
+ die "no song is currently playing\n";
+}
+
+if ($daemon) {
+ eval q{use Proc::Daemon};
+ Proc::Daemon::Init();
+ # daemonising closed the connection to mpd
+ $mpd=Audio::MPD->new(conntype => "reuse");
+}
+
+# Polling is evil, it could look at the seek position, and sleep until the
+# end. But that would break if something seeked in the song..
+
+while (sleep 1) {
+ last if $mpd->status->state ne 'play';
+ my $current=$mpd->current;
+ if ($current->id != $song->id) {
+ $num--;
+ if ($num == 0) {
+ $mpd->stop;
+ exit;
+ }
+ $song=$current;
+ }
+}
diff --git a/vipl b/vipl
new file mode 100755
index 0000000..903265c
--- /dev/null
+++ b/vipl
@@ -0,0 +1,161 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+use File::Spec;
+use File::Temp;
+use Audio::MPD q{0.19.0};
+use MpdToys;
+use Encode;
+
+=head1 NAME
+
+vipl - edit mpd playlist
+
+=head1 SYNOPSIS
+
+B<vipl> [host]
+
+=head1 DESCRIPTION
+
+vipl allows editing the mpd playlist using your text editor. The current
+playlist will be brought up in the editor. Delete or rearrange songs as
+desired using the editor.
+
+You can also enter the name of a playlist, or part of the name of an
+album, artist, or song. Matching items will be added to the playlist.
+Streaming urls can also be entered.
+
+(If the perl String::Approx module is available, it will be used to handle
+typos, etc in the names you enter.)
+
+The currently playing song is marked with a ">" at the front. To change
+which song is playing, just move the ">" to a different song.
+
+If the hostname is omitted, the MPD_HOST environment variable will be used.
+
+=head1 AUTHOR
+
+Copyright 2008 by Joey Hess <joey@kitenet.net>
+
+Licensed under the GNU GPL.
+
+=cut
+
+if (@ARGV) {
+ $ENV{MPD_HOST}=shift;
+}
+my $mpd=Audio::MPD->new(conntype => "reuse");
+my $current=$mpd->current;
+my $pl=$mpd->playlist;
+my %names=map { $_->as_string => $_ } $pl->as_items;
+my @origlist=$pl->as_items;
+
+my $tmp=File::Temp->new(TEMPLATE => "plXXXXX", DIR => File::Spec->tmpdir);
+open (OUT, ">".$tmp->filename) || die "$0: cannot create ".$tmp->filename.": $!\n";
+foreach my $song (@origlist) {
+ print OUT ">" if defined $current && $song->id eq $current->id;
+ print OUT encode_utf8($song->as_string);
+ print OUT "\n";
+}
+close OUT || die "$0: cannot write ".$tmp->filename.": $!\n";
+
+my @editor="vi";
+if (-x "/usr/bin/editor") {
+ @editor="/usr/bin/editor";
+}
+if (exists $ENV{EDITOR}) {
+ @editor=split(' ', $ENV{EDITOR});
+}
+if (exists $ENV{VISUAL}) {
+ @editor=split(' ', $ENV{VISUAL});
+}
+my $ret=system(@editor, $tmp);
+if ($ret != 0) {
+ die "@editor exited nonzero, aborting\n";
+}
+
+my $changed=0;
+my @list;
+open (IN, "<".$tmp->filename) || die "$0: cannot read ".$tmp->filename.": $!\n";
+my $playing;
+my $num=0;
+while (<IN>) {
+ chomp;
+ $_=decode_utf8($_);
+
+ if (s/^>\s*//) {
+ $playing=$num;
+ }
+
+ if (exists $names{$_}) {
+ push @list, $names{$_};
+ if (! $changed && !($origlist[$#list] &&
+ $names{$_}->file eq $origlist[$#list]->file)) {
+ $changed=1;
+ }
+ $num++;
+ }
+ else {
+ next if /^\s*$/;
+
+ my @matches=MpdToys::findmatchingsongs($_, $mpd);
+ if (! @matches && MpdToys::canmatch_fuzzy()) {
+ @matches=MpdToys::findmatchingsongs_fuzzy($_, $mpd);
+ }
+
+ if (! @matches) {
+ print STDERR "$0: no matches for \"$_\"\n";
+ }
+ else {
+ foreach (@matches) {
+ push @list, $_;
+ }
+ $changed=1;
+ $num+=@matches;
+ }
+ }
+}
+close IN;
+if ($#list != $#origlist) {
+ $changed=1;
+}
+
+if (! $changed) {
+ # yay for optimisation!
+}
+elsif ($current && grep { $_->file eq $current->file } @list) {
+ # Avoid touching the currently playing song, while changing
+ # the playlist around it.
+ $pl->crop;
+ my $pos=0;
+ my $past=0;
+ foreach my $song (@list) {
+ if (! $past && $song->file eq $current->file) {
+ $past=1;
+ # move current song into right location
+ $pl->move(0, $pos);
+ }
+ else {
+ $pl->add($song->file);
+ }
+ $pos++;
+ }
+ $mpd->play if @list;
+}
+else {
+ $pl->clear;
+ foreach my $song (@list) {
+ $pl->add($song->file);
+ }
+}
+
+if (defined $playing) {
+ if (! defined $mpd->status->song || $playing ne $mpd->status->song ||
+ $mpd->status->state ne 'play') {
+ $mpd->play($playing);
+ }
+}
+else {
+ $mpd->play(0) if @list;
+}