1
0
mirror of https://github.com/danbee/mpd-client synced 2025-03-04 08:39:09 +00:00

Merge pull request #1 from leemachin/gem-extract

WIP: Refactor and Gemify (for convenience)
This commit is contained in:
Daniel Barber 2013-12-12 03:52:54 -08:00
commit f8f8b9d194
33 changed files with 580 additions and 289 deletions

18
.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
*.gem
*.rbc
.bundle
.config
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp
.sass-cache

View File

@ -2,12 +2,15 @@ source 'https://rubygems.org'
ruby '2.0.0'
# todo: get everything working before going whole hog with the gem
# gemspec
gem 'sinatra'
gem 'sinatra-contrib'
gem 'sinatra-asset-pipeline'
gem 'foreman'
gem 'ruby-mpd', git: 'git@github.com:archSeer/ruby-mpd.git'
gem 'active_support', require: false
group :development, :test do
gem 'pry'

View File

@ -7,6 +7,9 @@ GIT
GEM
remote: https://rubygems.org/
specs:
active_support (3.0.0)
activesupport (= 3.0.0)
activesupport (3.0.0)
backports (3.3.5)
coderay (1.0.9)
coffee-script (2.2.0)
@ -29,7 +32,7 @@ GEM
method_source (~> 0.8)
slop (~> 3.4)
rack (1.5.2)
rack-protection (1.5.0)
rack-protection (1.5.1)
rack
rack-test (0.6.2)
rack (>= 1.0)
@ -45,11 +48,11 @@ GEM
sass (3.2.12)
shotgun (0.9)
rack (>= 1.0)
sinatra (1.4.3)
sinatra (1.4.4)
rack (~> 1.4)
rack-protection (~> 1.4)
tilt (~> 1.3, >= 1.3.4)
sinatra-asset-pipeline (0.2.1)
sinatra-asset-pipeline (0.3.3)
coffee-script
rake
sass
@ -65,12 +68,12 @@ GEM
sinatra (~> 1.4.0)
tilt (~> 1.3)
slop (3.4.6)
sprockets (2.10.0)
sprockets (2.10.1)
hike (~> 1.2)
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sprockets-helpers (1.0.1)
sprockets-helpers (1.1.0)
sprockets (~> 2.0)
sprockets-sass (1.0.2)
sprockets (~> 2.0)
@ -86,6 +89,7 @@ PLATFORMS
ruby
DEPENDENCIES
active_support
foreman
pry
rspec

View File

@ -1,6 +1,10 @@
ENV['RACK_ENV'] ||= 'development'
require 'bundler'
Bundler.setup
require './mpd_client'
Bundler.require(:default, ENV['RACK_ENV'])
run MPDClient
require File.expand_path('lib/mpd_client', __dir__)
run MPDClient::Webserver

49
lib/mpd_client.rb Normal file
View File

@ -0,0 +1,49 @@
require 'forwardable'
require 'ruby-mpd'
require 'set'
require 'json'
module MPDClient
autoload :Jsonable, File.expand_path('mpd_client/jsonable.rb', __dir__)
autoload :ClassToProc, File.expand_path('mpd_client/class_to_proc.rb', __dir__)
autoload :Connection, File.expand_path('mpd_client/connection.rb', __dir__)
autoload :Song, File.expand_path('mpd_client/song.rb', __dir__)
autoload :Album, File.expand_path('mpd_client/album.rb', __dir__)
autoload :Artist, File.expand_path('mpd_client/artist.rb', __dir__)
autoload :Control, File.expand_path('mpd_client/control.rb', __dir__)
autoload :Queue, File.expand_path('mpd_client/queue.rb', __dir__)
# Don't want to automatically require this thing
autoload :Webserver, File.expand_path('mpd_client/webserver.rb', __dir__)
MPD_HOST = ENV.fetch('MPD_HOST', 'localhost')
MPD_PORT = ENV.fetch('MPD_PORT', 6600)
def self.connect!
@conn ||= Connection.new(MPD_HOST, MPD_PORT)
@conn.connect unless @conn.connected?
@conn
end
def self.conn
@conn
end
def self.status
self.conn.status
end
def self.listen(&block)
self.conn.instance_eval(&block)
end
def self.connect_user(conn)
self.conn.connected_users << conn
end
def self.disconnect_user(conn)
self.conn.connected_users.delete(conn)
end
end

47
lib/mpd_client/album.rb Normal file
View File

