diff --git a/.gitignore b/.gitignore index 381affa..969e9e9 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ [#]*[#] .\#* .swp +vendor/bundle/ diff --git a/README.md b/README.md index 7275ee1..774033f 100644 --- a/README.md +++ b/README.md @@ -162,9 +162,9 @@ Show battery status. #### Bspwm (Experimental) -**Requires Bspwm**. Shows desktops for selected monitor. and highlights focused one. Unfocused desktops are clickable. Could do with some optimization work and feedback from people that use BSP frequently, especially with multiple monitors. +**Requires Bspwm**. Shows desktops for selected monitor. and highlights focused one. Unfocused desktops are clickable. Could do with some optimization work and feedback from people that use BSP frequently, especially with multiple monitors. -`bsp = Barr::Blocks::Bspwm.new monitor: "DP-4", invert_focus_colors: true` +`bsp = Barr::Blocks::Bspwm.new monitor: "DP-4", invert_focus_colors: true` | Option | Value | Description | Default | | --- | --- | --- | --- | @@ -190,8 +190,8 @@ Shows the current date and/or time. `conky = Barr::Blocks::Conky.new string: "${cpu}"` | Option | Value | Description | Default | -| --- | --- | --- | --- | -| `text` | Conky 'TEXT' string | String made up of [one or more conky variables](http://conky.sourceforge.net/variables.html), as you might find in the `TEXT` section of a conkyrc | **REQUIRED** | +| --- | --- | --- | --- | +| `text` | Conky 'TEXT' string | String made up of [one or more conky variables](http://conky.sourceforge.net/variables.html), as you might find in the `TEXT` section of a conkyrc | **REQUIRED** | #### CPU @@ -261,7 +261,6 @@ There are no `Processes` block specific configurable options. | `buttons` | bool | As above, but for the player control buttons | `true` | | `title` | bool | As above, but for the track title | `true` | - #### Separator This block is a simple string to be used as a separator between other blocks. @@ -270,6 +269,17 @@ This block is a simple string to be used as a separator between other blocks. | --- | --- | --- | --- | | `symbol` | any string | The string to use as a separator | `|` +#### Spotify + +**Requires Spotify**. Shows currently playing artist and/or track, as well as control buttons. Control buttons use FontAwesome. + +`rb = Barr::Blocks::Spotify.new buttons: false` + +| Option | Value | Description | Default | +| --- | --- | --- | --- | +| `artist` | bool | Set to `true` or `false` to set whether or not the currently playing artist should be shown. | `true` | +| `buttons` | bool | As above, but for the player control buttons | `true` | +| `title` | bool | As above, but for the track title | `true` | #### Temperature diff --git a/barr.gemspec b/barr.gemspec index ddec6fd..1faae14 100644 --- a/barr.gemspec +++ b/barr.gemspec @@ -32,9 +32,11 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency "i3ipc", "0.2.0" spec.add_runtime_dependency "weather-api", "1.2.0" + spec.add_runtime_dependency "ruby-dbus", "~> 0.11.0" spec.requirements << "Lemonbar with XFT support (https://github.com/krypt-n/bar)" spec.requirements << "(Optional) I3 for Workspace support" spec.requirements << "(Optional) RhythmBox & rhythmbox-client" + spec.requirements << "(Optional) Spotify" spec.requirements << "(Optional) FontAwesome font" end diff --git a/lib/barr.rb b/lib/barr.rb index ba862e2..018649c 100644 --- a/lib/barr.rb +++ b/lib/barr.rb @@ -13,6 +13,7 @@ require 'barr/blocks/mem' require 'barr/blocks/processes' require 'barr/blocks/rhythmbox' +require 'barr/blocks/spotify' require 'barr/blocks/temperature' require 'barr/blocks/whoami' require 'barr/blocks/separator' diff --git a/lib/barr/blocks/spotify.rb b/lib/barr/blocks/spotify.rb new file mode 100644 index 0000000..ab416e6 --- /dev/null +++ b/lib/barr/blocks/spotify.rb @@ -0,0 +1,89 @@ +# coding: utf-8 +require 'barr/block' +require 'dbus' + +module Barr + module Blocks + class Spotify < Block + DBUS_PLAY = 'dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.PlayPause'.freeze + DBUS_NEXT = 'dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Next'.freeze + DBUS_PREV = 'dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Previous'.freeze + + attr_reader :view_opts, :spotify_iface + + def initialize(opts = {}) + super + + @view_opts = { + artist: opts[:artist].nil? || opts[:artist], + buttons: opts[:buttons].nil? || opts[:buttons], + title: opts[:title].nil? || opts[:title] + } + @spotify_iface = dbus_connection + end + + def update! + op = [] + + if @view_opts[:artist] || @view_opts[:title] + if(running?) + info = sys_cmd[:foo] + + if @view_opts[:artist] && @view_opts[:title] + op << "#{info[:artist]} - #{info[:title]}" + elsif @view_opts[:artist] + op << info[:artist] + elsif @view_opts[:title] + op << info[:title] + end + else + op << 'None' + end + end + + op << buttons if @view_opts[:buttons] + + @output = op.join(' ') + + end + + def running? + `pgrep spotify`.chomp.length != 0 + end + + def buttons + [ + "%{A:#{DBUS_PREV}:}\uf048%{A}", + "%{A:#{DBUS_PLAY}:}\uf04b%{A}", + "%{A:#{DBUS_NEXT}:}\uf051%{A}" + ].join(' ').freeze + end + + def dbus_connection + begin + spotify_service = DBus.session_bus['org.mpris.MediaPlayer2.spotify'] + spotify_player = spotify_service.object '/org/mpris/MediaPlayer2' + spotify_player.introspect + rescue DBus::Error + # This should only happen when testing and Spotify is not running, + # because DBus cannot find a service file that provides + # org.mpris.MediaPlayer2.spotify. This will not be an issue with + # typical usage as long as #running? is checked before using this + # method or #sys_cmd. + return nil + else + return spotify_player['org.mpris.MediaPlayer2.Player'] + end + end + + private + + def sys_cmd + dbus_meta = @spotify_iface['Metadata'] + Hash.new title: dbus_meta['xesam:title'], + artist: dbus_meta['xesam:artist'].join(', '), + album: dbus_meta['xesam:album'] + end + end + end +end diff --git a/spec/blocks/spotify_spec.rb b/spec/blocks/spotify_spec.rb new file mode 100644 index 0000000..80faf6b --- /dev/null +++ b/spec/blocks/spotify_spec.rb @@ -0,0 +1,84 @@ +# coding: utf-8 +require 'barr/blocks/spotify' +require './spec/mocks/spotify' + +RSpec.describe Barr::Blocks::Spotify do + let(:sys_cmd) { Hash.new(title: 'Tear In My Heart', + artist: ['Twenty One Pilots'].join(', '), + album: 'Blurryface') } + let(:dbus_play) { 'dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.PlayPause' } + let(:dbus_next) { 'dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Next' } + let(:dbus_prev) { 'dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Previous' } + + before do + allow(subject).to receive(:running?).and_return(true) + allow(subject).to receive(:sys_cmd).and_return(sys_cmd) + allow(subject).to receive(:spotify_iface).and_return(SpotifyDbusMock.new) + end + + describe '#initialize' do + it 'sets the default options' do + expect(subject.view_opts[:artist]).to eq true + expect(subject.view_opts[:buttons]).to eq true + expect(subject.view_opts[:title]).to eq true + end + + it 'sets a DBus connection' do + subject { described_class.new } + expect(subject.spotify_iface).to be_a(SpotifyDbusMock) + end + end + + describe '#update' do + context 'with everything enabled' do + before { subject.update! } + + it 'sets the output correctly' do + expect(subject.output).to eq("Twenty One Pilots - Tear In My Heart %{A:#{dbus_prev}:}%{A} %{A:#{dbus_play}:}%{A} %{A:#{dbus_next}:}%{A}") + end + end + + context 'with only artist enabled' do + subject { described_class.new title: false, buttons: false } + + before { subject.update! } + + it 'sets the output correctly' do + expect(subject.output).to eq('Twenty One Pilots') + end + end + + context 'with only title enabled' do + subject { described_class.new artist: false, buttons: false } + + before { subject.update! } + + it 'sets the output correctly' do + expect(subject.output).to eq('Tear In My Heart') + end + end + + context 'with only buttons enabled' do + subject { described_class.new title: false, artist: false } + + before { subject.update! } + + it 'sets the output correctly' do + expect(subject.output).to eq("%{A:#{dbus_prev}:}%{A} %{A:#{dbus_play}:}%{A} %{A:#{dbus_next}:}%{A}") + end + end + + context 'when nothing is playing' do + subject { described_class.new buttons: false } + + before do + allow(subject).to receive(:running?).and_return(false) + subject.update! + end + + it 'sets the output correctly' do + expect(subject.output).to eq('None') + end + end + end +end diff --git a/spec/mocks/spotify.rb b/spec/mocks/spotify.rb new file mode 100644 index 0000000..5ccc74e --- /dev/null +++ b/spec/mocks/spotify.rb @@ -0,0 +1,19 @@ +class SpotifyDbusMock + def [](_key) + MetadataMock.new + end +end + +class MetadataMock + def [](key) + if key == 'xesam:title' + return 'Tear In My Heart' + elsif key == 'xesam:artist' + return ['Twenty One Pilots'] + elsif key == 'xesam:album' + return 'Blurryface' + else + raise "I don't know what to do with that key" + end + end +end