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.
Files changed (54) hide show
  1. data/Gemfile +2 -0
  2. data/Gemfile.lock +87 -0
  3. data/README.md +139 -0
  4. data/Rakefile +185 -0
  5. data/bin/play +49 -0
  6. data/config.ru +10 -0
  7. data/db/migrate/01_create_schema.rb +55 -0
  8. data/lib/play.rb +73 -0
  9. data/lib/play/album.rb +6 -0
  10. data/lib/play/app.rb +118 -0
  11. data/lib/play/app/api.rb +110 -0
  12. data/lib/play/artist.rb +21 -0
  13. data/lib/play/client.rb +67 -0
  14. data/lib/play/core_ext/hash.rb +6 -0
  15. data/lib/play/history.rb +7 -0
  16. data/lib/play/library.rb +58 -0
  17. data/lib/play/office.rb +34 -0
  18. data/lib/play/song.rb +97 -0
  19. data/lib/play/templates/album_songs.mustache +5 -0
  20. data/lib/play/templates/artist_songs.mustache +5 -0
  21. data/lib/play/templates/index.mustache +18 -0
  22. data/lib/play/templates/layout.mustache +23 -0
  23. data/lib/play/templates/now_playing.mustache +5 -0
  24. data/lib/play/templates/play_history.mustache +3 -0
  25. data/lib/play/templates/profile.mustache +24 -0
  26. data/lib/play/templates/search.mustache +3 -0
  27. data/lib/play/templates/show_song.mustache +9 -0
  28. data/lib/play/templates/song.mustache +21 -0
  29. data/lib/play/user.rb +59 -0
  30. data/lib/play/views/album_songs.rb +9 -0
  31. data/lib/play/views/artist_songs.rb +9 -0
  32. data/lib/play/views/index.rb +9 -0
  33. data/lib/play/views/layout.rb +6 -0
  34. data/lib/play/views/now_playing.rb +17 -0
  35. data/lib/play/views/play_history.rb +9 -0
  36. data/lib/play/views/profile.rb +9 -0
  37. data/lib/play/views/search.rb +9 -0
  38. data/lib/play/views/show_song.rb +19 -0
  39. data/lib/play/vote.rb +7 -0
  40. data/play.gemspec +129 -0
  41. data/play.yml.example +22 -0
  42. data/public/css/base.css +129 -0
  43. data/test/helper.rb +33 -0
  44. data/test/spec/mini.rb +24 -0
  45. data/test/test_api.rb +118 -0
  46. data/test/test_app.rb +57 -0
  47. data/test/test_artist.rb +15 -0
  48. data/test/test_client.rb +11 -0
  49. data/test/test_library.rb +19 -0
  50. data/test/test_office.rb +26 -0
  51. data/test/test_play.rb +17 -0
  52. data/test/test_song.rb +78 -0
  53. data/test/test_user.rb +21 -0
  54. metadata +299 -0
data/config.ru ADDED
@@ -0,0 +1,10 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/lib/play')
2
+
3
+ require 'play'
4
+ require 'omniauth/oauth'
5
+ oauth = Play.config
6
+
7
+ use Rack::Session::Cookie
8
+ use OmniAuth::Strategies::GitHub, oauth['gh_key'], oauth['gh_secret']
9
+
10
+ run Play::App
@@ -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
@@ -0,0 +1,6 @@
1
+ module Play
2
+ class Album < ActiveRecord::Base
3
+ has_many :songs
4
+ belongs_to :artist
5
+ end
6
+ end
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
@@ -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
@@ -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
@@ -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