@ -0,0 +1,47 @@
module MPDClient
class Album
include ClassToProc
include Jsonable
extend Forwardable
delegate %i(artist genre) => :@first_song
def initialize(album)
@first_song = MPDClient::Song.by(album: album).first
end
def title
@first_song.album
end
def year
@first_song.date
end
def <=>(other)
year <=> other.year
end
def to_h
{
title: title,
artist: artist,
genre: genre,
year: year
}
end
class << self
def all
MPDClient.conn.albums.sort.map(&self)
end
def by_artist(artist)
MPDClient.conn.albums(artist).map(&self)
end
end
end
end

25
lib/mpd_client/artist.rb Normal file
View File

@ -0,0 +1,25 @@
module MPDClient
class Artist
include ClassToProc
include Jsonable
attr :name
def initialize(name)
@name = name
end
def to_h
{
name: name
}
end
class << self
def all
MPDClient.conn.artists.sort.map(&self)
end
end
end
end

View File

@ -0,0 +1,11 @@
module MPDClient
module ClassToProc
def self.included(receiver)
receiver.define_singleton_method :to_proc do
proc { |*args| new(*args) }
end
end
end
end

View File

@ -0,0 +1,16 @@
module MPDClient
class Connection < MPD
attr_accessor :connected_users
def initialize(host, port, opts = {})
@connected_users = Set.new
super host, port, opts.merge(callbacks: true)
end
def each_conn(&block)
connected_users.each(&block)
end
end
end

35
lib/mpd_client/control.rb Normal file
View File

@ -0,0 +1,35 @@
module MPDClient
class Control
class << self
def controls
%i(play stop next previous pause)
end
def play(pos)
MPDClient.conn.play(pos)
end
def stop
MPDClient.conn.stop
end
def next
MPDClient.conn.next
end
def previous
MPDClient.conn.previous
end
def pause
MPDClient.conn.pause = !MPDClient.conn.paused?
end
def volume(value)
MPDClient.conn.volume = value
end
end
end
end

View File

@ -0,0 +1,15 @@
module MPDClient
module Jsonable
def self.included(receiver)
receiver.send(:include, InstanceMethods)
end
module InstanceMethods
def to_json(*args)
to_h.to_json(*args)
end
end
end
end

28
lib/mpd_client/queue.rb Normal file
View File

@ -0,0 +1,28 @@
module MPDClient
class Queue
include Enumerable
include Jsonable
attr :songs
def initialize
@songs = fetch_songs
end
def each(&block)
songs.each(&block)
end
def to_h
map(&:to_h)
end
private
def fetch_songs
MPDClient.conn.queue.map(&Song)
end
end
end

0
lib/mpd_client/server.rb Normal file
View File

61
lib/mpd_client/song.rb Normal file
View File

@ -0,0 +1,61 @@
module MPDClient
class Song
include ClassToProc
include Comparable
include Jsonable
extend Forwardable
delegate %i(id track artist album title genre date time pos) => :@song
def initialize(song, pos: nil)
@song = song
end
def playing?
if current = self.class.current_song
[artist, album, title] == [current.artist, current.album, current.title]
end
end
def length
time
end
def <=>(other)
[artist, album, title] <=> [other.artist, other.album, other.title]
end
def to_h
{
id: id,
track: track,
artist: artist,
album: album,
title: title,
length: length,
pos: pos,
playing: playing?
}
end
class << self
def by(**params)
params.delete_if {|_, v| v.nil? }
MPDClient.conn.where(params).map(&self)
end
def all
MPDClient.conn.songs.map(&self)
end
def current_song
if song = MPDClient.conn.current_song
new(song)
end
end
end
end
end

View File

@ -0,0 +1,3 @@
module MPDClient
VERSION = "0.0.1"
end

119
lib/mpd_client/webserver.rb Executable file
View File

