# Copyright (C) 2011 Quentin Sculo <squentin@free.fr> # # This file is part of Gmusicbrowser. # Gmusicbrowser is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, as # published by the Free Software Foundation =gmbplugin MPRIS2 name MPRIS v2 title MPRIS v2 support desc Allows controlling gmusicbrowser via DBus using the MPRIS v2.0 standard req perl(Net::DBus, libnet-dbus-perl perl-Net-DBus) =cut package GMB::Plugin::MPRIS2; use strict; use warnings; use constant { OPT => 'PLUGIN_MPRIS2_', }; use Net::DBus::Annotation 'dbus_call_async'; my $bus=$GMB::DBus::bus; die "Requires DBus support to be active\n" unless $bus; #only requires this to use the hack in gmusicbrowser_dbus.pm so that Net::DBus::GLib is not required, else could do just : use Net::DBus::GLib; $bus=Net::DBus::GLib->session; my @Objects; sub Start { my $service= $bus->export_service('org.mpris.MediaPlayer2.gmusicbrowser'); push @Objects, GMB::DBus::MPRIS2->new($service); } sub Stop { ::UnWatch_all($_) for @Objects; $_->disconnect for @Objects; @Objects=(); } sub prefbox { my $vbox=Gtk2::VBox->new(0,2); my $blacklist= Gtk2::CheckButton->new(_"Show in sound menu"); soundmenu_button_update($blacklist); $blacklist->signal_connect(toggled => \&soundmenu_toggle_cb); $vbox->pack_start($blacklist,0,0,0); return $vbox; } sub soundmenu_button_update { my $check=shift; eval { my $service = $bus->get_service('com.canonical.indicators.sound'); my $object = $service->get_object('/com/canonical/indicators/sound/service'); my $asyncreply=$object->IsBlacklisted(dbus_call_async,'gmusicbrowser'); #called async, because it seems to trigger the calling of gmb DBus methods before replying $check->{busy}=1; $asyncreply->set_notify(sub { soundmenu_button_set($check, eval {$_[0]->get_result;}) }); }; soundmenu_button_set($check,undef) unless $check->{busy}; } sub soundmenu_button_set { my ($check,$on)=@_; $check->set_active(1) if !$on; if (!defined $on) { $check->set_sensitive(0); $check->set_tooltip_text(_"No sound menu found"); } delete $check->{busy}; } sub soundmenu_toggle_cb { my $check=shift; return if $check->{busy}; my $on=$check->get_active; eval { my $service = $bus->get_service('com.canonical.indicators.sound'); my $object = $service->get_object('/com/canonical/indicators/sound/service'); $object->BlacklistMediaPlayer('gmusicbrowser',!$on); }; soundmenu_button_update($check); } package GMB::DBus::MPRIS2; use base 'Net::DBus::Object'; use Net::DBus::Exporter 'org.mpris.MediaPlayer2'; use Net::DBus ':typing'; our %PropChanged; # events watched by properties of org.mpris.MediaPlayer2.Player that send PropertiesChanged signal # the functions associated with these properties must bless the return value with dbus_string() and friends my %PropertiesWatch= ( PlaybackStatus => 'Playing', LoopStatus => 'Lock Repeat', Shuffle => 'Sort', Metadata => 'CurSong', Volume => 'Vol', CanGoNext => 'Playlist Sort Queue Repeat', CanGoPrevious => 'CurSongID', CanPlay => 'CurSongID', #CanSeek => 'CurSongID', # always true currently => no need to watch event ); sub new { my ($class,$service) = @_; my $self = $class->SUPER::new($service, '/org/mpris/MediaPlayer2'); bless $self, $class; ::Watch($self, Seek => \&Seeked); #watchers for properties of org.mpris.MediaPlayer2.Player that send PropertiesChanged signal my %events; for my $prop (sort keys %PropertiesWatch) { push @{ $events{$_} }, $prop for split / /, $PropertiesWatch{$prop}; } for my $event (keys %events) { my $props= $events{$event}; ::Watch($self, $event => sub { my $self=shift; $PropChanged{$_}=1 for @$props; ::IdleDo('2_MPRIS2_propchanged', 500, \&PropertiesChanged, $self); }); } return $self; } dbus_signal(PropertiesChanged => ['string',['dict','string',['variant']],['array','string']], 'org.freedesktop.DBus.Properties'); sub PropertiesChanged { my $self=shift; my %changed; for my $name (keys %PropChanged) { no strict "refs"; $changed{$name}= $name->(); } %PropChanged=(); $self->emit_signal( PropertiesChanged => 'org.mpris.MediaPlayer2.Player', \%changed,[] ); } dbus_method('Raise', [], [],{no_return=>1}); sub Raise { ::ShowHide(1); } dbus_method('Quit', [], [],{no_return=>1}); sub Quit { ::Quit(); } dbus_property('CanQuit', 'bool', 'read'); sub CanQuit {dbus_boolean(1)} dbus_property('CanRaise', 'bool', 'read'); sub CanRaise {dbus_boolean(1)} dbus_property('HasTrackList', 'bool', 'read'); sub HasTrackList {dbus_boolean(0)} dbus_property('Identity', 'string', 'read'); sub Identity { 'gmusicbrowser' } dbus_property('DesktopEntry', 'string', 'read'); sub DesktopEntry { 'gmusicbrowser' } dbus_property('SupportedUriSchemes', ['array','string'], 'read'); sub SupportedUriSchemes { return ['file']; } dbus_property('SupportedMimeTypes', ['array','string'], 'read'); sub SupportedMimeTypes { return [qw(audio/mpeg application/ogg audio/x-flac audio/x-musepack audio/x-m4a)]; } #FIXME dbus_method('Next', [], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub Next { ::NextSong(); } dbus_method('Previous', [], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub Previous { ::PrevSong(); } dbus_method('Pause', [], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub Pause { ::Pause(); } dbus_method('PlayPause',[], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub PlayPause { ::PlayPause(); } dbus_method('Stop', [], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub Stop { ::Stop(); } dbus_method('Play', [], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub Play { ::Play(); } dbus_method('Seek', ['int64'], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub Seek { my $offset= $_[1]/1_000_000; #convert from microseconds my $sec= $::PlayTime || 0; return unless defined $::SongID; if ($offset>0) { $sec+=$offset; my $length= Songs::Get($::SongID,'length'); if ($sec>$length) { ::NextSong(); } else { ::SkipTo($sec) } } elsif ($offset<0) { $sec+=$offset; if ($sec<0) { ::PrevSong(); } else { ::SkipTo($sec) } } } dbus_method('SetPosition', [['struct','objectpath','int64']], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub SetPosition { my ($ID,$position)= @{ $_[1] }; return unless defined $::SongID && $ID==$::SongID; $position/=1_000_000; my $length= Songs::Get($::SongID,'length'); return if $length<0 || $position>$length; ::SkipTo($position); } dbus_method('OpenUri', ['string'], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub OpenUri { my $uri=$_[1]; my $IDs= ::Uris_to_IDs($uri); my $ID= $IDs->[0]; ::Select(song => $ID, play=>1) if defined $ID; } dbus_signal(Seeked => ['int64'], 'org.mpris.MediaPlayer2.Player'); sub Seeked { $_[0]->emit_signal( Seeked => $_[1]*1_000_000 ); } dbus_property('PlaybackStatus', 'string', 'read', 'org.mpris.MediaPlayer2.Player'); sub PlaybackStatus { my $status= $::TogPlay ? 'Playing' : defined $::TogPlay ? 'Paused' : 'Stopped'; return dbus_string($status); } dbus_property('LoopStatus', 'string', 'readwrite', 'org.mpris.MediaPlayer2.Player'); sub LoopStatus { if (defined $_[1]) { my $m=$_[1]; my $notrack; if ($m eq 'None') { ::SetRepeat(0); $notrack=1; } elsif ($m eq 'Track') { ::SetRepeat(1); ::ToggleLock('fullfilename',1); } elsif ($m eq 'Playlist'){ ::SetRepeat(1); $notrack=1; } if ($notrack && $::TogLock && $::TogLock eq 'fullfilename') { ::ToggleLock('fullfilename') } } else { my $r= !$::Options{Repeat} ? 'None' : ($::TogLock && $::TogLock eq 'fullfilename') ? 'Track' : 'Playlist'; return dbus_string($r); } } dbus_property('Rate', 'double', 'readwrite', 'org.mpris.MediaPlayer2.Player'); sub Rate {dbus_double(1)} dbus_property('MinimumRate', 'double', 'read', 'org.mpris.MediaPlayer2.Player'); sub MinimumRate {dbus_double(1)} dbus_property('MaximumRate', 'double', 'read', 'org.mpris.MediaPlayer2.Player'); sub MaximumRate {dbus_double(1)} dbus_property('Shuffle', 'bool', 'readwrite', 'org.mpris.MediaPlayer2.Player'); sub Shuffle { my $on= ($::RandomMode || $::Options{Sort}=~m/shuffle/) ? 1 : 0; return dbus_boolean($on) if !defined $_[1]; ::ToggleSort() if $_[1] xor $on; } dbus_property('Metadata', ['dict','string',['variant']], 'read', 'org.mpris.MediaPlayer2.Player'); sub Metadata { GetMetadata_from($::SongID); } dbus_property('Volume', 'double', 'readwrite', 'org.mpris.MediaPlayer2.Player'); sub Volume { if (defined $_[1]) { my $v=$_[1]; $v=0 if $v<0; ::ChangeVol($v); } else { return dbus_double($::Volume/100); } } dbus_property('Position', 'int64', 'read', 'org.mpris.MediaPlayer2.Player'); sub Position { return dbus_int64( ($::PlayTime||0) *1_000_000 ); } dbus_property('CanGoNext', 'bool', 'read', 'org.mpris.MediaPlayer2.Player'); sub CanGoNext { return dbus_boolean(1) if !defined $::Position && @$::ListPlay; return dbus_boolean(1) if @$::Queue; return dbus_boolean(0) unless @$::ListPlay>1; return dbus_boolean(0) if !$::Options{Repeat} && $::Position==$#$::ListPlay; return dbus_boolean(1); } dbus_property('CanGoPrevious', 'bool', 'read', 'org.mpris.MediaPlayer2.Player'); sub CanGoPrevious { return dbus_boolean( @$::Recent > ($::RecentPos||0) ); } dbus_property('CanPlay', 'bool', 'read', 'org.mpris.MediaPlayer2.Player'); sub CanPlay { return dbus_boolean( defined $::SongID ); } dbus_property('CanSeek', 'bool', 'read', 'org.mpris.MediaPlayer2.Player'); sub CanSeek { return dbus_boolean( defined $::SongID ); #will need to check if stream when supported } dbus_property('CanControl', 'bool', 'read', 'org.mpris.MediaPlayer2.Player'); sub CanControl {dbus_boolean(1)} # 'org.mpris.MediaPlayer2.Player','Metadata' sub GetMetadata_from { my $ID=shift; # Net::DBus support for properties is incomplete, the following use undocumented functions to force it to use the correct data types for the returned values my $type= [ &Net::DBus::Binding::Message::TYPE_DICT_ENTRY, [ &Net::DBus::Binding::Message::TYPE_STRING, [ &Net::DBus::Binding::Message::TYPE_VARIANT, [], ]]]; #my ($type)= Net::DBus::Binding::Introspector->_convert(['dict','string',['variant']]); #works too, not sure which one is best return Net::DBus::Binding::Value->new($type,{}) unless defined $ID; my %h; $h{$_}=Songs::Get($ID,$_) for qw/title album artist comment length track disc year album_artist uri album_picture rating bitrate samprate genre playcount/; my %r= #return values ( 'mpris:length' => dbus_int64($h{'length'}*1_000_000), 'mpris:trackid' => dbus_string($ID), #FIXME should contain a string that uniquely identifies the track within the scope of the playlist 'xesam:album' => dbus_string($h{album}), 'xesam:albumArtist' => dbus_array([ $h{album_artist} ]), 'xesam:artist' => dbus_array([ $h{artist} ]), 'xesam:comment' => ( $h{comment} ne '' ? dbus_array([$h{comment}]) : undef ), 'xesam:contentCreated' => ($h{year} ? dbus_string($h{year}) : undef), # ."-01-01T00:00Z" ? 'xesam:discNumber' => ($h{disc} ? dbus_int32($h{disc}) : undef), 'xesam:genre', => dbus_array([split /\x00/, $h{genre}]), 'xesam:lastUsed', => ($h{lastplay} ? dbus_string( ::strftime("%FT%RZ",gmtime($h{lastplay})) ) : undef), 'xesam:title', => dbus_string( $h{title} ), 'xesam:trackNumber' => ( $h{track} ? dbus_int32($h{track}) : undef), 'xesam:url' => dbus_string( $h{uri} ), 'xesam:useCount' => dbus_int32($h{playcount}), # FIXME check if field exists #'xesam:audioBPM' => #'xesam:composer' => dbus_array([ $h{composer} ]), #'xesam:lyricist', => dbus_array([ $h{lyricist} ]), ); my $rating=$h{rating}; if (defined $rating && length $rating) { $r{'xesam:userRating'}=dbus_double($rating/100); } if (my $pic= $h{album_picture}) #FIXME use ~album.picture.uri when available { $r{'mpris:artUrl'}= dbus_string( 'file://'.::url_escape($pic) ) if $pic=~m/\.(?:jpe?g|png|gif)$/i; # ignore embedded pictures } delete $r{$_} for grep !defined $r{$_}, keys %r; return Net::DBus::Binding::Value->new($type,\%r); } 1;