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
@@ -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