@ -0,0 +1,119 @@
require 'sinatra'
require 'sinatra/asset_pipeline'
require 'sass'
require 'cgi'
require 'active_support/core_ext/hash/slice'
module MPDClient
class Webserver < Sinatra::Base
set server: 'thin'
set :root, File.expand_path('../../', __dir__)
set :assets_precompile, %w(app.js app.css *.png *.jpg *.svg *.eot *.ttf *.woff)
set :assets_prefix, ['assets']
register Sinatra::AssetPipeline
register Sinatra::Namespace
MPDClient.connect!
MPDClient.listen do
on :song do
each_conn do |conn|
json = { type: 'status', data: status }.to_json
conn << "data: #{json}\n\n"
end
end
on :state do
each_conn do |conn|
json = { type: 'status', data: status }.to_json
conn << "data: #{json}\n\n"
end
end
on :playlist do
each_conn do |conn|
json = { type: 'queue', data: Queue.new }.to_json
conn << "data: #{json}\n\n"
end
end
on :time do |elapsed, total|
each_conn do |conn|
json = { type: 'time', data: [elapsed, total] }.to_json
conn << "data: #{json}\n\n"
end
end
end
get '/' do
erb :index
end
namespace '/api' do
get '/status' do
MPDClient.status.to_json
end
get '/stream', provides: 'text/event-stream' do
stream :keep_open do |conn|
MPDClient.connect_user(conn)
conn.callback { MPDClient.disconnect_user(conn) }
end
end
get '/albums' do
content_type 'application/json'
if params[:artist]
Album.by_artist(params[:artist]).sort.to_json
else
Album.all.to_json
end
end
get '/artists' do
content_type 'application/json'
Artist.all.to_json
end
get '/songs' do
content_type 'application/json'
if query = params.slice(:artist, :album) and !query.empty?
Song.by(**query).to_json
else
Song.all.sort.to_json
end
end
get '/queue' do
content_type 'application/json'
{ data: Queue.new }.to_json
end
put '/control/play' do
Control.play(params[:pos])
end
put '/control/:action' do
if Control.controls.include?(params[:action].to_sym)
Control.send(params[:action])
else
not_found
end
end
put '/control/volume/:value' do
content_type 'application/json'
unless Control.volume(params[:value].to_i)
status 422
end
end
end
end
end

View File

@ -1,24 +0,0 @@
require './models/mpd_connection'
class Album < Struct.new(:title, :artist, :genre, :year)
def initialize(album)
first_song = MPDConnection.mpd.where(album: album).first
self.title = first_song.album
self.artist = first_song.artist
self.genre = first_song.genre
self.year = first_song.date
end
def <=>(album)
year <=> album.year
end
def self.all
MPDConnection.mpd.albums.sort.map { |album| self.new(album) }
end
def self.by_artist(artist)
MPDConnection.mpd.albums(artist).map { |album| self.new(album) }
end
end

View File

@ -1,7 +0,0 @@
require './models/mpd_connection'
class Artist < Struct.new(:name)
def self.all
MPDConnection.mpd.artists.sort.map { |artist| self.new(artist) }
end
end

View File

@ -1,33 +0,0 @@
require './models/mpd_connection'
class Control
class << self
def controls
[:play, :stop, :next, :previous, :pause]
end
def play(pos = nil)
MPDConnection.mpd.play(pos)
end
def stop
MPDConnection.mpd.stop
end
def next
MPDConnection.mpd.next
end
def previous
MPDConnection.mpd.previous
end
def pause
MPDConnection.mpd.pause = !MPDConnection.mpd.paused?
end
def volume(value)
MPDConnection.mpd.volume = value
end
end
end

View File

@ -1,11 +0,0 @@
class MPDConnection
def self.mpd
@mpd ||= MPD.new('localhost', 6600, { callbacks: true })
@mpd.connect unless @mpd.connected?
@mpd
end
def self.status
self.mpd.status
end
end

View File

@ -1,36 +0,0 @@
require './models/mpd_connection'
class Song < Struct.new(:id, :track, :artist, :album, :title, :length, :pos, :playing)
def initialize(song, pos: nil, playing: false)
self.id = song.id
self.track = song.track
self.artist = song.artist
self.album = song.album
self.title = song.title
self.length = song.time
self.pos = pos
self.playing = playing
end
def <=>(song)
title <=> song.title
end
def self.queue
current_song = MPDConnection.mpd.status[:songid]
MPDConnection.mpd.queue.map { |song| self.new(song, playing: (song.id == current_song), pos: song.pos) }
end
def self.all
MPDConnection.mpd.songs.map { |album| self.new(album) }
end
def self.by_artist(artist)
MPDConnection.mpd.where(artist: artist).map { |song| self.new(song) }
end
def self.by_album(artist, album)
MPDConnection.mpd.where(artist: artist, album: album).map { |song| self.new(song) }
end
end

View File

