play 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|