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