pax_global_header00006660000000000000000000000064112600163550014511gustar00rootroot0000000000000052 comment=7c666e022d0ad7077f95beb165fc13e3dd87045c mpdtoys-0.20/000075500000000000000000000000001126001635500131275ustar00rootroot00000000000000mpdtoys-0.20/Makefile000064400000000000000000000012271126001635500145710ustar00rootroot00000000000000progs=mpstore mprev mpgenplaylists mpfade sats mpskip mptoggle mprand \ mpinsert vipl mprompt 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) mpdtoys-0.20/MpdToys.pm000064400000000000000000000021641126001635500150670ustar00rootroot00000000000000#!/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 mpdtoys-0.20/TODO000064400000000000000000000003411126001635500136150ustar00rootroot00000000000000mprm: 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. mpdtoys-0.20/debian/000075500000000000000000000000001126001635500143515ustar00rootroot00000000000000mpdtoys-0.20/debian/changelog000064400000000000000000000107251126001635500162300ustar00rootroot00000000000000mpdtoys (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 mpdtoys-0.20/debian/compat000064400000000000000000000000021126001635500155470ustar00rootroot000000000000007 mpdtoys-0.20/debian/control000064400000000000000000000020361126001635500157550ustar00rootroot00000000000000Source: mpdtoys Section: sound Priority: optional Build-Depends: debhelper (>= 7), dpkg-dev (>= 1.9.0) Maintainer: Joey Hess Standards-Version: 3.8.3 Homepage: http://kitenet.net/~joey/code/mpdtoys/ Vcs-Git: git://git.kitenet.net/mpdtoys Package: mpdtoys Architecture: all Depends: perl, libaudio-mpd-perl (>= 0.19.0), ${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. mpdtoys-0.20/debian/copyright000064400000000000000000000002541126001635500163050ustar00rootroot00000000000000Files: * 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. mpdtoys-0.20/debian/docs000064400000000000000000000000051126001635500152170ustar00rootroot00000000000000TODO mpdtoys-0.20/debian/rules000075500000000000000000000002151126001635500154270ustar00rootroot00000000000000#!/usr/bin/make -f %: dh $@ # Not intended for use by anyone except the author. announcedir: @echo ${HOME}/src/joeywiki/code/mpdtoys/news mpdtoys-0.20/mpcp000077700000000000000000000000001126001635500154222mpstoreustar00rootroot00000000000000mpdtoys-0.20/mpfade000075500000000000000000000052171126001635500143160ustar00rootroot00000000000000#!/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 => $Audio::MPD::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); } } } mpdtoys-0.20/mpgenplaylists000075500000000000000000000041321126001635500161300ustar00rootroot00000000000000#!/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 mpdtoys-0.20/mpinsert000075500000000000000000000040151126001635500147160ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use Audio::MPD q{0.19.0}; use MpdToys; =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 =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; GetOptions( "n" => \$shownum, ) || usage(); sub usage { die "Usage: mpinsert [-n] [song ...]\n"; } my @list=@ARGV; if (! @list) { while (<>) { chomp; push @list, $_; } } my $mpd=Audio::MPD->new(conntype => $Audio::MPD::REUSE); my $pl=$mpd->playlist; @list=reverse @list if $mpd->current; my @out; 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); } push @out, ($pos+1)."\n" if $shownum; } } if ($shownum) { @out=reverse @out if $mpd->current; print $out[0]; } mpdtoys-0.20/mpload000077700000000000000000000000001126001635500157372mpstoreustar00rootroot00000000000000mpdtoys-0.20/mpmv000077700000000000000000000000001126001635500154422mpstoreustar00rootroot00000000000000mpdtoys-0.20/mprand000075500000000000000000000012621126001635500143370ustar00rootroot00000000000000#!/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 => $Audio::MPD::REUSE); $mpd->playlist->clear; my @playlists=$mpd->collection->all_playlists; $mpd->playlist->load($playlists[rand @playlists]); $mpd->play; mpdtoys-0.20/mprev000075500000000000000000000020051126001635500142030ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use Audio::MPD q{0.19.0}; use Data::Dumper; =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 => $Audio::MPD::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++); } } mpdtoys-0.20/mprompt000075500000000000000000000157101126001635500145570ustar00rootroot00000000000000#!/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 => $Audio::MPD::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"; } } mpdtoys-0.20/mpskip000075500000000000000000000037641126001635500143720ustar00rootroot00000000000000#!/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 => $Audio::MPD::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; } } mpdtoys-0.20/mpstore000075500000000000000000000141661126001635500145560ustar00rootroot00000000000000#!/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 => $Audio::MPD::REUSE); sethost(shift); my $b=Audio::MPD->new(conntype => $Audio::MPD::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 => $Audio::MPD::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 => $Audio::MPD::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 => $Audio::MPD::REUSE); sethost(shift); my $dest=Audio::MPD->new(conntype => $Audio::MPD::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->seconds_sofar, 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; } mpdtoys-0.20/mpswap000077700000000000000000000000001126001635500157722mpstoreustar00rootroot00000000000000mpdtoys-0.20/mptoggle000075500000000000000000000025761126001635500147050ustar00rootroot00000000000000#!/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 => $Audio::MPD::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; } mpdtoys-0.20/sats000075500000000000000000000031051126001635500140260ustar00rootroot00000000000000#!/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 => $Audio::MPD::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 => $Audio::MPD::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; } } mpdtoys-0.20/vipl000075500000000000000000000065721126001635500140410ustar00rootroot00000000000000#!/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 => $Audio::MPD::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; }