summaryrefslogtreecommitdiff
path: root/mpstore
diff options
context:
space:
mode:
Diffstat (limited to 'mpstore')
-rwxr-xr-xmpstore272
1 files changed, 272 insertions, 0 deletions
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;
+}