diff options
Diffstat (limited to 'mpstore')
-rwxr-xr-x | mpstore | 272 |
1 files changed, 272 insertions, 0 deletions
@@ -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; +} |