@ -1,113 +0,0 @@
require 'bundler'
ENV['RACK_ENV'] ||= 'development'
Bundler.require(:default, ENV['RACK_ENV'])
require 'sinatra/asset_pipeline'
require 'sass'
require 'json'
require 'cgi'
require './models/mpd_connection'
require './models/control'
require './models/album'
require './models/artist'
require './models/song'
class MPDClient < Sinatra::Base
set server: 'thin', connections: []
set :assets_precompile, %w(app.js app.css *.png *.jpg *.svg *.eot *.ttf *.woff)
set :assets_prefix, 'assets'
register Sinatra::AssetPipeline
register Sinatra::Namespace
get '/' do
erb :index
end
def self.send_status
response = JSON({ type: 'status', data: MPDConnection.status })
settings.connections.each { |out| out << "data: #{response}\n\n" }
end
def self.send_queue
response = JSON({ type: 'queue', data: Song.queue.map(&:to_h) })
settings.connections.each { |out| out << "data: #{response}\n\n" }
end
def self.send_time(elapsed, total)
response = JSON({ type: 'time', data: [elapsed, total] })
settings.connections.each { |out| out << "data: #{response}\n\n" }
end
MPDConnection.mpd.on(:song) { |song| send_status }
MPDConnection.mpd.on(:state) { |state| send_status }
MPDConnection.mpd.on(:playlist) { |playlist| send_queue }
MPDConnection.mpd.on(:time) { |elapsed, total| send_time(elapsed, total) }
namespace '/api' do
get '/status' do
JSON MPDConnection.status
end
get '/stream', provides: 'text/event-stream' do
stream :keep_open do |out|
settings.connections << out
out.callback { settings.connections.delete(out) }
end
end
get '/albums' do
content_type 'application/json'
if params[:artist]
JSON Album.by_artist(CGI.unescape(params[:artist])).sort.map(&:to_h)
else
JSON Album.all.map(&:to_h)
end
end
get '/artists' do
content_type 'application/json'
JSON Artist.all.map(&:to_h)
end
get '/songs' do
content_type 'application/json'
if params[:artist] && params[:album]
JSON Song.by_album(CGI.unescape(params[:artist]), CGI.unescape(params[:album])).map(&:to_h)
elsif params[:artist]
JSON Song.by_artist(CGI.unescape(params[:artist])).map(&:to_h)
else
JSON Song.all.sort.map(&:to_h)
end
end
get '/queue' do
content_type 'application/json'
JSON({ data: Song.queue.map(&:to_h) })
end
put '/control/play' do
Control.play(params[:pos])
end
put '/control/:action' do
if Control.controls.include?(params[:action].to_sym)
Control.send(params[:action])
else
not_found
end
end
put '/control/volume/:value' do
content_type 'application/json'
Control.volume(params[:value])
end
end
end

View File

@ -1,30 +1,34 @@
require 'spec_helper'
describe Album do
describe MPDClient::Album do
subject { MPDClient::Album }
let(:song1) { MPD::Song.new({ album: 'Back in Black', genre: 'Rock', date: '1980' }) }
let(:song2) { MPD::Song.new({ album: 'Highway to Hell', genre: 'Rock', date: '1979' }) }
before do
MPDConnection.mpd.stub(:albums).and_return([song1.album, song2.album])
MPDConnection.mpd.stub(:search).and_return([song1, song2])
MPDConnection.mpd.stub(:search).with(:album, song1.album).and_return([song1])
MPDConnection.mpd.stub(:search).with(:album, song2.album).and_return([song2])
MPDClient.conn.tap do |mpd|
mpd.stub(:albums).and_return([song1.album, song2.album])
mpd.stub(:where).and_return([song1, song2])
mpd.stub(:where).with(album: song1.album).and_return([song1])
mpd.stub(:where).with(album: song2.album).and_return([song2])
end
end
it 'has attributes based on first song' do
album = Album.new(song1.album)
album = subject.new(song1.album)
expect(album.title).to eq(song1.album)
expect(album.genre).to eq(song1.genre)
expect(album.year).to eq(song1.date)
end
it 'should return a list of albums' do
expect(Album.by_artist('AC/DC')).to have(2).items
expect(subject.by_artist('AC/DC')).to have(2).items
end
it 'should sort the albums by year' do
albums = Album.by_artist('AC/DC')
albums = subject.by_artist('AC/DC')
expect(albums.sort.map(&:year)).to eq(['1979', '1980'])
end

View File

@ -0,0 +1,23 @@
require 'spec_helper'
describe MPDClient::Artist do
subject { MPDClient::Artist }
let(:artist) { subject.new('Alice Cooper') }
let(:artists) { ['Alice Cooper', 'Jimmy Eat World', 'Dream Theater'] }
before do
MPDClient.conn.stub(:artists).and_return(artists)
end
it 'has attributes' do
expect(artist.name).to eq('Alice Cooper')
end
it 'returns all artists' do
expect(subject.all).to have(3).items
expect(subject.all.map(&:name)).to eq(artists.sort)
end
end

