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

WIP: Refactor and Gemify

This commit is contained in:
Lee Machin 2013-12-11 00:42:09 +00:00
parent d4a8cfc4a3
commit 5e45aad95a
21 changed files with 424 additions and 236 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

134
bin/mpd_client Executable file
View File

@ -0,0 +1,134 @@
#!/usr/bin/env ruby
ENV['RACK_ENV'] ||= 'development'
require 'bundler'
Bundler.setup
Bundler.require(:default, ENV['RACK_ENV'])
require 'sinatra'
require 'sinatra/asset_pipeline'
require 'sass'
require 'json'
require 'cgi'
require 'active_support/core_ext/hash/slice'
require File.expand_path('../lib/mpd_client', __dir__)
module MPDClient
class Application < Sinatra::Base
set server: 'thin'
set :root, File.expand_path('../', __dir__).to_s
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!
# TODO: Figure out why failing to supplying args breaks stuff
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: Song.queue.map(&:to_h) }.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.map(&:to_h).to_json
else
Album.all.to_json
end
end
get '/artists' do
content_type 'application/json'
Artist.all.map(&:to_h).to_json
end
get '/songs' do
content_type 'application/json'
if query = params.slice(:artist, :album) and !query.empty?
Song.by(**query).map(&:to_h).to_json
else
Song.all.sort.map(&:to_h).to_json
end
end
get '/queue' do
content_type 'application/json'
{ data: Song.queue.map(&:to_h) }.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'
Control.volume(params[:value])
end
end
end
end
MPDClient::Application.run!

View File

@ -1,6 +0,0 @@
require 'bundler'
Bundler.setup
require './mpd_client'
run MPDClient

43
lib/mpd_client.rb Normal file
View File

@ -0,0 +1,43 @@
require 'forwardable'
require 'ruby-mpd'
require 'set'
require File.expand_path('mpd_client/class_to_proc', __dir__)
module MPDClient
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__)
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

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

@ -0,0 +1,48 @@
module MPDClient
class Album
include ClassToProc
include Enumerable
extend Forwardable
delegate %i(artist genre) => :@first_song
def initialize(album)
@songs = MPDClient::Song.by(album: album)
@first_song = @songs.first
end
def each(&block)
@songs.each(&block)
end
def title
@first_song.album
end
def year
@first_song.year
end
def to_h
{
title: title,
artist: artist,
genre: genre,
year: year,
songs: self.map(&:to_h)
}
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

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

@ -0,0 +1,12 @@
module MPDClient
class Artist
include ClassToProc
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

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

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

@ -0,0 +1,59 @@
module MPDClient
class Song
include ClassToProc
include Comparable
extend Forwardable
delegate %i(id track artist album title time pos) => :@song
def initialize(song, pos: nil)
@song = song
end
def playing?
self == self.class.current_song
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 queue
MPDClient.conn.queue.map(&self)
end
def current_song
new(MPDClient.conn.current_song)
end
end
end
end

View File

@ -0,0 +1,3 @@
module MPDClient
VERSION = "0.0.1"
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

32
mpd_client.gemspec Normal file
View File

@ -0,0 +1,32 @@
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'mpd_client/version'
Gem::Specification.new do |spec|
spec.name = "mpd_client"
spec.version = MPDClient::VERSION
spec.authors = ["Dan Barber", "Lee Machin"]
spec.email = ["dan@new-bamboo.co.uk", "lee@new-bamboo.co.uk"]
spec.description = %q{Tasty web interface for MPD}
spec.summary = %q{Tasty web interface for MPD}
spec.homepage = ""
spec.files = `git ls-files`.split($/)
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ["lib"]
spec.add_development_dependency "bundler", "~> 1.3"
spec.add_development_dependency "rake"
spec.add_development_dependency "rspec"
spec.add_development_dependency "rspec-mocks"
spec.add_development_dependency "pry"
spec.add_dependency "thin"
spec.add_dependency "sinatra"
spec.add_dependency "sinatra-contrib"
spec.add_dependency "sinatra-asset-pipeline"
spec.add_dependency "ruby-mpd"
spec.add_dependency "active_support"
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