play 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +2 -0
- data/Gemfile.lock +87 -0
- data/README.md +139 -0
- data/Rakefile +185 -0
- data/bin/play +49 -0
- data/config.ru +10 -0
- data/db/migrate/01_create_schema.rb +55 -0
- data/lib/play.rb +73 -0
- data/lib/play/album.rb +6 -0
- data/lib/play/app.rb +118 -0
- data/lib/play/app/api.rb +110 -0
- data/lib/play/artist.rb +21 -0
- data/lib/play/client.rb +67 -0
- data/lib/play/core_ext/hash.rb +6 -0
- data/lib/play/history.rb +7 -0
- data/lib/play/library.rb +58 -0
- data/lib/play/office.rb +34 -0
- data/lib/play/song.rb +97 -0
- data/lib/play/templates/album_songs.mustache +5 -0
- data/lib/play/templates/artist_songs.mustache +5 -0
- data/lib/play/templates/index.mustache +18 -0
- data/lib/play/templates/layout.mustache +23 -0
- data/lib/play/templates/now_playing.mustache +5 -0
- data/lib/play/templates/play_history.mustache +3 -0
- data/lib/play/templates/profile.mustache +24 -0
- data/lib/play/templates/search.mustache +3 -0
- data/lib/play/templates/show_song.mustache +9 -0
- data/lib/play/templates/song.mustache +21 -0
- data/lib/play/user.rb +59 -0
- data/lib/play/views/album_songs.rb +9 -0
- data/lib/play/views/artist_songs.rb +9 -0
- data/lib/play/views/index.rb +9 -0
- data/lib/play/views/layout.rb +6 -0
- data/lib/play/views/now_playing.rb +17 -0
- data/lib/play/views/play_history.rb +9 -0
- data/lib/play/views/profile.rb +9 -0
- data/lib/play/views/search.rb +9 -0
- data/lib/play/views/show_song.rb +19 -0
- data/lib/play/vote.rb +7 -0
- data/play.gemspec +129 -0
- data/play.yml.example +22 -0
- data/public/css/base.css +129 -0
- data/test/helper.rb +33 -0
- data/test/spec/mini.rb +24 -0
- data/test/test_api.rb +118 -0
- data/test/test_app.rb +57 -0
- data/test/test_artist.rb +15 -0
- data/test/test_client.rb +11 -0
- data/test/test_library.rb +19 -0
- data/test/test_office.rb +26 -0
- data/test/test_play.rb +17 -0
- data/test/test_song.rb +78 -0
- data/test/test_user.rb +21 -0
- metadata +299 -0
data/config.ru
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
class CreateSchema < ActiveRecord::Migration
|
|
2
|
+
def self.up
|
|
3
|
+
create_table :songs do |t|
|
|
4
|
+
t.string :title
|
|
5
|
+
t.string :path
|
|
6
|
+
t.integer :artist_id
|
|
7
|
+
t.integer :album_id
|
|
8
|
+
t.integer :playcount
|
|
9
|
+
t.boolean :queued, :default => false
|
|
10
|
+
t.boolean :now_playing, :default => false
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
create_table :artists do |t|
|
|
15
|
+
t.string :name
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
create_table :albums do |t|
|
|
20
|
+
t.string :name
|
|
21
|
+
t.integer :artist_id
|
|
22
|
+
t.timestamps
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
create_table :users do |t|
|
|
26
|
+
t.string :login
|
|
27
|
+
t.string :name
|
|
28
|
+
t.string :email
|
|
29
|
+
t.string :office_string
|
|
30
|
+
t.string :alias
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
create_table :votes do |t|
|
|
34
|
+
t.integer :song_id
|
|
35
|
+
t.integer :user_id
|
|
36
|
+
t.integer :artist_id
|
|
37
|
+
t.boolean :active, :default => true
|
|
38
|
+
t.timestamps
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
create_table :histories do |t|
|
|
42
|
+
t.integer :song_id
|
|
43
|
+
t.integer :artist_id
|
|
44
|
+
t.timestamps
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.down
|
|
49
|
+
drop_table :songs
|
|
50
|
+
drop_table :artists
|
|
51
|
+
drop_table :albums
|
|
52
|
+
drop_table :queues
|
|
53
|
+
drop_table :votes
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/play.rb
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
|
|
2
|
+
|
|
3
|
+
require "rubygems"
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
|
|
6
|
+
require 'active_record'
|
|
7
|
+
require 'audioinfo'
|
|
8
|
+
require 'sinatra/base'
|
|
9
|
+
require 'mustache/sinatra'
|
|
10
|
+
require 'digest'
|
|
11
|
+
require 'yajl'
|
|
12
|
+
|
|
13
|
+
require 'play/core_ext/hash'
|
|
14
|
+
|
|
15
|
+
require 'play/app'
|
|
16
|
+
require 'play/artist'
|
|
17
|
+
require 'play/album'
|
|
18
|
+
require 'play/client'
|
|
19
|
+
require 'play/history'
|
|
20
|
+
require 'play/library'
|
|
21
|
+
require 'play/office'
|
|
22
|
+
require 'play/song'
|
|
23
|
+
require 'play/views/layout'
|
|
24
|
+
require 'play/user'
|
|
25
|
+
require 'play/vote'
|
|
26
|
+
|
|
27
|
+
module Play
|
|
28
|
+
|
|
29
|
+
VERSION = '0.0.1'
|
|
30
|
+
|
|
31
|
+
# The path to your music library. All of the music underneath this directory
|
|
32
|
+
# will be added to the internal library.
|
|
33
|
+
#
|
|
34
|
+
# path - a String absolute path to your music
|
|
35
|
+
#
|
|
36
|
+
# Returns nothing.
|
|
37
|
+
def self.path=(path)
|
|
38
|
+
@path = path
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The path of the music library on-disk.
|
|
42
|
+
#
|
|
43
|
+
# Returns a String absolute path on the local file system.
|
|
44
|
+
def self.path
|
|
45
|
+
config['path']
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The song that's currently playing.
|
|
49
|
+
#
|
|
50
|
+
# Returns the Song object from the database that's currently playing.
|
|
51
|
+
def self.now_playing
|
|
52
|
+
Song.where(:now_playing => true).first
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The path to play.yml.
|
|
56
|
+
#
|
|
57
|
+
# Returns the String path to the configuration file.
|
|
58
|
+
def self.config_path
|
|
59
|
+
"#{ENV['HOME']}/.play.yml"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The configuration object for Play.
|
|
63
|
+
#
|
|
64
|
+
# Returns the Hash containing the configuration for Play. This includes:
|
|
65
|
+
#
|
|
66
|
+
# path - the String path to where your music is located
|
|
67
|
+
# gh_key - the Client ID from your GitHub app's OAuth settings
|
|
68
|
+
# gh_secret - the Client Secret from your GitHub app's OAuth settings
|
|
69
|
+
# office_url - the URL to an endpoint where we can see who's in your office
|
|
70
|
+
def self.config
|
|
71
|
+
YAML::load(File.open(config_path))
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/play/album.rb
ADDED
data/lib/play/app.rb
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
require 'play/app/api'
|
|
2
|
+
|
|
3
|
+
module Play
|
|
4
|
+
class App < Sinatra::Base
|
|
5
|
+
register Mustache::Sinatra
|
|
6
|
+
|
|
7
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
|
8
|
+
|
|
9
|
+
set :public, "#{dir}/../../public"
|
|
10
|
+
set :static, true
|
|
11
|
+
set :mustache, {
|
|
12
|
+
:namespace => Play,
|
|
13
|
+
:templates => "#{dir}/templates",
|
|
14
|
+
:views => "#{dir}/views"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def current_user
|
|
18
|
+
session['user_id'].blank? ? nil : User.find_by_id(session['user_id'])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
configure :development,:production do
|
|
22
|
+
# This should use Play.config eventually, but there's some weird loading
|
|
23
|
+
# problems right now with this file. So it goes. Dupe it for now.
|
|
24
|
+
config = YAML::load(File.open("#{ENV['HOME']}/.play.yml"))
|
|
25
|
+
ActiveRecord::Base.establish_connection(config['db'])
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
configure :test do
|
|
29
|
+
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3',
|
|
30
|
+
:database => ":memory:")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
before do
|
|
34
|
+
if current_user
|
|
35
|
+
@login = current_user.login
|
|
36
|
+
else
|
|
37
|
+
if ['production','test'].include?(ENV['RACK_ENV'])
|
|
38
|
+
redirect '/login' unless request.path_info =~ /\/login/ ||
|
|
39
|
+
request.path_info =~ /\/auth/ ||
|
|
40
|
+
request.path_info =~ /\/api/
|
|
41
|
+
else
|
|
42
|
+
# This will create a test user for you locally in development.
|
|
43
|
+
session['user_id'] = User.create(:login => 'user',
|
|
44
|
+
:email => 'play@example.com').id
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
get "/" do
|
|
50
|
+
@songs = Song.queue.all
|
|
51
|
+
mustache :index
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
get "/login" do
|
|
55
|
+
redirect '/auth/github'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
get '/auth/:name/callback' do
|
|
59
|
+
auth = request.env['omniauth.auth']
|
|
60
|
+
@user = User.authenticate(auth['user_info'])
|
|
61
|
+
session['user_id'] = @user.id
|
|
62
|
+
redirect '/'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
get "/now_playing" do
|
|
66
|
+
@song = Play.now_playing
|
|
67
|
+
mustache :now_playing
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
get "/add/:id" do
|
|
71
|
+
@song = Song.find(params[:id])
|
|
72
|
+
@song.enqueue!(current_user)
|
|
73
|
+
redirect '/'
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
get "/remove/:id" do
|
|
77
|
+
@song = Song.find(params[:id])
|
|
78
|
+
@song.dequeue!(current_user)
|
|
79
|
+
redirect '/'
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
get "/artist/*/album/*" do
|
|
83
|
+
@artist = Artist.where(:name => params[:splat].first).first
|
|
84
|
+
@album = @artist.albums.where(:name => params[:splat].last).first
|
|
85
|
+
@songs = @album.songs
|
|
86
|
+
mustache :album_songs
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
get "/artist/*" do
|
|
90
|
+
@artist = Artist.where(:name => params[:splat].first).first
|
|
91
|
+
@songs = @artist.songs
|
|
92
|
+
mustache :artist_songs
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
get "/song/:id" do
|
|
96
|
+
@song = Song.find(params[:id])
|
|
97
|
+
mustache :show_song
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
get "/search" do
|
|
101
|
+
@search = params[:q]
|
|
102
|
+
artist = Artist.where("LOWER(name) = ?", @search.downcase).first
|
|
103
|
+
redirect "/artist/#{URI.escape(artist.name)}" if artist
|
|
104
|
+
@songs = Song.where("title LIKE ?", "%#{@search}%").limit(100).all
|
|
105
|
+
mustache :search
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
get "/history" do
|
|
109
|
+
@songs = History.limit(100).order('created_at desc').collect(&:song)
|
|
110
|
+
mustache :play_history
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
get "/:login" do
|
|
114
|
+
@user = User.where(:login => params[:login]).first
|
|
115
|
+
mustache :profile
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
data/lib/play/app/api.rb
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module Play
|
|
2
|
+
class App < Sinatra::Base
|
|
3
|
+
get "/api/now_playing" do
|
|
4
|
+
song = Play.now_playing
|
|
5
|
+
music_response(song)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
post "/api/user/add_alias" do
|
|
9
|
+
user = User.find_by_login(params[:login])
|
|
10
|
+
if user
|
|
11
|
+
user.alias = params[:alias]
|
|
12
|
+
{ :success => user.save }.to_json
|
|
13
|
+
else
|
|
14
|
+
error "Couldn't find that user. Crap."
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
post "/api/import" do
|
|
19
|
+
if Play::Library.import_songs
|
|
20
|
+
{ :success => 'true' }.to_json
|
|
21
|
+
else
|
|
22
|
+
error "Had some problems importing into Play. Uh-oh."
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
post "/api/add_song" do
|
|
27
|
+
api_authenticate
|
|
28
|
+
artist = Play::Artist.find_by_name(params[:artist_name])
|
|
29
|
+
if artist
|
|
30
|
+
song = artist.songs.find_by_title(params[:song_title])
|
|
31
|
+
if song
|
|
32
|
+
song.enqueue!(api_user)
|
|
33
|
+
music_response(song)
|
|
34
|
+
else
|
|
35
|
+
error("Sorry, but we couldn't find that song.")
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
error("Sorry, but we couldn't find that artist.")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
post "/api/add_artist" do
|
|
43
|
+
api_authenticate
|
|
44
|
+
artist = Play::Artist.find_by_name(params[:artist_name])
|
|
45
|
+
if artist
|
|
46
|
+
{:song_titles => artist.enqueue!(api_user).collect(&:title),
|
|
47
|
+
:artist_name => artist.name}.to_json
|
|
48
|
+
else
|
|
49
|
+
error("Sorry, but we couldn't find that artist.")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
post "/api/remove" do
|
|
54
|
+
error "This hasn't been implemented yet. Whoops."
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
get "/api/search" do
|
|
58
|
+
songs = case params[:facet]
|
|
59
|
+
when 'artist'
|
|
60
|
+
artist = Artist.find_by_name(params[:q])
|
|
61
|
+
artist ? artist.songs : nil
|
|
62
|
+
when 'song'
|
|
63
|
+
Song.where(:title => params[:q])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
songs ? {:song_titles => songs.collect(&:title)}.to_json : error("Search. Problem?")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
post "/api/volume" do
|
|
70
|
+
if Play::Client.volume(params[:level].to_i)
|
|
71
|
+
{ :success => 'true' }.to_json
|
|
72
|
+
else
|
|
73
|
+
error "There's a problem adjusting the volume."
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
post "/api/pause" do
|
|
78
|
+
if Play::Client.pause
|
|
79
|
+
{ :success => 'true' }.to_json
|
|
80
|
+
else
|
|
81
|
+
error "There's a problem pausing."
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def api_user
|
|
86
|
+
Play::User.find_by_login(params[:user_login]) ||
|
|
87
|
+
Play::User.find_by_alias(params[:user_login])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def api_authenticate
|
|
91
|
+
if api_user
|
|
92
|
+
true
|
|
93
|
+
else
|
|
94
|
+
halt error("You must supply a valid `user_login` in your requests.")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def error(msg)
|
|
99
|
+
{ :error => msg }.to_json
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def music_response(song)
|
|
103
|
+
{
|
|
104
|
+
'artist_name' => song.artist_name,
|
|
105
|
+
'song_title' => song.title,
|
|
106
|
+
'album_name' => song.album_name
|
|
107
|
+
}.to_json
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
data/lib/play/artist.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Play
|
|
2
|
+
class Artist < ActiveRecord::Base
|
|
3
|
+
has_many :songs
|
|
4
|
+
has_many :albums
|
|
5
|
+
has_many :votes
|
|
6
|
+
|
|
7
|
+
# Queue up an artist. This will grab ten random tracks for this artist and
|
|
8
|
+
# queue 'em up.
|
|
9
|
+
#
|
|
10
|
+
# user - the User who is requesting the artist be queued
|
|
11
|
+
#
|
|
12
|
+
# Returns nothing.
|
|
13
|
+
def enqueue!(user)
|
|
14
|
+
songs.shuffle[0..9].collect do |song|
|
|
15
|
+
song.enqueue!(user)
|
|
16
|
+
song
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/play/client.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module Play
|
|
2
|
+
class Client
|
|
3
|
+
# The main event loop for an audio client. It will loop through each song
|
|
4
|
+
# in the queue, unless it's paused.
|
|
5
|
+
#
|
|
6
|
+
# Returns nothing.
|
|
7
|
+
def self.loop
|
|
8
|
+
while true
|
|
9
|
+
Signal.trap("INT") { exit! }
|
|
10
|
+
|
|
11
|
+
if paused?
|
|
12
|
+
sleep(1)
|
|
13
|
+
else
|
|
14
|
+
system("afplay", Song.play_next_in_queue.path)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The temp file we use to signify whether Play should be paused.
|
|
20
|
+
#
|
|
21
|
+
# Returns the String path of the pause file.
|
|
22
|
+
def self.pause_path
|
|
23
|
+
'/tmp/play_is_paused'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# "Pauses" a client by stopping currently playing songs and setting up the
|
|
27
|
+
# pause temp file.
|
|
28
|
+
#
|
|
29
|
+
# Returns nothing.
|
|
30
|
+
def self.pause
|
|
31
|
+
paused? ? `rm -f #{pause_path}` : `touch #{pause_path}`
|
|
32
|
+
`killall afplay > /dev/null 2>&1`
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Are we currently paused?
|
|
36
|
+
#
|
|
37
|
+
# Returns the Boolean value of whether we're paused.
|
|
38
|
+
def self.paused?
|
|
39
|
+
File.exist?(pause_path)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Are we currently playing? Look at the process list and check it out.
|
|
43
|
+
#
|
|
44
|
+
# Returns true if we're playing, false if we aren't.
|
|
45
|
+
def self.playing?
|
|
46
|
+
`ps aux | grep afplay | grep -v grep | wc -l | tr -d ' '`.chomp != '0'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Stop the music, and stop the music server.
|
|
50
|
+
#
|
|
51
|
+
# Returns nothing.
|
|
52
|
+
def self.stop
|
|
53
|
+
`killall afplay > /dev/null 2>&1`
|
|
54
|
+
`kill \`ps ax | grep "play -d" | cut -d ' ' -f 1\``
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Set the volume level of the client.
|
|
58
|
+
#
|
|
59
|
+
# number - The Integer volume level. This should be a number between 0
|
|
60
|
+
# and 10, with "0" being "muted" and "10" being "real real loud"
|
|
61
|
+
#
|
|
62
|
+
# Returns nothing.
|
|
63
|
+
def self.volume(number)
|
|
64
|
+
system "osascript -e 'set volume #{number}' 2>/dev/null"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|