[banshee] Add MPRIS v2 support (bgo#570841)



commit 51fd931cc789ccf614913e8596f320d1bc16e785
Author: Bertrand Lorentz <bertrand lorentz gmail com>
Date:   Tue Aug 31 22:16:03 2010 +0200

    Add MPRIS v2 support (bgo#570841)
    
    This extension brings an implementation of version 2.0 of the Media
    Player Remote Interfacing Specification, available at
    http://www.mpris.org, allowing external applications
    to interact with Banshee through a standard D-Bus API.
    
    This API supports basic playback and media player state control, and
    provides some metadata for the current track. The optional TrackList
    interface is not implemented.
    
    The extension is disabled by default for now, as there's not a lot of
    MPRIS v2 clients currently in the wild. This might change in the future.
    
    The MPRIS v2 spec make extensive use of D-Bus properties, so the
    org.freedesktop.DBus.Properties interface had to be implemented
    manually, including the PropertiesChanged signal.
    
    This code is based on an old patch by John Millikin, which was an
    implementation of MPRIS v1.
    
    I'm trying to beat Aaron's record for the longest commit message, but I
    think I'm not there yet, and his messages are way funnier anyway.

 Banshee.sln                                        |    7 +
 build/build.environment.mk                         |    1 +
 configure.ac                                       |    1 +
 data/addin-xml-strings.cs                          |    5 +
 .../Banshee.Mpris/Banshee.Mpris.addin.xml          |   21 +
 src/Extensions/Banshee.Mpris/Banshee.Mpris.csproj  |   86 ++++
 .../Banshee.Mpris/Banshee.Mpris/IMediaPlayer.cs    |   62 +++
 .../Banshee.Mpris/Banshee.Mpris/IPlayer.cs         |   69 +++
 .../Banshee.Mpris/Banshee.Mpris/MediaPlayer.cs     |  493 ++++++++++++++++++++
 .../Banshee.Mpris/Banshee.Mpris/Metadata.cs        |  100 ++++
 .../Banshee.Mpris/Banshee.Mpris/MprisService.cs    |  119 +++++
 src/Extensions/Banshee.Mpris/Makefile.am           |   16 +
 src/Extensions/Makefile.am                         |    1 +
 13 files changed, 981 insertions(+), 0 deletions(-)
---
diff --git a/Banshee.sln b/Banshee.sln
index cc1e339..e921bdb 100644
--- a/Banshee.sln
+++ b/Banshee.sln
@@ -134,6 +134,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.YouTube", "src\Exte
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.LastfmStreaming", "src\Extensions\Banshee.LastfmStreaming\Banshee.LastfmStreaming.csproj", "{66617494-94CB-43E8-877C-A586F5A6F4EC}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.Mpris", "src\Extensions\Banshee.Mpris\Banshee.Mpris.csproj", "{8FF8A538-7D48-4AEB-A5C0-CA295DC1FE8D}"
+EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.AmazonMp3", "src\Extensions\Banshee.AmazonMp3\Banshee.AmazonMp3.csproj", "{930ADBFD-07F1-4044-A1C5-8B28BD23DB5C}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.AmazonMp3.Store", "src\Extensions\Banshee.AmazonMp3.Store\Banshee.AmazonMp3.Store.csproj", "{AF8A9C6D-2188-413D-8EB8-C5E242BD68AC}"
@@ -307,6 +309,10 @@ Global
 		{8E8D7EAD-3B7A-4F7D-8146-75AFCB9DEE83}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{8E8D7EAD-3B7A-4F7D-8146-75AFCB9DEE83}.Windows|Any CPU.ActiveCfg = Windows|Any CPU
 		{8E8D7EAD-3B7A-4F7D-8146-75AFCB9DEE83}.Windows|Any CPU.Build.0 = Windows|Any CPU
+		{8FF8A538-7D48-4AEB-A5C0-CA295DC1FE8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{8FF8A538-7D48-4AEB-A5C0-CA295DC1FE8D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{8FF8A538-7D48-4AEB-A5C0-CA295DC1FE8D}.Release|Any CPU.ActiveCfg = Debug|Any CPU
+		{8FF8A538-7D48-4AEB-A5C0-CA295DC1FE8D}.Release|Any CPU.Build.0 = Debug|Any CPU
 		{930ADBFD-07F1-4044-A1C5-8B28BD23DB5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{930ADBFD-07F1-4044-A1C5-8B28BD23DB5C}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{930ADBFD-07F1-4044-A1C5-8B28BD23DB5C}.Release|Any CPU.ActiveCfg = Debug|Any CPU
@@ -512,6 +518,7 @@ Global
 		{05148D80-6C5C-4BE5-9BD7-89613753D027} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
 		{387DC336-45F9-4263-9606-2881907D2105} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
 		{66617494-94CB-43E8-877C-A586F5A6F4EC} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
+		{8FF8A538-7D48-4AEB-A5C0-CA295DC1FE8D} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
 		{930ADBFD-07F1-4044-A1C5-8B28BD23DB5C} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
 		{AF8A9C6D-2188-413D-8EB8-C5E242BD68AC} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
 		{95374549-9553-4C1E-9D89-667755F90E12} = {4F47D6F1-4047-4A89-AE85-3AE5EF9F2961}
diff --git a/build/build.environment.mk b/build/build.environment.mk
index 7862a74..a2bd1aa 100644
--- a/build/build.environment.mk
+++ b/build/build.environment.mk
@@ -143,6 +143,7 @@ REF_EXTENSION_LIBRARYWATCHER = $(LINK_BANSHEE_SERVICES_DEPS)
 REF_EXTENSION_MINIMODE = $(LINK_BANSHEE_THICKCLIENT_DEPS)
 REF_EXTENSION_MEEGO = $(LINK_BANSHEE_THICKCLIENT_DEPS)
 LINK_EXTENSION_MEEGO = -r:$(DIR_BIN)/Banshee.MeeGo.dll $(REF_EXTENSION_MEEGO)
+REF_EXTENSION_MPRIS = $(LINK_BANSHEE_THICKCLIENT_DEPS)
 REF_EXTENSION_MULTIMEDIAKEYS = $(LINK_BANSHEE_SERVICES_DEPS)
 REF_EXTENSION_FIXUP = $(LINK_BANSHEE_THICKCLIENT_DEPS) $(LINK_MUSICBRAINZ_DEPS)
 REF_EXTENSION_NOTIFICATIONAREA = $(LINK_BANSHEE_THICKCLIENT_DEPS)
diff --git a/configure.ac b/configure.ac
index 794a6de..3f32ecd 100644
--- a/configure.ac
+++ b/configure.ac
@@ -352,6 +352,7 @@ src/Extensions/Banshee.LibraryWatcher/Makefile
 src/Extensions/Banshee.MiniMode/Makefile
 src/Extensions/Banshee.MiroGuide/Makefile
 src/Extensions/Banshee.MeeGo/Makefile
+src/Extensions/Banshee.Mpris/Makefile
 src/Extensions/Banshee.MultimediaKeys/Makefile
 src/Extensions/Banshee.NotificationArea/Makefile
 src/Extensions/Banshee.NowPlaying/Makefile
diff --git a/data/addin-xml-strings.cs b/data/addin-xml-strings.cs
index 9f4b483..f264e9e 100644
--- a/data/addin-xml-strings.cs
+++ b/data/addin-xml-strings.cs
@@ -160,6 +160,11 @@ internal static class AddinXmlStringCatalog
         Catalog.GetString (@"Browse and subscribe to thousands of audio and video podcasts.");
         Catalog.GetString (@"Online Sources");
 
+        // ../src/Extensions/Banshee.Mpris/Banshee.Mpris.addin.xml
+        Catalog.GetString (@"MPRIS D-Bus interface");
+        Catalog.GetString (@"Control Banshee using the MPRIS D-Bus interface.");
+        Catalog.GetString (@"Utilities");
+
         // ../src/Extensions/Banshee.MultimediaKeys/Banshee.MultimediaKeys.addin.xml
         Catalog.GetString (@"Multimedia Keys");
         Catalog.GetString (@"Control playback via the multimedia keys on your keyboard.");
diff --git a/src/Extensions/Banshee.Mpris/Banshee.Mpris.addin.xml b/src/Extensions/Banshee.Mpris/Banshee.Mpris.addin.xml
new file mode 100644
index 0000000..a97cfd1
--- /dev/null
+++ b/src/Extensions/Banshee.Mpris/Banshee.Mpris.addin.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Addin
+    id="Banshee.Mpris"
+    version="1.0"
+    compatVersion="1.0"
+    copyright="© 2010 John Millikin, Bertrand Lorentz. Licensed under the MIT X11 license."
+    name="MPRIS D-Bus interface"
+    description="Control Banshee using the MPRIS D-Bus interface."
+    author="John Millikin, Bertrand Lorentz"
+    category="Utilities"
+    defaultEnabled="false">
+
+  <Dependencies>
+    <Addin id="Banshee.Services" version="1.0" />
+  </Dependencies>
+
+  <Extension path="/Banshee/ServiceManager/Service">
+    <Service class="Banshee.Mpris.MprisService"/>
+  </Extension>
+
+</Addin>
diff --git a/src/Extensions/Banshee.Mpris/Banshee.Mpris.csproj b/src/Extensions/Banshee.Mpris/Banshee.Mpris.csproj
new file mode 100644
index 0000000..709870b
--- /dev/null
+++ b/src/Extensions/Banshee.Mpris/Banshee.Mpris.csproj
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"; ToolsVersion="3.5">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProductVersion>9.0.21022</ProductVersion>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>{8FF8A538-7D48-4AEB-A5C0-CA295DC1FE8D}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <RootNamespace>Banshee.Mpris</RootNamespace>
+    <ReleaseVersion>1.3</ReleaseVersion>
+    <AssemblyName>Banshee.Mpris</AssemblyName>
+    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>..\..\..\bin</OutputPath>
+    <DefineConstants>DEBUG</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>none</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>..\..\..\bin</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="Banshee.Mpris\Metadata.cs" />
+    <Compile Include="Banshee.Mpris\MediaPlayer.cs" />
+    <Compile Include="Banshee.Mpris\MprisService.cs" />
+    <Compile Include="Banshee.Mpris\IPlayer.cs" />
+    <Compile Include="Banshee.Mpris\IMediaPlayer.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="NDesk.DBus, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f6716e4f9b2ed099">
+      <Package>ndesk-dbus-1.0</Package>
+    </Reference>
+    <Reference Include="NDesk.DBus.GLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f6716e4f9b2ed099">
+      <Package>ndesk-dbus-glib-1.0</Package>
+    </Reference>
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\..\Core\Banshee.Core\Banshee.Core.csproj">
+      <Project>{2ADB831A-A050-47D0-B6B9-9C19D60233BB}</Project>
+      <Name>Banshee.Core</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Core\Banshee.Services\Banshee.Services.csproj">
+      <Project>{B28354F0-BA87-44E8-989F-B864A3C7C09F}</Project>
+      <Name>Banshee.Services</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Hyena\Hyena\Hyena.csproj">
+      <Project>{95374549-9553-4C1E-9D89-667755F90E12}</Project>
+      <Name>Hyena</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Core\Banshee.ThickClient\Banshee.ThickClient.csproj">
+      <Project>{AC839523-7BDF-4AB6-8115-E17921B96EC6}</Project>
+      <Name>Banshee.ThickClient</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <ProjectExtensions>
+    <MonoDevelop>
+      <Properties>
+        <MonoDevelop.Autotools.MakefileInfo IntegrationEnabled="true" RelativeMakefileName="Makefile.am" RelativeConfigureInPath="../../..">
+          <BuildFilesVar Sync="true" Name="SOURCES" />
+          <DeployFilesVar />
+          <ResourcesVar Sync="true" Name="RESOURCES" />
+          <OthersVar />
+          <GacRefVar />
+          <AsmRefVar />
+          <ProjectRefVar />
+        </MonoDevelop.Autotools.MakefileInfo>
+      </Properties>
+    </MonoDevelop>
+  </ProjectExtensions>
+  <ItemGroup>
+    <EmbeddedResource Include="Banshee.Mpris.addin.xml">
+      <LogicalName>Banshee.Mpris.addin.xml</LogicalName>
+    </EmbeddedResource>
+  </ItemGroup>
+</Project>
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Mpris/Banshee.Mpris/IMediaPlayer.cs b/src/Extensions/Banshee.Mpris/Banshee.Mpris/IMediaPlayer.cs
new file mode 100644
index 0000000..c75f3fe
--- /dev/null
+++ b/src/Extensions/Banshee.Mpris/Banshee.Mpris/IMediaPlayer.cs
@@ -0,0 +1,62 @@
+//
+// IMediaPlayer.cs
+//
+// Author:
+//   Bertrand Lorentz <bertrand lorentz gmail com>
+//
+// Copyright (c) 2010 Bertrand Lorentz
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+using NDesk.DBus;
+
+namespace Banshee.Mpris
+{
+    public delegate void PropertiesChangedHandler (string @interface,
+                                                   IDictionary<string, object> changed_properties,
+                                                   string [] invalidated_properties);
+
+    [Interface ("org.freedesktop.DBus.Properties")]
+    public interface IProperties
+    {
+        [return: Argument ("value")]
+        object Get (string @interface, string propname);
+        void Set (string @interface, string propname, object value);
+        [return: Argument ("props")]
+        IDictionary<string, object> GetAll (string @interface);
+        event PropertiesChangedHandler PropertiesChanged;
+    }
+
+    [Interface ("org.mpris.MediaPlayer2")]
+    public interface IMediaPlayer
+    {
+        bool CanQuit { get; }
+        bool CanRaise { get; }
+        bool HasTrackList { get; }
+        string Identity { get; }
+        string DesktopEntry { get; }
+        string [] SupportedUriSchemes { get; }
+        string [] SupportedMimeTypes { get; }
+
+        void Quit ();
+        void Raise ();
+    }
+}
diff --git a/src/Extensions/Banshee.Mpris/Banshee.Mpris/IPlayer.cs b/src/Extensions/Banshee.Mpris/Banshee.Mpris/IPlayer.cs
new file mode 100644
index 0000000..297bfbe
--- /dev/null
+++ b/src/Extensions/Banshee.Mpris/Banshee.Mpris/IPlayer.cs
@@ -0,0 +1,69 @@
+//
+// IPlayer.cs
+//
+// Author:
+//   Bertrand Lorentz <bertrand lorentz gmail com>
+//
+// Copyright (C) 2010 Bertrand Lorentz.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Collections.Generic;
+using NDesk.DBus;
+
+namespace Banshee.Mpris
+{
+    public delegate void DBusPlayerSeekedHandler (long position);
+
+    [Interface ("org.mpris.MediaPlayer2.Player")]
+    public interface IPlayer
+    {
+        event DBusPlayerSeekedHandler Seeked;
+
+        bool CanControl { get; }
+        bool CanGoNext { get; }
+        bool CanGoPrevious { get; }
+        bool CanPause { get; }
+        bool CanPlay { get; }
+        bool CanSeek { get; }
+        double MinimumRate { get; }
+        double MaximumRate { get; }
+        double Rate { get; set; }
+        bool Shuffle { get; set; }
+        string LoopStatus { get; set; }
+        string PlaybackStatus { get; }
+        IDictionary<string, object> Metadata { get; }
+        double Volume { get; set; }
+        long Position { get; }
+
+        void Next ();
+        void Previous ();
+        void Pause ();
+        void PlayPause ();
+        void Stop ();
+        void Play ();
+        void Seek (long offset);
+        void SetPosition (string trackid, long position);
+        void OpenUri (string uri);
+    }
+}
+
diff --git a/src/Extensions/Banshee.Mpris/Banshee.Mpris/MediaPlayer.cs b/src/Extensions/Banshee.Mpris/Banshee.Mpris/MediaPlayer.cs
new file mode 100644
index 0000000..419f0ae
--- /dev/null
+++ b/src/Extensions/Banshee.Mpris/Banshee.Mpris/MediaPlayer.cs
@@ -0,0 +1,493 @@
+//
+// MediaPlayer.cs
+//
+// Authors:
+//   John Millikin <jmillikin gmail com>
+//   Bertrand Lorentz <bertrand lorentz gmail com>
+//
+// Copyright (C) 2009 John Millikin
+// Copyright (C) 2010 Bertrand Lorentz
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Collections.Generic;
+
+using NDesk.DBus;
+using Hyena;
+
+using Banshee.Gui;
+using Banshee.MediaEngine;
+using Banshee.PlaybackController;
+using Banshee.ServiceStack;
+
+namespace Banshee.Mpris
+{
+    public class MediaPlayer : IMediaPlayer, IPlayer, IProperties
+    {
+        private static string mediaplayer_interface_name = "org.mpris.MediaPlayer2";
+        private static string player_interface_name = "org.mpris.MediaPlayer2.Player";
+        private PlaybackControllerService playback_service;
+        private PlayerEngineService engine_service;
+        private Dictionary<string, object> changed_properties;
+        private List<string> invalidated_properties;
+
+        private static ObjectPath path = new ObjectPath ("/org/mpris/MediaPlayer2");
+        public static ObjectPath Path {
+            get { return path; }
+        }
+
+        private event PropertiesChangedHandler properties_changed;
+        event PropertiesChangedHandler IProperties.PropertiesChanged {
+            add { properties_changed += value; }
+            remove { properties_changed -= value; }
+        }
+
+        private event DBusPlayerSeekedHandler dbus_seeked;
+        event DBusPlayerSeekedHandler IPlayer.Seeked {
+            add { dbus_seeked += value; }
+            remove { dbus_seeked -= value; }
+        }
+
+        public MediaPlayer ()
+        {
+            playback_service = ServiceManager.PlaybackController;
+            engine_service = ServiceManager.PlayerEngine;
+            changed_properties = new Dictionary<string, object> ();
+            invalidated_properties = new List<string> ();
+        }
+
+#region IMediaPlayer
+
+        public bool CanQuit {
+            get { return true; }
+        }
+
+        public bool CanRaise {
+            get { return true; }
+        }
+
+        public bool HasTrackList {
+            get { return false; }
+        }
+
+        public string Identity {
+            get { return "Banshee"; }
+        }
+
+        public string DesktopEntry {
+            get { return "banshee-1"; }
+        }
+
+        // This is just a list of commonly supported MIME types.
+        // We don't know exactly which ones are supported by the PlayerEngine
+        private static string [] supported_mimetypes = { "application/ogg", "audio/flac",
+            "audio/mp3", "audio/mp4", "audio/mpeg", "audio/ogg", "audio/vorbis", "audio/wav",
+            "audio/x-flac", "audio/x-vorbis+ogg",
+            "video/avi", "video/mp4", "video/mpeg" };
+        public string [] SupportedMimeTypes {
+            get { return supported_mimetypes; }
+        }
+
+        public string [] SupportedUriSchemes {
+            get { return (string [])ServiceManager.PlayerEngine.ActiveEngine.SourceCapabilities; }
+        }
+
+        public void Raise ()
+        {
+            if (!CanRaise) {
+                return;
+            }
+
+            ServiceManager.Get<GtkElementsService> ().PrimaryWindow.SetVisible (true);
+        }
+
+        public void Quit ()
+        {
+            if (!CanQuit) {
+                return;
+            }
+
+            Application.Shutdown ();
+        }
+
+#endregion
+
+#region IPlayer
+
+        public bool CanControl {
+            get { return true; }
+        }
+
+        // We don't really know if we can actually go next or previous
+        public bool CanGoNext {
+            get { return CanControl; }
+        }
+
+        public bool CanGoPrevious {
+            get { return CanControl; }
+        }
+
+        public bool CanPause {
+            get { return engine_service.CanPause; }
+        }
+
+        public bool CanPlay {
+            get { return CanControl; }
+        }
+
+        public bool CanSeek {
+            get { return engine_service.CanSeek; }
+        }
+
+        public double MaximumRate {
+            get { return 1.0; }
+        }
+
+        public double MinimumRate {
+            get { return 1.0; }
+        }
+
+        public double Rate {
+            get { return 1.0; }
+            set {}
+        }
+
+        public bool Shuffle {
+            get { return !(playback_service.ShuffleMode == "off"); }
+            set { playback_service.ShuffleMode = value ? "song" : "off"; }
+        }
+
+        public string LoopStatus {
+            get {
+                string loop_status;
+                switch (playback_service.RepeatMode) {
+                    case PlaybackRepeatMode.None:
+                        loop_status = "None";
+                        break;
+                    case PlaybackRepeatMode.RepeatSingle:
+                        loop_status = "Track";
+                        break;
+                    case PlaybackRepeatMode.RepeatAll:
+                        loop_status = "Playlist";
+                        break;
+                    default:
+                        loop_status = "None";
+                        break;
+                }
+                return loop_status;
+            }
+            set {
+                switch (value) {
+                    case "None":
+                        playback_service.RepeatMode = PlaybackRepeatMode.None;
+                        break;
+                    case "Track":
+                        playback_service.RepeatMode = PlaybackRepeatMode.RepeatSingle;
+                        break;
+                    case "Playlist":
+                        playback_service.RepeatMode = PlaybackRepeatMode.RepeatAll;
+                        break;
+                }
+            }
+        }
+
+        public string PlaybackStatus {
+            get {
+                string status;
+                switch (engine_service.CurrentState) {
+                    case PlayerState.Playing:
+                        status = "Playing";
+                        break;
+                    case PlayerState.Paused:
+                        status = "Paused";
+                        break;
+                    default:
+                        status = "Stopped";
+                        break;
+                }
+                return status;
+            }
+        }
+
+        public IDictionary<string, object> Metadata {
+            get {
+                var metadata = new Metadata (playback_service.CurrentTrack);
+                return metadata.DataStore;
+            }
+        }
+
+        public double Volume {
+            get { return engine_service.Volume / 100.0; }
+            set { engine_service.Volume = (ushort)Math.Round (value * 100); }
+        }
+
+        // Position is expected in microseconds
+        public long Position {
+            get { return (long)engine_service.Position * 1000; }
+        }
+
+        public void Next ()
+        {
+            playback_service.Next ();
+        }
+
+        public void Previous ()
+        {
+            playback_service.Previous ();
+        }
+
+        public void Pause ()
+        {
+            engine_service.Pause ();
+        }
+
+        public void PlayPause ()
+        {
+            engine_service.TogglePlaying ();
+        }
+
+        public void Stop ()
+        {
+            engine_service.Close ();
+        }
+
+        public void Play ()
+        {
+            engine_service.Play ();
+        }
+
+        public void SetPosition (string trackid, long position)
+        {
+            if (!CanSeek) {
+                return;
+            }
+
+            if (String.IsNullOrEmpty (trackid) || trackid != (string)Metadata["trackid"]) {
+                return;
+            }
+
+            // position is in microseconds, we speak in milliseconds
+            long position_ms = position / 1000;
+            if (position_ms < 0 || position_ms > playback_service.CurrentTrack.Duration.TotalMilliseconds) {
+                return;
+            }
+
+            engine_service.Position = (uint)position_ms;
+        }
+
+        public void Seek (long position)
+        {
+            if (!CanSeek) {
+                return;
+            }
+
+            // position is in microseconds, relative to the current position and can be negative
+            long new_pos = (int)engine_service.Position + (position / 1000);
+            if (new_pos < 0) {
+                engine_service.Position = 0;
+            } else {
+                engine_service.Position = (uint)new_pos;
+            }
+        }
+
+        public void OpenUri (string uri)
+        {
+            engine_service.Open (new SafeUri (uri));
+            engine_service.Play ();
+        }
+
+#endregion
+
+#region Signals
+
+        public void HandlePropertiesChange ()
+        {
+            PropertiesChangedHandler handler = properties_changed;
+            if (handler != null) {
+                lock (changed_properties) {
+                    // Properties that trigger this signal are all on the Player interface
+                    handler (player_interface_name, changed_properties, invalidated_properties.ToArray ());
+                    changed_properties.Clear ();
+                    invalidated_properties.Clear ();
+                }
+            }
+        }
+
+        public void HandleSeek ()
+        {
+            DBusPlayerSeekedHandler dbus_handler = dbus_seeked;
+            if (dbus_handler != null) {
+                dbus_handler (Position);
+            }
+        }
+
+        public void AddPropertyChange (params PlayerProperties [] properties)
+        {
+            lock (changed_properties) {
+                foreach (PlayerProperties prop in properties) {
+                    string prop_name = prop.ToString ();
+                    changed_properties[prop_name] = Get (player_interface_name, prop_name);
+                }
+            }
+            // TODO We could check if a property really has changed and only fire the event in that case
+            HandlePropertiesChange ();
+        }
+
+#endregion
+
+#region Dbus.Properties
+
+        private static string [] mediaplayer_properties = { "CanQuit", "CanRaise", "HasTrackList", "Identity",
+            "DesktopEntry", "SupportedMimeTypes", "SupportedUriSchemes" };
+
+        private static string [] player_properties = { "CanControl", "CanGoNext", "CanGoPrevious", "CanPause",
+            "CanPlay", "CanSeek", "LoopStatus", "MaximumRate", "Metadata", "MinimumRate", "PlaybackStatus",
+            "Position", "Rate", "Shuffle", "Volume" };
+
+        public object Get (string interface_name, string propname)
+        {
+            if (interface_name == mediaplayer_interface_name) {
+                switch (propname) {
+                    case "CanQuit":
+                        return CanQuit;
+                    case "CanRaise":
+                        return CanRaise;
+                    case "HasTrackList":
+                        return HasTrackList;
+                    case "Identity":
+                        return Identity;
+                    case "DesktopEntry":
+                        return DesktopEntry;
+                    case "SupportedMimeTypes":
+                        return SupportedMimeTypes;
+                    case "SupportedUriSchemes":
+                        return SupportedUriSchemes;
+                    default:
+                        return null;
+                }
+            } else if (interface_name == player_interface_name) {
+                switch (propname) {
+                    case "CanControl":
+                        return CanControl;
+                    case "CanGoNext":
+                        return CanGoNext;
+                    case "CanGoPrevious":
+                        return CanGoPrevious;
+                    case "CanPause":
+                        return CanPause;
+                    case "CanPlay":
+                        return CanPlay;
+                    case "CanSeek":
+                        return CanSeek;
+                    case "MinimumRate":
+                        return MinimumRate;
+                    case "MaximumRate":
+                        return MaximumRate;
+                    case "Rate":
+                        return Rate;
+                    case "Shuffle":
+                        return Shuffle;
+                    case "LoopStatus":
+                        return LoopStatus;
+                    case "PlaybackStatus":
+                        return PlaybackStatus;
+                    case "Position":
+                        return Position;
+                    case "Metadata":
+                        return Metadata;
+                    case "Volume":
+                        return Volume;
+                    default:
+                        return null;
+                }
+            } else {
+                return null;
+            }
+        }
+
+        public void Set (string interface_name, string propname, object value)
+        {
+            // All writable properties are on the Player interface
+            if (interface_name != player_interface_name) {
+                return;
+            }
+
+            switch (propname) {
+            case "LoopStatus":
+                string s = value as string;
+                if (!String.IsNullOrEmpty (s)) {
+                    LoopStatus = s;
+                }
+                break;
+            case "Shuffle":
+                if (value is bool) {
+                    Shuffle = (bool)value;
+                }
+                break;
+            case "Volume":
+                if (value is double) {
+                    Volume = (double)value;
+                }
+                break;
+            }
+        }
+
+        public IDictionary<string, object> GetAll (string interface_name)
+        {
+            var props = new Dictionary<string, object> ();
+
+            if (interface_name == mediaplayer_interface_name) {
+                foreach (string prop in mediaplayer_properties) {
+                    props.Add (prop, Get (interface_name, prop));
+                }
+            } else if (interface_name == player_interface_name) {
+                foreach (string prop in player_properties) {
+                    props.Add (prop, Get (interface_name, prop));
+                }
+            }
+
+            return props;
+        }
+    }
+
+#endregion
+
+    // Those are all the properties that can trigger the PropertiesChanged signal
+    // The names must match exactly the names of the properties
+    public enum PlayerProperties
+    {
+        CanControl,
+        CanGoNext,
+        CanGoPrevious,
+        CanPause,
+        CanPlay,
+        CanSeek,
+        MinimumRate,
+        MaximumRate,
+        Rate,
+        Shuffle,
+        LoopStatus,
+        PlaybackStatus,
+        Metadata,
+        Volume
+    }
+}
diff --git a/src/Extensions/Banshee.Mpris/Banshee.Mpris/Metadata.cs b/src/Extensions/Banshee.Mpris/Banshee.Mpris/Metadata.cs
new file mode 100644
index 0000000..6f3514c
--- /dev/null
+++ b/src/Extensions/Banshee.Mpris/Banshee.Mpris/Metadata.cs
@@ -0,0 +1,100 @@
+//
+// Metadata.cs
+//
+// Authors:
+//   John Millikin <jmillikin gmail com>
+//   Bertrand Lorentz <bertrand lorentz gmail com>
+//
+// Copyright (C) 2009 John Millikin
+// Copyright (C) 2010 Bertrand Lorentz
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Collections.Generic;
+using Banshee.Base;
+using Banshee.Collection;
+using Banshee.ServiceStack;
+
+namespace Banshee.Mpris
+{
+    public class Metadata
+    {
+        private static string object_path = String.Concat (DBusServiceManager.ObjectRoot, "/Track/");
+
+        private Dictionary<string, object> data_store;
+
+        public Metadata (TrackInfo track)
+        {
+            data_store = new Dictionary<string, object> ();
+
+            if (track == null) {
+                // Managed dbus doesn't like null or empty dictionaries
+                data_store["mpris:trackid"] = String.Concat (object_path, "Empty");
+                return;
+            }
+
+            // The trackid must be formatted like a dbus object path
+            data_store["mpris:trackid"] = String.Concat (object_path, track.CacheModelId, track.CacheEntryId);
+            SetInfo ("mpris:length", (long)track.Duration.TotalMilliseconds * 1000);
+            SetInfo ("xesam:url", track.Uri.ToString ());
+            SetInfo ("xesam:title", track.TrackTitle);
+            SetInfo ("xesam:album", track.AlbumTitle);
+            if (!String.IsNullOrEmpty (track.ArtistName)) {
+                SetInfo ("xesam:artist", new string [] {track.ArtistName});
+            }
+            if (!String.IsNullOrEmpty (track.AlbumArtist)) {
+                SetInfo ("xesam:albumArtist", new string [] {track.AlbumArtist});
+            }
+            if (!String.IsNullOrEmpty (track.Genre)) {
+                SetInfo ("xesam:genre", new string [] {track.Genre});
+            }
+            if (!String.IsNullOrEmpty (track.Comment)) {
+                SetInfo ("xesam:comment", new string [] {track.Comment});
+            }
+
+            if (track.TrackNumber > 0) {
+                data_store["xesam:trackNumber"] = track.TrackNumber;
+            }
+
+            if (track.ReleaseDate.Ticks > 0) {
+                SetInfo ("xesam:contentCreated", track.ReleaseDate.ToString ("s"));
+            }
+
+            string artid = track.ArtworkId;
+            if (artid != null) {
+                SetInfo ("mpris:artUrl", String.Concat ("file://", CoverArtSpec.GetPath (artid)));
+            }
+        }
+
+        private void SetInfo (string name, object property)
+        {
+            if (property == null) {
+                return;
+            }
+            data_store[name] = property;
+        }
+
+        public IDictionary<string, object> DataStore {
+            get { return data_store; }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Mpris/Banshee.Mpris/MprisService.cs b/src/Extensions/Banshee.Mpris/Banshee.Mpris/MprisService.cs
new file mode 100644
index 0000000..84a9ea8
--- /dev/null
+++ b/src/Extensions/Banshee.Mpris/Banshee.Mpris/MprisService.cs
@@ -0,0 +1,119 @@
+//
+// Service.cs
+//
+// Authors:
+//   John Millikin <jmillikin gmail com>
+//   Bertrand Lorentz <bertrand lorentz gmail com>
+//
+// Copyright (C) 2009 John Millikin
+// Copyright (C) 2010 Bertrand Lorentz
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using org.freedesktop.DBus;
+using NDesk.DBus;
+using Hyena;
+using Banshee.MediaEngine;
+using Banshee.PlaybackController;
+using Banshee.ServiceStack;
+
+namespace Banshee.Mpris
+{
+    public class MprisService : IExtensionService, IDisposable
+    {
+        private static string bus_name = "org.mpris.MediaPlayer2.banshee";
+
+        private MediaPlayer player;
+
+        public MprisService ()
+        {
+        }
+
+        void IExtensionService.Initialize ()
+        {
+            if (!DBusConnection.Enabled) {
+                return;
+            }
+
+            ServiceManager.PlayerEngine.ConnectEvent (OnPlayerEvent,
+                PlayerEvent.StartOfStream |
+                PlayerEvent.StateChange |
+                PlayerEvent.TrackInfoUpdated |
+                PlayerEvent.Seek |
+                PlayerEvent.Volume);
+            ServiceManager.PlaybackController.RepeatModeChanged += OnRepeatModeChanged;
+            ServiceManager.PlaybackController.ShuffleModeChanged += OnShuffleModeChanged;
+
+            player = new MediaPlayer();
+            Bus.Session.Register (MediaPlayer.Path, player);
+
+            if (Bus.Session.RequestName (bus_name) != RequestNameReply.PrimaryOwner) {
+                Hyena.Log.Warning ("MPRIS service couldn't grab bus name");
+                return;
+            }
+        }
+
+        void IDisposable.Dispose ()
+        {
+            Bus.Session.Unregister (MediaPlayer.Path);
+
+            ServiceManager.PlayerEngine.DisconnectEvent (OnPlayerEvent);
+            ServiceManager.PlaybackController.RepeatModeChanged -= OnRepeatModeChanged;
+            ServiceManager.PlaybackController.ShuffleModeChanged -= OnShuffleModeChanged;
+
+            Bus.Session.ReleaseName (bus_name);
+        }
+
+        private void OnPlayerEvent (PlayerEventArgs args)
+        {
+            switch (args.Event) {
+                case PlayerEvent.StartOfStream:
+                case PlayerEvent.TrackInfoUpdated:
+                    player.AddPropertyChange (PlayerProperties.Metadata);
+                    break;
+                case PlayerEvent.StateChange:
+                    player.AddPropertyChange (PlayerProperties.PlaybackStatus);
+                    break;
+                case PlayerEvent.Seek:
+                    player.HandleSeek ();
+                    break;
+                case PlayerEvent.Volume:
+                    player.AddPropertyChange (PlayerProperties.Volume);
+                    break;
+            }
+        }
+
+        private void OnRepeatModeChanged (object o, EventArgs<PlaybackRepeatMode> args)
+        {
+            player.AddPropertyChange (PlayerProperties.LoopStatus);
+        }
+
+        private void OnShuffleModeChanged (object o, EventArgs<string> args)
+        {
+            player.AddPropertyChange (PlayerProperties.Shuffle);
+        }
+
+        string IService.ServiceName {
+            get { return "MprisService"; }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Mpris/Makefile.am b/src/Extensions/Banshee.Mpris/Makefile.am
new file mode 100644
index 0000000..d3f35ea
--- /dev/null
+++ b/src/Extensions/Banshee.Mpris/Makefile.am
@@ -0,0 +1,16 @@
+ASSEMBLY = Banshee.Mpris
+TARGET = library
+LINK = $(REF_EXTENSION_MPRIS)
+INSTALL_DIR = $(EXTENSIONS_INSTALL_DIR)
+
+SOURCES =  \
+	Banshee.Mpris/IMediaPlayer.cs \
+	Banshee.Mpris/IPlayer.cs \
+	Banshee.Mpris/MediaPlayer.cs \
+	Banshee.Mpris/Metadata.cs \
+	Banshee.Mpris/MprisService.cs
+
+RESOURCES = Banshee.Mpris.addin.xml
+
+include $(top_srcdir)/build/build.mk
+
diff --git a/src/Extensions/Makefile.am b/src/Extensions/Makefile.am
index 28b7cc4..3ccce53 100644
--- a/src/Extensions/Makefile.am
+++ b/src/Extensions/Makefile.am
@@ -16,6 +16,7 @@ SUBDIRS = \
 	Banshee.LastfmStreaming \
 	Banshee.LibraryWatcher \
 	Banshee.MiniMode \
+	Banshee.Mpris \
 	Banshee.MiroGuide \
 	Banshee.MultimediaKeys \
 	Banshee.NotificationArea \



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]