View File

@ -0,0 +1,23 @@
require 'spec_helper'
describe MPDClient::Connection do
subject { MPDClient::Connection }
let(:conn) { subject.new('localhost', 6600) }
describe "#each_conn" do
let(:fake_users) { [1, 2, 3] }
it "enumerates the connected users" do
fake_users.each {|n| conn.connected_users << n }
conn.each_conn.to_a.should eq(fake_users)
end
end
describe "#connected_users" do
it "doesn't allow duplicate connections" do
3.times { conn.connected_users << "user" }
conn.connected_users.should have(1).item
end
end
end

View File

@ -0,0 +1,4 @@
require 'spec_helper'
describe MPDClient::Control do
end

View File

@ -1,16 +1,17 @@
require 'spec_helper'
describe Song do
describe MPDClient::Queue do
subject { MPDClient::Queue.new }
let(:song1) { MPD::Song.new({ title: 'Back in Black', album: 'Back in Black', genre: 'Rock', date: '1980' }) }
let(:song2) { MPD::Song.new({ title: 'Highway to Hell', album: 'Highway to Hell', genre: 'Rock', date: '1979' }) }
before do
MPDConnection.mpd.stub(:queue).and_return([song1, song2])
MPDClient.conn.stub(:queue).and_return([song1, song2])
end
it 'returns the queue of songs' do
queue = Song.queue
expect(queue).to have(2).items
end
its(:songs) { should have(2).items }
end

View File

@ -0,0 +1,30 @@
require 'spec_helper'
describe MPDClient::Song do
subject { MPDClient::Song }
let(:song1) { MPD::Song.new({ title: 'Back in Black', album: 'Back in Black', genre: 'Rock', date: '1980' }) }
let(:song2) { MPD::Song.new({ title: 'Highway to Hell', album: 'Highway to Hell', genre: 'Rock', date: '1979' }) }
before do
MPDClient.conn.stub(:queue).and_return([song1, song2])
end
describe "#playing?" do
let(:playing_song) { subject.new(song1) }
let(:not_playing_song) { subject.new(song2) }
before do
MPDClient.conn.stub(:current_song).and_return(song1)
end
it "should be true when the song is playing" do
playing_song.playing?.should eq(true)
end
it "should be false when the song is not playing" do
not_playing_song.playing?.should eq(false)
end
end
end

View File

@ -0,0 +1,23 @@
require 'spec_helper'
describe MPDClient do
subject { MPDClient }
before do
subject.connect!
end
describe "#listen" do
it "exposes `Connection#on` for easy event listening" do
subject.conn.should_receive(:on).exactly(3).times
subject.listen do
on(:first) { "something" }
on(:second) { "something" }
on(:third) { "something" }
end
end
end
end

View File

@ -1,18 +0,0 @@
require 'spec_helper'
describe Artist do
let(:artist) { Artist.new('Alice Cooper') }
let(:artists) { ['Alice Cooper', 'Jimmy Eat World', 'Dream Theater'] }
it 'has attributes' do
expect(artist.name).to eq('Alice Cooper')
end
it 'returns all artists' do
MPDConnection.mpd.stub(:artists).and_return(artists)
expect(Artist.all).to have(3).items
expect(Artist.all.map(&:name)).to eq(artists.sort)
end
end

View File

@ -1,4 +0,0 @@
require 'spec_helper'
describe Control do
end

View File

@ -1,8 +0,0 @@
require 'spec_helper'
describe MPDClient do
it 'should respond to GET' do
get '/'
expect(last_response.status).to eq(404)
end
end

View File

@ -1,20 +1,19 @@
ENV['RACK_ENV'] = 'test'
require File.join(File.dirname(__FILE__), '..', 'mpd_client.rb')
require 'bundler'
Bundler.setup
Bundler.require(:default, ENV['RACK_ENV'])
require 'sinatra'
require File.expand_path('../lib/mpd_client', __dir__)
require 'rspec'
require 'rspec/mocks'
require 'rack/test'
# setup test environment
set :environment, :test
set :run, false
set :raise_errors, true
set :logging, false
def app
MPDClient
end
RSpec.configure do |config|
config.include Rack::Test::Methods
config.before(:each) do
allow_message_expectations_on_nil
MPDClient::Connection.any_instance.stub(:connected?).and_return(true)
end
end