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:
commit
f8f8b9d194
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal 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
|
||||||
5
Gemfile
5
Gemfile
@ -2,12 +2,15 @@ source 'https://rubygems.org'
|
|||||||
|
|
||||||
ruby '2.0.0'
|
ruby '2.0.0'
|
||||||
|
|
||||||
|
# todo: get everything working before going whole hog with the gem
|
||||||
|
# gemspec
|
||||||
|
|
||||||
gem 'sinatra'
|
gem 'sinatra'
|
||||||
gem 'sinatra-contrib'
|
gem 'sinatra-contrib'
|
||||||
gem 'sinatra-asset-pipeline'
|
gem 'sinatra-asset-pipeline'
|
||||||
gem 'foreman'
|
gem 'foreman'
|
||||||
|
|
||||||
gem 'ruby-mpd', git: 'git@github.com:archSeer/ruby-mpd.git'
|
gem 'ruby-mpd', git: 'git@github.com:archSeer/ruby-mpd.git'
|
||||||
|
gem 'active_support', require: false
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'pry'
|
gem 'pry'
|
||||||
|
|||||||
14
Gemfile.lock
14
Gemfile.lock
@ -7,6 +7,9 @@ GIT
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
|
active_support (3.0.0)
|
||||||
|
activesupport (= 3.0.0)
|
||||||
|
activesupport (3.0.0)
|
||||||
backports (3.3.5)
|
backports (3.3.5)
|
||||||
coderay (1.0.9)
|
coderay (1.0.9)
|
||||||
coffee-script (2.2.0)
|
coffee-script (2.2.0)
|
||||||
@ -29,7 +32,7 @@ GEM
|
|||||||
method_source (~> 0.8)
|
method_source (~> 0.8)
|
||||||
slop (~> 3.4)
|
slop (~> 3.4)
|
||||||
rack (1.5.2)
|
rack (1.5.2)
|
||||||
rack-protection (1.5.0)
|
rack-protection (1.5.1)
|
||||||
rack
|
rack
|
||||||
rack-test (0.6.2)
|
rack-test (0.6.2)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
@ -45,11 +48,11 @@ GEM
|
|||||||
sass (3.2.12)
|
sass (3.2.12)
|
||||||
shotgun (0.9)
|
shotgun (0.9)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
sinatra (1.4.3)
|
sinatra (1.4.4)
|
||||||
rack (~> 1.4)
|
rack (~> 1.4)
|
||||||
rack-protection (~> 1.4)
|
rack-protection (~> 1.4)
|
||||||
tilt (~> 1.3, >= 1.3.4)
|
tilt (~> 1.3, >= 1.3.4)
|
||||||
sinatra-asset-pipeline (0.2.1)
|
sinatra-asset-pipeline (0.3.3)
|
||||||
coffee-script
|
coffee-script
|
||||||
rake
|
rake
|
||||||
sass
|
sass
|
||||||
@ -65,12 +68,12 @@ GEM
|
|||||||
sinatra (~> 1.4.0)
|
sinatra (~> 1.4.0)
|
||||||
tilt (~> 1.3)
|
tilt (~> 1.3)
|
||||||
slop (3.4.6)
|
slop (3.4.6)
|
||||||
sprockets (2.10.0)
|
sprockets (2.10.1)
|
||||||
hike (~> 1.2)
|
hike (~> 1.2)
|
||||||
multi_json (~> 1.0)
|
multi_json (~> 1.0)
|
||||||
rack (~> 1.0)
|
rack (~> 1.0)
|
||||||
tilt (~> 1.1, != 1.3.0)
|
tilt (~> 1.1, != 1.3.0)
|
||||||
sprockets-helpers (1.0.1)
|
sprockets-helpers (1.1.0)
|
||||||
sprockets (~> 2.0)
|
sprockets (~> 2.0)
|
||||||
sprockets-sass (1.0.2)
|
sprockets-sass (1.0.2)
|
||||||
sprockets (~> 2.0)
|
sprockets (~> 2.0)
|
||||||
@ -86,6 +89,7 @@ PLATFORMS
|
|||||||
ruby
|
ruby
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
active_support
|
||||||
foreman
|
foreman
|
||||||
pry
|
pry
|
||||||
rspec
|
rspec
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
|
ENV['RACK_ENV'] ||= 'development'
|
||||||
|
|
||||||
require 'bundler'
|
require 'bundler'
|
||||||
|
|
||||||
Bundler.setup
|
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
49
lib/mpd_client.rb
Normal 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
47
lib/mpd_client/album.rb
Normal 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
25
lib/mpd_client/artist.rb
Normal 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
|
||||||
11
lib/mpd_client/class_to_proc.rb
Normal file
11
lib/mpd_client/class_to_proc.rb
Normal 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
|
||||||
16
lib/mpd_client/connection.rb
Normal file
16
lib/mpd_client/connection.rb
Normal 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
35
lib/mpd_client/control.rb
Normal 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
|
||||||
15
lib/mpd_client/jsonable.rb
Normal file
15
lib/mpd_client/jsonable.rb
Normal 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
28
lib/mpd_client/queue.rb
Normal 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
0
lib/mpd_client/server.rb
Normal file
61
lib/mpd_client/song.rb
Normal file
61
lib/mpd_client/song.rb
Normal 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
|
||||||
3
lib/mpd_client/version.rb
Normal file
3
lib/mpd_client/version.rb
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module MPDClient
|
||||||
|
VERSION = "0.0.1"
|
||||||
|
end
|
||||||
119
lib/mpd_client/webserver.rb
Executable file
119
lib/mpd_client/webserver.rb
Executable 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
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
113
mpd_client.rb
113
mpd_client.rb
@ -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
|
|
||||||
@ -1,30 +1,34 @@
|
|||||||
require 'spec_helper'
|
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(: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' }) }
|
let(:song2) { MPD::Song.new({ album: 'Highway to Hell', genre: 'Rock', date: '1979' }) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
MPDConnection.mpd.stub(:albums).and_return([song1.album, song2.album])
|
MPDClient.conn.tap do |mpd|
|
||||||
MPDConnection.mpd.stub(:search).and_return([song1, song2])
|
mpd.stub(:albums).and_return([song1.album, song2.album])
|
||||||
MPDConnection.mpd.stub(:search).with(:album, song1.album).and_return([song1])
|
mpd.stub(:where).and_return([song1, song2])
|
||||||
MPDConnection.mpd.stub(:search).with(:album, song2.album).and_return([song2])
|
mpd.stub(:where).with(album: song1.album).and_return([song1])
|
||||||
|
mpd.stub(:where).with(album: song2.album).and_return([song2])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has attributes based on first song' do
|
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.title).to eq(song1.album)
|
||||||
expect(album.genre).to eq(song1.genre)
|
expect(album.genre).to eq(song1.genre)
|
||||||
expect(album.year).to eq(song1.date)
|
expect(album.year).to eq(song1.date)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'should return a list of albums' do
|
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
|
end
|
||||||
|
|
||||||
it 'should sort the albums by year' do
|
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'])
|
expect(albums.sort.map(&:year)).to eq(['1979', '1980'])
|
||||||
end
|
end
|
||||||
|
|
||||||
23
spec/lib/mpd_client/artist_spec.rb
Normal file
23
spec/lib/mpd_client/artist_spec.rb
Normal 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
|
||||||
23
spec/lib/mpd_client/connection_spec.rb
Normal file
23
spec/lib/mpd_client/connection_spec.rb
Normal 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
|
||||||
4
spec/lib/mpd_client/control_spec.rb
Normal file
4
spec/lib/mpd_client/control_spec.rb
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe MPDClient::Control do
|
||||||
|
end
|
||||||
@ -1,16 +1,17 @@
|
|||||||
require 'spec_helper'
|
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(: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' }) }
|
let(:song2) { MPD::Song.new({ title: 'Highway to Hell', album: 'Highway to Hell', genre: 'Rock', date: '1979' }) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
MPDConnection.mpd.stub(:queue).and_return([song1, song2])
|
MPDClient.conn.stub(:queue).and_return([song1, song2])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the queue of songs' do
|
its(:songs) { should have(2).items }
|
||||||
queue = Song.queue
|
|
||||||
expect(queue).to have(2).items
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
30
spec/lib/mpd_client/song_spec.rb
Normal file
30
spec/lib/mpd_client/song_spec.rb
Normal 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
|
||||||
23
spec/lib/mpd_client_spec.rb
Normal file
23
spec/lib/mpd_client_spec.rb
Normal 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
|
||||||
@ -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
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
describe Control do
|
|
||||||
end
|
|
||||||
@ -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
|
|
||||||
@ -1,20 +1,19 @@
|
|||||||
ENV['RACK_ENV'] = 'test'
|
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'
|
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|
|
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
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user