From 44a3d2d1b9f97def795cbe8ab846d2a1593cfb64 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Mon, 3 Jan 2011 09:49:13 -0800 Subject: 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 --- Makefile | 23 ++++ MpdToys.pm | 62 ++++++++++ TODO | 4 + debian/changelog | 175 ++++++++++++++++++++++++++ debian/compat | 1 + debian/control | 24 ++++ debian/copyright | 5 + debian/docs | 1 + debian/rules | 7 ++ mpcp | 1 + mpfade | 118 ++++++++++++++++++ mpgenplaylists | 84 +++++++++++++ mpinsert | 123 ++++++++++++++++++ mplength | 44 +++++++ mpload | 1 + mpmv | 1 + mprand | 38 ++++++ mprandomwalk | 62 ++++++++++ mprev | 56 +++++++++ mprompt | 370 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ mpskip | 90 ++++++++++++++ mpstore | 272 ++++++++++++++++++++++++++++++++++++++++ mpswap | 1 + mptoggle | 65 ++++++++++ sats | 89 +++++++++++++ vipl | 161 ++++++++++++++++++++++++ 26 files changed, 1878 insertions(+) create mode 100644 Makefile create mode 100644 MpdToys.pm create mode 100644 TODO create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/docs create mode 100755 debian/rules create mode 120000 mpcp create mode 100755 mpfade create mode 100755 mpgenplaylists create mode 100755 mpinsert create mode 100755 mplength create mode 120000 mpload create mode 120000 mpmv create mode 100755 mprand create mode 100755 mprandomwalk create mode 100755 mprev create mode 100755 mprompt create mode 100755 mpskip create mode 100755 mpstore create mode 120000 mpswap create mode 100755 mptoggle create mode 100755 sats create mode 100755 vipl 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 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 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 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 Wed, 25 Nov 2009 06:43:25 -0500 + +mpdtoys (0.20) unstable; urgency=low + + * Work around bug #548305. + + -- Joey Hess 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 Tue, 22 Sep 2009 21:40:06 -0400 + +mpdtoys (0.18) unstable; urgency=low + + * mprompt: Configure getopt to not ignore case. + + -- Joey Hess 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 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 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 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 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 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 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 Sat, 28 Feb 2009 19:35:18 -0500 + +mpdtoys (0.10) unstable; urgency=low + + * Fix some bad man page synopses. Closes: #515304 + + -- Joey Hess 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 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 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 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 Sat, 19 Jan 2008 17:28:02 -0500 + +mpdtoys (0.5) unstable; urgency=low + + * mpskip: Fix mispaste in synopsis. Closes: #457553 + + -- Joey Hess 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 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 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 Fri, 14 Dec 2007 23:39:54 -0500 + +mpdtoys (0.1) unstable; urgency=low + + * First release. + + -- Joey Hess 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 +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 +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 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 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 (defaults to a tenth of what it was +at the start). Then it stops playing. + +B 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 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 + +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 + +< 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 + +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 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 + +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 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 + +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 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 + +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 plays random segments of randomly chosen songs in the +playlist. + +The B 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 + +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 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 + +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 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 + +=item + +=item + +=item + +=item + +=item + +=item + +=item + +=back + +For example, -m "n=" will map the "n" key to the down arrow, causing +that key to change to the next track; -m "=" will make the space +bar act as a pause. + +It's possible to swap keys too. For example, -m "=" -m "=" + +A single key can also be bound to a series of keystrokes. For example, +-m "1=Mule Variations" 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 + +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" => '', + "\t" => '', + " " => '', + "\e[A" => '', + "\e[3~" => '', # delete on some terminals, raw on others + "\e[B" => '', + "\e[D" => '', + "\e[C" => '', + $controlchars{ERASE} => "", +); +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 " \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 '') { + if ($shell && $line =~ /^\!(.*)/) { + print "\nrunning $1\n"; + system($1); + } + elsif (length $line && $line !~ /^\s*$/) { + queue($line); + } + else { + toggle(); + } + $line=""; + showprompt(); + } + elsif ($key eq '') { + if (length $line) { + chop $line; + print "\b \b"; + } + } + elsif ($key eq '') { + print " "; + $line.=" "; + } + elsif ($key eq '') { + toggle(); + showprompt(); + } + elsif ($key eq '') { + adjustvolume(-5); + } + elsif ($key eq '') { + adjustvolume(+5); + } + elsif ($key eq '') { + $mpd->prev; + $mpd->play; + showplaying(); + showprompt(); + } + elsif ($key eq '') { + $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 makes mpd emulate a skipping record. + +If B 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 is how long the skip should be, in seconds. Floating point +values can be used. + +The B 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 + +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 dumps a daemon's state to stdout. + +B loads a state dump from stdin and sends it to a daemon. + +B 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 moves the state, so the dest daemon is left playing what the src +daemon was playing, and the src daemon is paused. + +B 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 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 + +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 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 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 + +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 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 songs (default is 1). + +=back + +=head1 AUTHOR + +Copyright 2007 Joey Hess + +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 [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 + +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 () { + 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; +} -- cgit v1.2.3