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/lib/play/history.rb
ADDED
data/lib/play/library.rb
ADDED
|
@@ -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
|
data/lib/play/office.rb
ADDED
|
@@ -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,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,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,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
|