mumbletune 0.1.0
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/.gitignore +20 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +66 -0
- data/LICENSE.txt +22 -0
- data/README.md +31 -0
- data/Rakefile +1 -0
- data/bin/mumbletune +6 -0
- data/docs/commands.md +13 -0
- data/lib/mumbletune/collection.rb +17 -0
- data/lib/mumbletune/handle_sp_error.rb +23 -0
- data/lib/mumbletune/messages.rb +163 -0
- data/lib/mumbletune/mpd_client.rb +180 -0
- data/lib/mumbletune/mumble_client.rb +55 -0
- data/lib/mumbletune/resolver.rb +148 -0
- data/lib/mumbletune/sp_uri_server.rb +42 -0
- data/lib/mumbletune/spotify_track.rb +90 -0
- data/lib/mumbletune/templates/commands.mustache +100 -0
- data/lib/mumbletune/templates/queue.mustache +41 -0
- data/lib/mumbletune/track.rb +32 -0
- data/lib/mumbletune/version.rb +3 -0
- data/lib/mumbletune.rb +79 -0
- data/mumbletune.gemspec +36 -0
- metadata +246 -0
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'meta-spotify'
|
3
|
+
require 'text'
|
4
|
+
|
5
|
+
|
6
|
+
module Mumbletune
|
7
|
+
def self.resolve(argument)
|
8
|
+
Resolvers.workers.each do |r|
|
9
|
+
if r.matches?(argument)
|
10
|
+
return r.resolve(argument)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
return false
|
14
|
+
end
|
15
|
+
|
16
|
+
module Resolvers
|
17
|
+
class << self
|
18
|
+
attr_accessor :workers
|
19
|
+
end
|
20
|
+
|
21
|
+
@workers = []
|
22
|
+
|
23
|
+
class Resolver
|
24
|
+
def matches?(arg); end
|
25
|
+
def resolve(arg); end
|
26
|
+
def self.inherited(subcl)
|
27
|
+
Resolvers.workers.push(subcl.new)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class SpotifyURIResolver < Resolver
|
32
|
+
def matches?(uri)
|
33
|
+
http_uris = URI.extract(uri, ['http', 'https'])
|
34
|
+
sp_uris = URI.extract(uri, 'spotify')
|
35
|
+
if http_uris.any?
|
36
|
+
parsed_uri = URI.parse(http_uris.join)
|
37
|
+
if parsed_uri.hostname =~ /(?:open|play)\.spotify\.com/i
|
38
|
+
true
|
39
|
+
else
|
40
|
+
false
|
41
|
+
end
|
42
|
+
elsif sp_uris.any?
|
43
|
+
true
|
44
|
+
else
|
45
|
+
false
|
46
|
+
end
|
47
|
+
end
|
48
|
+
def resolve(uri)
|
49
|
+
raise "Not a Spotify URI." unless matches?(uri)
|
50
|
+
regexp = /(?<type>track|artist|album)[\/|:](?<id>\w+)/i
|
51
|
+
matched = regexp.match(uri)
|
52
|
+
type = matched[:type]
|
53
|
+
id = matched[:id]
|
54
|
+
sp_uri = "spotify:#{type}:#{id}"
|
55
|
+
|
56
|
+
# behave according to URI type
|
57
|
+
case type
|
58
|
+
when "track" # Return this track
|
59
|
+
SpotifyTrack::track_from_uri(sp_uri)
|
60
|
+
when "album" # Return all tracks of the album to queue
|
61
|
+
SpotifyTrack::tracks_from_album(sp_uri)
|
62
|
+
when "artist" # Return 10 tracks for this artist
|
63
|
+
SpotifyTrack::tracks_from_artist(sp_uri)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class SpotifySearchResolver < Resolver
|
69
|
+
def matches?(query)
|
70
|
+
# basically we will search for anything that's not a URL
|
71
|
+
if URI.extract(query).any?
|
72
|
+
return false
|
73
|
+
else
|
74
|
+
return true
|
75
|
+
end
|
76
|
+
end
|
77
|
+
def resolve(query)
|
78
|
+
first_word = query.split.first
|
79
|
+
|
80
|
+
# if first word is a type to search for, it needs to be stripped
|
81
|
+
# from the query so we don't search for it (e.g. "track starships")
|
82
|
+
if first_word =~ /^(artist|album|track)$/i
|
83
|
+
query_a = query.split
|
84
|
+
query_a.delete_at 0
|
85
|
+
query = query_a.join(" ")
|
86
|
+
end
|
87
|
+
|
88
|
+
# used to check if tracks are playable in in region
|
89
|
+
region = Mumbletune.config["spotify"]["region"]
|
90
|
+
|
91
|
+
# determine result based on a type in the first word
|
92
|
+
if first_word =~ /^artist$/i
|
93
|
+
artist = Mumbletune.handle_sp_error { MetaSpotify::Artist.search(query) }
|
94
|
+
result = artist[:artists].first
|
95
|
+
|
96
|
+
elsif first_word =~ /^album$/i
|
97
|
+
album = Mumbletune.handle_sp_error { MetaSpotify::Album.search(query) }
|
98
|
+
album[:albums].select! { |a| a.available_territories.include? region }
|
99
|
+
result = album[:albums].first
|
100
|
+
|
101
|
+
elsif first_word =~ /^track$/i
|
102
|
+
track = Mumbletune.handle_sp_error { MetaSpotify::Track.search(query) }
|
103
|
+
track[:tracks].select! { |t| t.album.available_territories.include? region }
|
104
|
+
result = track[:tracks].first
|
105
|
+
|
106
|
+
else # determine intended result by similarity to the query
|
107
|
+
artist = Mumbletune.handle_sp_error { MetaSpotify::Artist.search(query) }
|
108
|
+
album = Mumbletune.handle_sp_error { MetaSpotify::Album.search(query) }
|
109
|
+
track = Mumbletune.handle_sp_error { MetaSpotify::Track.search(query) }
|
110
|
+
|
111
|
+
# searches now finished
|
112
|
+
|
113
|
+
# remove anything out-of-region
|
114
|
+
album[:albums].select! { |a| a.available_territories.include? region } if album[:albums].any?
|
115
|
+
track[:tracks].select! { |t| t.album.available_territories.include? region } if track[:tracks].any?
|
116
|
+
|
117
|
+
compare = []
|
118
|
+
compare.push track[:tracks].first if track[:tracks].any?
|
119
|
+
compare.push album[:albums].first if album[:albums].any?
|
120
|
+
compare.push artist[:artists].first if artist[:artists].any?
|
121
|
+
|
122
|
+
white = Text::WhiteSimilarity.new
|
123
|
+
compare.sort! do |a, b|
|
124
|
+
a_sim = white.similarity(query, a.name)
|
125
|
+
b_sim = white.similarity(query, b.name)
|
126
|
+
if a_sim > b_sim
|
127
|
+
-1
|
128
|
+
elsif b_sim > a_sim
|
129
|
+
1
|
130
|
+
else
|
131
|
+
0
|
132
|
+
end
|
133
|
+
end
|
134
|
+
result = compare.first
|
135
|
+
end
|
136
|
+
|
137
|
+
if result.class == MetaSpotify::Artist
|
138
|
+
SpotifyTrack.tracks_from_artist(result)
|
139
|
+
elsif result.class == MetaSpotify::Album
|
140
|
+
SpotifyTrack.tracks_from_album(result)
|
141
|
+
elsif result.class == MetaSpotify::Track
|
142
|
+
SpotifyTrack.track_from_uri(result)
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
|
3
|
+
module Mumbletune
|
4
|
+
|
5
|
+
module SPURIServer
|
6
|
+
|
7
|
+
class Server < Sinatra::Base
|
8
|
+
set :run, true
|
9
|
+
set :server, :thin
|
10
|
+
set :logging, false
|
11
|
+
set :app_file, __FILE__
|
12
|
+
set :bind, 'localhost'
|
13
|
+
set :port, 8081
|
14
|
+
|
15
|
+
get '/play/:uri' do
|
16
|
+
cred = Mumbletune.config['spotify']
|
17
|
+
sp = Mumbletune::Spotify.new(cred['username'], cred['password'])
|
18
|
+
|
19
|
+
track = sp.objectFromURI(params[:uri])
|
20
|
+
halt 404, "Could not find a track with that URI." if track == nil
|
21
|
+
|
22
|
+
url = track.getFileURL().to_s
|
23
|
+
halt 404, "Could not find a track URL for that URI." if track == nil
|
24
|
+
|
25
|
+
sp.logout
|
26
|
+
redirect url, 303
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.url_for(uri)
|
31
|
+
bind = Server::bind
|
32
|
+
port = Server::port
|
33
|
+
"http://#{bind}:#{port}/play/#{uri}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.sp_uri_for(url)
|
37
|
+
regexp = /(.+)(<sp_uri>spotify:\w+:\w+)/i
|
38
|
+
matched = regexp.match(url)
|
39
|
+
matched[:sp_uri]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Mumbletune
|
4
|
+
|
5
|
+
class SpotifyTrack < Track
|
6
|
+
def initialize(params)
|
7
|
+
super
|
8
|
+
|
9
|
+
@uri = params[:uri]
|
10
|
+
@url = SPURIServer.url_for(@uri)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.track_from_uri(track)
|
14
|
+
track = MetaSpotify::Track.lookup(track) unless track.class == MetaSpotify::Track
|
15
|
+
|
16
|
+
# force track to be playable within region
|
17
|
+
unless track.album.available_territories.include? Mumbletune.config["spotify"]["region"]
|
18
|
+
raise "#{track.name}: Not available in this region."
|
19
|
+
end
|
20
|
+
|
21
|
+
song = SpotifyTrack.new({
|
22
|
+
:name => track.name,
|
23
|
+
:artist => track.artists.first.name,
|
24
|
+
:album => track.album.name,
|
25
|
+
:uri => track.uri
|
26
|
+
})
|
27
|
+
|
28
|
+
# Technically, a collection of one.
|
29
|
+
Collection.new(
|
30
|
+
:TRACK,
|
31
|
+
song,
|
32
|
+
"<b>#{song.name}</b> by <b>#{song.artist}</b>"
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.tracks_from_album(album_ref)
|
37
|
+
album_uri = album_ref.uri if album_ref.class == MetaSpotify::Album
|
38
|
+
album = MetaSpotify::Album.lookup(album_uri, {:extras => "track"})
|
39
|
+
|
40
|
+
# force album to be playable in region
|
41
|
+
unless album.available_territories.include? Mumbletune.config["spotify"]["region"]
|
42
|
+
raise "#{album.name}: Not available in this region."
|
43
|
+
end
|
44
|
+
|
45
|
+
tracks = []
|
46
|
+
album.tracks.each do |track|
|
47
|
+
tracks.push SpotifyTrack.new({
|
48
|
+
:name => track.name,
|
49
|
+
:artist => track.artists.first.name,
|
50
|
+
:album => album.name,
|
51
|
+
:uri => track.uri
|
52
|
+
})
|
53
|
+
end
|
54
|
+
|
55
|
+
Collection.new(
|
56
|
+
:ALBUM,
|
57
|
+
tracks,
|
58
|
+
"the album <b>#{album.name}</b> by <b>#{album.artists.first.name}</b>"
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.tracks_from_artist(artist)
|
63
|
+
artist = MetaSpotify::Artist.lookup(artist) unless artist.class == MetaSpotify::Artist
|
64
|
+
|
65
|
+
# spotify metadata api still error-prone
|
66
|
+
search_result = Mumbletune.handle_sp_error { MetaSpotify::Track.search("artist:\"#{artist.name}\"") }
|
67
|
+
|
68
|
+
# filter out tracks outside region
|
69
|
+
search_result[:tracks].select! do |track|
|
70
|
+
track.album.available_territories.include? Mumbletune.config["spotify"]["region"]
|
71
|
+
end
|
72
|
+
|
73
|
+
tracks = []
|
74
|
+
search_result[:tracks][0...10].each do |track|
|
75
|
+
tracks.push SpotifyTrack.new({
|
76
|
+
:name => track.name,
|
77
|
+
:artist => track.artists.first.name,
|
78
|
+
:album => track.album.name,
|
79
|
+
:uri => track.uri
|
80
|
+
})
|
81
|
+
end
|
82
|
+
|
83
|
+
Collection.new(
|
84
|
+
:ARTIST_TOP,
|
85
|
+
tracks,
|
86
|
+
"#{tracks.length} tracks by <b>#{artist.name}</b>"
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
{{#unknown}}
|
2
|
+
<p>
|
3
|
+
I've got literally no idea what "{{command}}" means. Try one of these commands:
|
4
|
+
</p>
|
5
|
+
{{/unknown}}
|
6
|
+
|
7
|
+
<table>
|
8
|
+
<thead>
|
9
|
+
<tr class="header">
|
10
|
+
<th align="left">
|
11
|
+
Command
|
12
|
+
</th>
|
13
|
+
<th align="left">
|
14
|
+
Action
|
15
|
+
</th>
|
16
|
+
</tr>
|
17
|
+
</thead>
|
18
|
+
<tbody>
|
19
|
+
<tr class="odd">
|
20
|
+
<td align="left">
|
21
|
+
play <track>
|
22
|
+
</td>
|
23
|
+
<td align="left">
|
24
|
+
Queue this song.
|
25
|
+
</td>
|
26
|
+
</tr>
|
27
|
+
<tr class="even">
|
28
|
+
<td align="left">
|
29
|
+
play <artist>
|
30
|
+
</td>
|
31
|
+
<td align="left">
|
32
|
+
Queue 10 tracks by this artist.
|
33
|
+
</td>
|
34
|
+
</tr>
|
35
|
+
<tr class="odd">
|
36
|
+
<td align="left">
|
37
|
+
play <album>
|
38
|
+
</td>
|
39
|
+
<td align="left">
|
40
|
+
Queue this entire album.
|
41
|
+
</td>
|
42
|
+
</tr>
|
43
|
+
<tr class="even">
|
44
|
+
<td align="left">
|
45
|
+
play <something> now
|
46
|
+
</td>
|
47
|
+
<td align="left">
|
48
|
+
Play this shit right now!
|
49
|
+
</td>
|
50
|
+
</tr>
|
51
|
+
<tr class="odd">
|
52
|
+
<td align="left">
|
53
|
+
what
|
54
|
+
</td>
|
55
|
+
<td align="left">
|
56
|
+
Gets what is playing, and what is queued.
|
57
|
+
</td>
|
58
|
+
</tr>
|
59
|
+
<tr class="even">
|
60
|
+
<td align="left">
|
61
|
+
next
|
62
|
+
</td>
|
63
|
+
<td align="left">
|
64
|
+
Jump to the next song in the queue.
|
65
|
+
</td>
|
66
|
+
</tr>
|
67
|
+
<tr class="odd">
|
68
|
+
<td align="left">
|
69
|
+
clear
|
70
|
+
</td>
|
71
|
+
<td align="left">
|
72
|
+
Clears the current queue.
|
73
|
+
</td>
|
74
|
+
</tr>
|
75
|
+
<tr class="even">
|
76
|
+
<td align="left">
|
77
|
+
volume?
|
78
|
+
</td>
|
79
|
+
<td align="left">
|
80
|
+
Get the current volume level.
|
81
|
+
</td>
|
82
|
+
</tr>
|
83
|
+
<tr class="odd">
|
84
|
+
<td align="left">
|
85
|
+
volume [0-100]
|
86
|
+
</td>
|
87
|
+
<td align="left">
|
88
|
+
Set the volume.
|
89
|
+
</td>
|
90
|
+
</tr>
|
91
|
+
<tr class="even">
|
92
|
+
<td align="left">
|
93
|
+
wisdom
|
94
|
+
</td>
|
95
|
+
<td align="left">
|
96
|
+
Obtain it.
|
97
|
+
</td>
|
98
|
+
</tr>
|
99
|
+
</tbody>
|
100
|
+
</table>
|
@@ -0,0 +1,41 @@
|
|
1
|
+
{{#anything?}}
|
2
|
+
<table>
|
3
|
+
<col width="29%">
|
4
|
+
<col width="13%">
|
5
|
+
<col width="19%">
|
6
|
+
<thead>
|
7
|
+
<tr class="header">
|
8
|
+
<th align="left"></th>
|
9
|
+
<th align="left">
|
10
|
+
Track
|
11
|
+
</th>
|
12
|
+
<th align="left">
|
13
|
+
Artist
|
14
|
+
</th>
|
15
|
+
</tr>
|
16
|
+
</thead>
|
17
|
+
<tbody>
|
18
|
+
{{/anything?}}
|
19
|
+
|
20
|
+
{{#queue}}
|
21
|
+
<tr class="odd">
|
22
|
+
<td align="left">
|
23
|
+
{{#playing?}} ► {{/playing?}}
|
24
|
+
</td>
|
25
|
+
<td align="left">
|
26
|
+
{{name}}
|
27
|
+
</td>
|
28
|
+
<td align="left">
|
29
|
+
{{artist}}
|
30
|
+
</td>
|
31
|
+
</tr>
|
32
|
+
{{/queue}}
|
33
|
+
|
34
|
+
{{#anything?}}
|
35
|
+
</tbody>
|
36
|
+
</table>
|
37
|
+
{{/anything?}}
|
38
|
+
|
39
|
+
{{^anything?}}
|
40
|
+
Nothing is playing. :c
|
41
|
+
{{/anything?}}
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Mumbletune
|
2
|
+
class Track
|
3
|
+
attr_accessor :name, :artist, :album, :url, :mpd_id, :queue_pos
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_accessor :store
|
7
|
+
end
|
8
|
+
self.store = []
|
9
|
+
|
10
|
+
def initialize(params)
|
11
|
+
@name = params[:name]
|
12
|
+
@artist = params[:artist]
|
13
|
+
@album = params[:album]
|
14
|
+
@url = params[:url]
|
15
|
+
|
16
|
+
Track.store.push self
|
17
|
+
end
|
18
|
+
|
19
|
+
def playing?
|
20
|
+
if self == Mumbletune.player.current_song
|
21
|
+
true
|
22
|
+
else
|
23
|
+
false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.retreive_from_mpd_id(id)
|
28
|
+
Track.store.select { |t| t.mpd_id == id }.first
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
data/lib/mumbletune.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'mumbletune/mumble_client'
|
2
|
+
require 'mumbletune/mpd_client'
|
3
|
+
require 'mumbletune/messages'
|
4
|
+
require 'mumbletune/track'
|
5
|
+
require 'mumbletune/collection'
|
6
|
+
require 'mumbletune/sp_uri_server'
|
7
|
+
require 'mumbletune/spotify_track'
|
8
|
+
require 'mumbletune/resolver'
|
9
|
+
require 'mumbletune/handle_sp_error'
|
10
|
+
|
11
|
+
require 'optparse'
|
12
|
+
require 'yaml'
|
13
|
+
require 'eventmachine'
|
14
|
+
require 'rubypython'
|
15
|
+
|
16
|
+
module Mumbletune
|
17
|
+
class << self
|
18
|
+
attr_reader :player, :mumble, :uri_server, :config
|
19
|
+
end
|
20
|
+
|
21
|
+
# parse command line options
|
22
|
+
config_file = nil
|
23
|
+
OptionParser.new do |opts|
|
24
|
+
opts.banner = "Usage: mumbletune.rb [options]"
|
25
|
+
opts.on("-c", "--config FILE", "=MANDATORY", "Path to configuration file") do |file|
|
26
|
+
config_file = file
|
27
|
+
end
|
28
|
+
opts.on("-h", "--help", "This help message") do
|
29
|
+
puts opts.help()
|
30
|
+
exit
|
31
|
+
end
|
32
|
+
end.parse!
|
33
|
+
raise OptionParser::MissingArgument unless config_file
|
34
|
+
|
35
|
+
# load configuration file
|
36
|
+
@config = YAML.load_file(config_file)
|
37
|
+
|
38
|
+
# load spotify-websocket-api
|
39
|
+
puts ">> Loading Spotify APIs..."
|
40
|
+
RubyPython.start(:python_exe => 'python2.7')
|
41
|
+
Spotify = RubyPython.import('spotify_web.friendly').Spotify
|
42
|
+
|
43
|
+
# open URI server
|
44
|
+
uri_thread = Thread.new do
|
45
|
+
SPURIServer::Server.run!
|
46
|
+
end
|
47
|
+
|
48
|
+
# initialize player
|
49
|
+
play_thread = Thread.new do
|
50
|
+
@player = Player.new
|
51
|
+
end
|
52
|
+
|
53
|
+
# connect to mumble & start streaming
|
54
|
+
mumble_thread = Thread.new do
|
55
|
+
@mumble = MumbleClient.new
|
56
|
+
@mumble.connect
|
57
|
+
@mumble.stream
|
58
|
+
end
|
59
|
+
|
60
|
+
# shutdown code
|
61
|
+
def self.shutdown
|
62
|
+
puts "\nGoodbye forever. Exiting..."
|
63
|
+
exit
|
64
|
+
end
|
65
|
+
|
66
|
+
# exit when Ctrl-C pressed
|
67
|
+
EventMachine.schedule do
|
68
|
+
trap("INT") { Mumbletune.shutdown }
|
69
|
+
end
|
70
|
+
|
71
|
+
# testing
|
72
|
+
# sleep 3
|
73
|
+
# @player.command_test_load
|
74
|
+
# @player.command_play
|
75
|
+
|
76
|
+
Thread.stop # wake up to shut down
|
77
|
+
self.shutdown
|
78
|
+
|
79
|
+
end
|
data/mumbletune.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'mumbletune/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "mumbletune"
|
8
|
+
spec.version = Mumbletune::VERSION
|
9
|
+
spec.authors = ["Elliott Williams"]
|
10
|
+
spec.email = ["e@elliottwillia.ms"]
|
11
|
+
spec.description = "Mumbletune connects to a mumble server and allows users to"\
|
12
|
+
" interact with and play a queue of music. Currently plays"\
|
13
|
+
" from Spotify alone."
|
14
|
+
spec.summary = "A mumble server bot that plays music"
|
15
|
+
spec.homepage = "http://github.com/elliottwilliams/mumbletune"
|
16
|
+
spec.license = "MIT"
|
17
|
+
|
18
|
+
spec.files = `git ls-files`.split($/)
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
|
26
|
+
spec.add_runtime_dependency "mumble-ruby"
|
27
|
+
spec.add_runtime_dependency "ruby-mpd"
|
28
|
+
spec.add_runtime_dependency "sinatra"
|
29
|
+
spec.add_runtime_dependency "meta-spotify"
|
30
|
+
spec.add_runtime_dependency "rubypython"
|
31
|
+
|
32
|
+
spec.add_runtime_dependency "thin"
|
33
|
+
spec.add_runtime_dependency "eventmachine"
|
34
|
+
spec.add_runtime_dependency "text"
|
35
|
+
spec.add_runtime_dependency "mustache"
|
36
|
+
end
|