play 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,6 @@
1
+ # From Sinatra's symbolize_keys port.
2
+ class Hash
3
+ def symbolize_keys
4
+ self.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ module Play
2
+ class History < ActiveRecord::Base
3
+ belongs_to :song
4
+
5
+ validates_presence_of :song
6
+ end
7
+ end
@@ -0,0 +1,58 @@
1
+ module Play
2
+ class Library
3
+ # Search a directory and return all of the files in it, recursively.
4
+ #
5
+ # Returns an Array of String file paths.
6
+ def self.fs_songs
7
+ `find "#{Play.path}" -type f ! -name '.*'`.split("\n")
8
+ end
9
+
10
+ # Imports an array of songs into the database.
11
+ #
12
+ # Returns nothing.
13
+ def self.import_songs
14
+ fs_songs.each do |path|
15
+ import_song(path)
16
+ end
17
+ end
18
+
19
+ # Imports a song into the database. This will identify a file's artist and
20
+ # albums, run through the associations, and so on. It should be idempotent,
21
+ # so you should be able to run it repeatedly on the same set of files and
22
+ # not screw anything up.
23
+ #
24
+ # path - the String path to the music file on-disk
25
+ #
26
+ # Returns the imported (or found) Song.
27
+ def self.import_song(path)
28
+ artist_name,title,album_name = fs_get_artist_and_title_and_album(path)
29
+ artist = Artist.find_or_create_by_name(artist_name)
30
+ song = Song.where(:path => path).first
31
+
32
+ if !song
33
+ album = Album.where(:artist_id => artist.id, :name => album_name).first ||
34
+ Album.create(:artist_id => artist.id, :name => album_name)
35
+ Song.create(:path => path,
36
+ :artist => artist,
37
+ :album => album,
38
+ :title => title)
39
+ end
40
+ end
41
+
42
+ # Splits a music file up into three constituent parts: artist, title,
43
+ # album.
44
+ #
45
+ # path - the String path to the music file on-disk
46
+ #
47
+ # Returns an Array with three String elements: the artist, the song title,
48
+ # and the album.
49
+ def self.fs_get_artist_and_title_and_album(path)
50
+ AudioInfo.open(path) do |info|
51
+ return info.artist.try(:strip),
52
+ info.title.try(:strip),
53
+ info.album.try(:strip)
54
+ end
55
+ rescue AudioInfoError
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,34 @@
1
+ require 'open-uri'
2
+
3
+ module Play
4
+ class Office
5
+ # The users currently present in the office.
6
+ #
7
+ # Returns an Array of User objects.
8
+ def self.users
9
+ string_cache = user_string
10
+ return unless string_cache
11
+ users = []
12
+ string_cache.split(',').each do |string|
13
+ users << User.find_by_office_string(string.downcase)
14
+ end
15
+ users.compact
16
+ end
17
+
18
+ # Hits the URL that we'll use to identify users.
19
+ #
20
+ # Returns a String of users (hopefully in comma-separated format).
21
+ def self.user_string
22
+ open(url).read
23
+ rescue Exception
24
+ nil
25
+ end
26
+
27
+ # The URL we can check to come up with the list of users in the office.
28
+ #
29
+ # Returns the String configuration value for `office_url`.
30
+ def self.url
31
+ Play.config['office_url']
32
+ end
33
+ end
34
+ end
data/lib/play/song.rb ADDED
@@ -0,0 +1,97 @@
1
+ module Play
2
+ class Song < ActiveRecord::Base
3
+ belongs_to :artist
4
+ belongs_to :album
5
+ has_many :votes
6
+ has_many :histories
7
+
8
+ scope :queue, select("songs.*,(select count(song_id) from votes where song_id=songs.id and active=1) as song_count").
9
+ where(:queued => true).
10
+ order("song_count desc, updated_at")
11
+
12
+ # The name of the artist. Used for Mustache purposes.
13
+ #
14
+ # Returns the String name of the artist.
15
+ def artist_name
16
+ artist.name
17
+ end
18
+
19
+ # The name of the album. Used for Mustache purposes.
20
+ #
21
+ # Returns the String name of the album.
22
+ def album_name
23
+ album.name
24
+ end
25
+
26
+ # The current votes for a song. A song may have many historical votes,
27
+ # which is well and good, but here we're only concerned for the current
28
+ # round of whether it's voted for.
29
+ #
30
+ # Returns and Array of Vote objects.
31
+ def current_votes
32
+ votes.where(:active => true).all
33
+ end
34
+
35
+ # Queue up a song.
36
+ #
37
+ # user - the User who is requesting the song to be queued
38
+ #
39
+ # Returns the result of the user's vote for that song.
40
+ def enqueue!(user)
41
+ self.queued = true
42
+ save
43
+ user.vote_for(self)
44
+ end
45
+
46
+ # Remove a song from the queue
47
+ #
48
+ # user - the User who is requesting the song be removed
49
+ #
50
+ # Returns true if removed properly, false otherwise.
51
+ def dequeue!(user=nil)
52
+ self.queued = false
53
+ save
54
+ end
55
+
56
+ # Update the metadata surrounding playing a song.
57
+ #
58
+ # Returns a Boolean of whether we've saved the song.
59
+ def play!
60
+ Song.update_all(:now_playing => false)
61
+ self.now_playing = true
62
+ votes.update_all(:active => false)
63
+ save
64
+ end
65
+
66
+ # Pull a magic song from a hat, depending on who's in the office.
67
+ #
68
+ # Returns a Song that's pulled from the favorite artists of the users
69
+ # currently located in the office.
70
+ def self.office_song
71
+ users = Play::Office.users
72
+ if !users.empty?
73
+ artist = users.collect(&:favorite_artists).flatten.shuffle.first
74
+ end
75
+
76
+ if artist
77
+ artist.songs.shuffle.first
78
+ else
79
+ Play::Song.order("rand()").first
80
+ end
81
+ end
82
+
83
+ # Plays the next song in the queue. Updates the appropriate metainformation
84
+ # in surrounding tables. Will pull an office favorite if there's nothing in
85
+ # the queue currently.
86
+ #
87
+ # Returns the Song that was selected next to be played.
88
+ def self.play_next_in_queue
89
+ song = queue.first
90
+ song ||= office_song
91
+ Play::History.create(:song => song)
92
+ song.play!
93
+ song.dequeue!
94
+ song
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,5 @@
1
+ <div class="songs">
2
+ {{#songs}}
3
+ {{>song}}
4
+ {{/songs}}
5
+ </div>
@@ -0,0 +1,5 @@
1
+ <div class="songs">
2
+ {{#songs}}
3
+ {{>song}}
4
+ {{/songs}}
5
+ </div>
@@ -0,0 +1,18 @@
1
+ {{#songs}}
2
+ {{>song}}
3
+ <div class="votes">
4
+ {{#current_votes}}
5
+ {{#user}}
6
+ <a href="/{{login}}">
7
+ <img src="http://www.gravatar.com/avatar.php?gravatar_id={{gravatar_id}}&size=50" class="gravatar" height="25" width="25" />
8
+ </a>
9
+ {{/user}}
10
+ {{/current_votes}}
11
+ </div>
12
+ {{/songs}}
13
+
14
+ {{^songs}}
15
+ <div class="content">
16
+ The queue is empty. Quick, play some <a href="/artist/Ace+of+Base">Ace of Base</a>; no one's looking.
17
+ </div>
18
+ {{/songs}}
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Play: {{title}}</title>
5
+ <link rel="stylesheet" href="/css/base.css" type="text/css" />
6
+ </head>
7
+
8
+ <body>
9
+ <h1>{{title}}</h1>
10
+
11
+ <div class="navigation">
12
+ <form method="get" action="/search">
13
+ <input type="search" name="q" />
14
+ </form>
15
+
16
+ <a href="/">Queue</a>
17
+ <a href="/history">History</a>
18
+ <a href="/{{login}}">@{{login}}</a>
19
+ </div>
20
+
21
+ {{{yield}}}
22
+ </body>
23
+ </html>
@@ -0,0 +1,5 @@
1
+ <h3 style="padding: 0 10px;">
2
+ {{artist_name}}: {{song_title}}
3
+ </h3>
4
+
5
+ <p style="padding: 0 10px;">Yeah, this page should be fixed.</p>
@@ -0,0 +1,3 @@
1
+ {{#songs}}
2
+ {{>song}}
3
+ {{/songs}}
@@ -0,0 +1,24 @@
1
+ <div class="content" style="height: 75px">
2
+ {{#user}}
3
+ <div class="profile_box">
4
+ <img src="http://www.gravatar.com/avatar.php?gravatar_id={{gravatar_id}}&size=140" class="gravatar" height="75" width="75" />
5
+ </div>
6
+
7
+ <div class="profile_box bio">
8
+ <strong>@{{login}}</strong><br />
9
+ Queued up {{votes_count}} songs.<br />
10
+ </div>
11
+
12
+ {{/user}}
13
+ </div>
14
+
15
+ <div class="favorite_artists">
16
+ <h2>Favorite Artists</h2>
17
+ <ul>
18
+ {{#user}}
19
+ {{#favorite_artists}}
20
+ <li><a href="/artist/{{name}}">{{name}}</a></li>
21
+ {{/favorite_artists}}
22
+ {{/user}}
23
+ </ul>
24
+ </div>
@@ -0,0 +1,3 @@
1
+ {{#songs}}
2
+ {{>song}}
3
+ {{/songs}}
@@ -0,0 +1,9 @@
1
+ <div class="content">
2
+ {{#users}}
3
+ <a href="/{{login}}">
4
+ <img src="http://www.gravatar.com/avatar.php?gravatar_id={{gravatar_id}}&size=100" class="gravatar" height="50" width="50" />
5
+ </a>
6
+ {{/users}}
7
+
8
+ Played {{plays}} times.
9
+ </div>
@@ -0,0 +1,21 @@
1
+ <div class="song">
2
+ <span class="controls">
3
+ {{#queued?}}
4
+ <a href="/remove/{{id}}" title="remove from queue">-</a>
5
+ {{/queued?}}
6
+ {{^queued?}}
7
+ <a href="/add/{{id}}" title="add to queue">+</a>
8
+ {{/queued?}}
9
+ </span>
10
+
11
+ <span class="artist">
12
+ <a href="/artist/{{artist_name}}">{{artist_name}}</a>
13
+ </span>
14
+
15
+ <span class="title">
16
+ <a href="/song/{{id}}">{{title}}</a>
17
+ </span>
18
+ <span class="album">
19
+ <a href="/artist/{{artist_name}}/album/{{album_name}}">{{album_name}}</a>
20
+ </span>
21
+ </div>
data/lib/play/user.rb ADDED
@@ -0,0 +1,59 @@
1
+ module Play
2
+ class User < ActiveRecord::Base
3
+ has_many :votes
4
+
5
+ # Let the user vote for a particular song.
6
+ #
7
+ # song - the Song that the user wants to vote up
8
+ #
9
+ # Returns the Vote object.
10
+ def vote_for(song)
11
+ votes.create(:song => song, :artist => song.artist)
12
+ end
13
+
14
+ # The count of the votes for this user. Used for Mustache purposes.
15
+ #
16
+ # Returns the Integer number of votes.
17
+ def votes_count
18
+ votes.count
19
+ end
20
+
21
+ # Queries the database for a user's favorite artists. It's culled just from
22
+ # the historical votes of that user.
23
+ #
24
+ # Returns an Array of five Artist objects.
25
+ def favorite_artists
26
+ Artist.includes(:votes).
27
+ where("votes.user_id = ?",id).
28
+ group("votes.artist_id").
29
+ order("count(votes.artist_id) desc").
30
+ limit(5).
31
+ all
32
+ end
33
+
34
+ # The MD5 hash of the user's email account. Used for showing their
35
+ # Gravatar.
36
+ #
37
+ # Returns the String MD5 hash.
38
+ def gravatar_id
39
+ Digest::MD5.hexdigest(email)
40
+ end
41
+
42
+ # Authenticates a user. This will either select the existing user account,
43
+ # or if it doesn't exist yet, create it on the system.
44
+ #
45
+ # auth - the Hash representation returned by OmniAuth after
46
+ # authenticating
47
+ #
48
+ # Returns the User account.
49
+ def self.authenticate(auth)
50
+ if user = User.where(:login => auth['nickname']).first
51
+ user
52
+ else
53
+ user = User.create(:login => auth['nickname'],
54
+ :name => auth['name'],
55
+ :email => auth['email'])
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,9 @@
1
+ module Play
2
+ module Views
3
+ class AlbumSongs < Layout
4
+ def title
5
+ "#{@artist.name}: #{@album.name}"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Play
2
+ module Views
3
+ class ArtistSongs < Layout
4
+ def title
5
+ @artist.name
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Play
2
+ module Views
3
+ class Index < Layout
4
+ def title
5
+ "Queue"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ module Play
2
+ module Views
3
+ class Layout < Mustache
4
+ end
5
+ end
6
+ end