mumbletune 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ InstalledFiles
7
+ _yardoc
8
+ coverage
9
+ doc/
10
+ lib/bundler/man
11
+ pkg
12
+ rdoc
13
+ spec/reports
14
+ test/tmp
15
+ test/version_tmp
16
+ tmp
17
+
18
+ .DS_Store
19
+ conf.yml
20
+ conf.yaml
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mumbletune.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,66 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ mumbletune (0.1.0)
5
+ eventmachine
6
+ meta-spotify
7
+ mumble-ruby
8
+ mustache
9
+ ruby-mpd
10
+ rubypython
11
+ sinatra
12
+ text
13
+ thin
14
+
15
+ GEM
16
+ remote: https://rubygems.org/
17
+ specs:
18
+ activesupport (3.2.12)
19
+ i18n (~> 0.6)
20
+ multi_json (~> 1.0)
21
+ blankslate (3.1.2)
22
+ celt-ruby (0.0.1)
23
+ ffi
24
+ daemons (1.1.9)
25
+ eventmachine (1.0.3)
26
+ ffi (1.4.0)
27
+ httparty (0.10.2)
28
+ multi_json (~> 1.0)
29
+ multi_xml (>= 0.5.2)
30
+ i18n (0.6.4)
31
+ meta-spotify (0.3.0)
32
+ httparty (> 0.8)
33
+ multi_json (1.6.1)
34
+ multi_xml (0.5.3)
35
+ mumble-ruby (0.0.3)
36
+ activesupport
37
+ celt-ruby
38
+ ruby_protobuf
39
+ mustache (0.99.4)
40
+ rack (1.5.2)
41
+ rack-protection (1.5.0)
42
+ rack
43
+ rake (10.0.3)
44
+ ruby-mpd (0.2.1)
45
+ ruby_protobuf (0.4.11)
46
+ rubypython (0.6.3)
47
+ blankslate (>= 2.1.2.3)
48
+ ffi (>= 1.0.7)
49
+ sinatra (1.4.1)
50
+ rack (~> 1.5, >= 1.5.2)
51
+ rack-protection (~> 1.4)
52
+ tilt (~> 1.3, >= 1.3.4)
53
+ text (1.2.1)
54
+ thin (1.5.0)
55
+ daemons (>= 1.0.9)
56
+ eventmachine (>= 0.12.6)
57
+ rack (>= 1.0.0)
58
+ tilt (1.3.5)
59
+
60
+ PLATFORMS
61
+ ruby
62
+
63
+ DEPENDENCIES
64
+ bundler (~> 1.3)
65
+ mumbletune!
66
+ rake
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Elliott Williams
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # Mumbletune
2
+
3
+ MUMBLETUNE is an amiable bot that connects to a mumble server and allows users to interact with and play a queue of music. It currently plays music through Spotify.
4
+
5
+ ## Installation
6
+
7
+ First, install Hexxeh's (spotify-websocket-api)[https://github.com/Hexxeh/spotify-websocket-api].
8
+
9
+ git clone git://github.com/Hexxeh/spotify-websocket-api.git
10
+ cd spotify-websocket-api
11
+ python setup.py build
12
+ puthon setup.py install
13
+
14
+ Then, add this line to your application's Gemfile:
15
+
16
+ gem 'mumbletune'
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install mumbletune
25
+
26
+ ## Usage
27
+
28
+ 1. Create a configuration file. See `conf.example.yaml` for help.
29
+ 2. Start Mumbletune, passing your config with `-c`.
30
+
31
+ $ mumbletune -c config_file.yaml
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/mumbletune ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
5
+
6
+ require 'mumbletune'
data/docs/commands.md ADDED
@@ -0,0 +1,13 @@
1
+ Command Action
2
+ ------ ------
3
+ play <track> Queue this song.
4
+ play <artist> Queue 10 tracks by this artist.
5
+ play <album> Queue this entire album.
6
+ play <something> now Play this shit right now!
7
+ what Gets what is playing, and what is queued.
8
+ next Jump to the next song in the queue.
9
+ clear Clears the current queue.
10
+ volume? Get the current volume level.
11
+ volume [0-100] Set the volume.
12
+ wisdom Obtain it.
13
+ ------ ------
@@ -0,0 +1,17 @@
1
+ module Mumbletune
2
+ class Collection
3
+ attr_accessor :type, :tracks, :description, :user
4
+
5
+ def initialize(type, tracks, description)
6
+ @type = type
7
+ @description = description
8
+
9
+ if tracks.class == Array
10
+ @tracks = tracks
11
+ else
12
+ @tracks = [tracks]
13
+ end
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ module Mumbletune
2
+
3
+ # The Spotify Metadata API seems to throw 502 (Bad Gateway) errors *a lot*.
4
+ # There's some complaint about this online, and I can reproduce it with
5
+ # plenty of REST clients, so I am sure it's Spotify's problem. But they
6
+ # don't seem too interested in fixing what appears to be a long-standing
7
+ # issue. Ho hum.
8
+ def self.handle_sp_error
9
+ begin
10
+ yield
11
+ rescue MetaSpotify::ServerError => err
12
+ puts "Caught ServerError: #{err}"
13
+ failed ||= 0
14
+ failed += 1
15
+ if failed < 4
16
+ sleep 1
17
+ retry
18
+ else
19
+ raise
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,163 @@
1
+ require 'uri'
2
+ require 'text'
3
+ require 'mustache'
4
+
5
+ Thread.abort_on_exception=true # development only
6
+
7
+ module Mumbletune
8
+
9
+ class Message
10
+
11
+ # class methods
12
+
13
+ class << self
14
+ attr_accessor :template
15
+ end
16
+ self.template = Hash.new
17
+
18
+ def self.parse(client, data)
19
+ message = Message.new(client, data)
20
+
21
+ begin
22
+ case message.text
23
+
24
+ when /^play/i
25
+ if message.argument.length > 0 # user wants to play something
26
+ if message.words.last =~ /now/i
27
+ play_now = true
28
+ message.argument = message.words[1...message.words.length-1].join(" ")
29
+ else
30
+ play_now = false
31
+ end
32
+
33
+ # reassurance that it's working
34
+ message.respond "I'm searching. Hang tight."
35
+
36
+ collection = Mumbletune.resolve(message.argument)
37
+
38
+ # handle unknown argument
39
+ return message.respond "I couldn't find what you wanted me to play. :'(" unless collection
40
+
41
+ # associate the collection with a user
42
+ collection.user = message.sender.name
43
+
44
+ # add these tracks to the queue
45
+ Mumbletune.player.add_collection collection, (play_now) ? true : false
46
+
47
+ if play_now
48
+ message.respond_all "#{message.sender.name} is playing #{collection.description} RIGHT NOW."
49
+ else
50
+ message.respond_all "#{message.sender.name} added #{collection.description} to the queue."
51
+ end
52
+
53
+ Mumbletune.player.play unless Mumbletune.player.playing?
54
+
55
+
56
+ else # user wants to unpause
57
+ if Mumbletune.player.paused?
58
+ Mumbletune.player.play
59
+ message.respond "Unpaused."
60
+ else
61
+ message.respond "Not paused."
62
+ end
63
+ end
64
+
65
+ when /^pause$/i
66
+ paused = Mumbletune.player.pause
67
+ response = (paused) ? "Paused." : "Unpaused."
68
+ message.respond response
69
+
70
+ when /^unpause$/i
71
+ if Mumbletune.player.paused?
72
+ Mumbletune.player.play
73
+ message.respond "Unpaused."
74
+ else
75
+ message.respond "Not paused."
76
+ end
77
+
78
+
79
+ when /^next$/i
80
+ Mumbletune.player.next
81
+ current = Mumbletune.player.current_song
82
+ message.respond "OK, now playing #{current.artist} - #{current.name}"
83
+
84
+ when /^clear$/i
85
+ Mumbletune.player.clear_queue
86
+ message.respond "#{message.sender.name} cleared the queue."
87
+
88
+ when /^undo$/i
89
+ removed = Mumbletune.player.undo
90
+ message.respond_all "#{message.sender.name} removed #{removed.description} that #{removed.user} added."
91
+
92
+
93
+ when /^(what|queue)$/i
94
+ queue = Mumbletune.player.queue
95
+
96
+ # Now, a template.
97
+ rendered = Mustache.render Message.template[:queue],
98
+ :queue => queue,
99
+ :anything? => (queue.empty?) ? false : true
100
+ message.respond rendered
101
+
102
+ when /^volume\?$/i
103
+ message.respond "The volume is #{Mumbletune.player.volume?}."
104
+
105
+ when /^volume/i
106
+ if message.argument.length == 0
107
+ message.respond "The volume is #{Mumbletune.player.volume?}."
108
+ else
109
+ Mumbletune.player.volume(message.argument)
110
+ message.respond "Now the volume is #{Mumbletune.player.volume?}."
111
+ end
112
+
113
+ when /^wisdom$/i
114
+ message.respond `fortune`
115
+
116
+ when /^help$/i
117
+ rendered = Mustache.render Message.template[:commands]
118
+ message.respond rendered
119
+
120
+ else # Unknown command was given.
121
+ rendered = Mustache.render Message.template[:commands],
122
+ :unknown => { :command => message.text }
123
+ message.respond rendered
124
+ end
125
+
126
+ rescue => err # Catch any command that errored.
127
+ message.respond "Woah, an error occurred: #{err.message}"
128
+ puts "#{err.class}: #{err.message}"
129
+ puts err.backtrace
130
+ end
131
+ end
132
+
133
+
134
+ # instance methods
135
+
136
+ attr_accessor :client, :sender, :text, :command, :argument, :words
137
+
138
+ def initialize(client, data)
139
+ @client = client
140
+ @sender = client.users[data[:actor]] # users are stored by their session ID
141
+ @me = client.me
142
+ @text = data[:message]
143
+
144
+ @words = @text.split
145
+ @command = words[0]
146
+ @argument = words[1...words.length].join(" ")
147
+ end
148
+
149
+ def respond(message)
150
+ @client.text_user(@sender.session, message)
151
+ end
152
+
153
+ def respond_all(message) # send to entire channel
154
+ @client.text_channel(@me.channel_id, message)
155
+ end
156
+ end
157
+
158
+ # load templates
159
+ Dir.glob(File.dirname(__FILE__) + "/templates/*.mustache").each do |f_path|
160
+ f = File.open(f_path)
161
+ Message.template[File.basename(f_path, ".mustache").to_sym] = f.read
162
+ end
163
+ end
@@ -0,0 +1,180 @@
1
+ require 'ruby-mpd'
2
+
3
+ module Mumbletune
4
+
5
+ class Player
6
+
7
+ attr_accessor :history
8
+
9
+ def initialize(host=Mumbletune.config['mpd']['host'], port=Mumbletune.config['mpd']['port'])
10
+ @mpd = MPD.new(host, port)
11
+ @mpd.connect true # 'true' enables callbacks
12
+ @mpd.clear
13
+
14
+ @history = Array.new
15
+ @prev_id = 0
16
+
17
+ self.default_volume
18
+ self.establish_callbacks
19
+ end
20
+
21
+ # Setup
22
+
23
+ def default_volume
24
+ @mpd.volume = Mumbletune.config["mpd"]["default_volume"] || 100
25
+ end
26
+
27
+ def establish_callbacks
28
+ @mpd.on :connection do |status|
29
+ if status == false
30
+ @mpd.connect true
31
+ else
32
+ puts ">> MPD happens to be connected."
33
+ end
34
+ end
35
+
36
+ # Fires when currently playing song changes.
37
+ @mpd.on :songid do |id|
38
+
39
+ # Clear old tracks from the store.
40
+ Track.store.delete_if { |t| t.mpd_id == @prev_id }
41
+
42
+ @prev_id = id
43
+ end
44
+ end
45
+
46
+ # Status methods
47
+
48
+ def playing?
49
+ state = @mpd.status[:state]
50
+ if state =~ /^(play|pause)$/i
51
+ true
52
+ else
53
+ false
54
+ end
55
+ end
56
+
57
+ def paused?
58
+ state = @mpd.status[:state]
59
+ if state == :pause
60
+ true
61
+ else
62
+ false
63
+ end
64
+ end
65
+
66
+
67
+ # MPD Settings
68
+
69
+ def volume?
70
+ @mpd.volume
71
+ end
72
+
73
+ def volume(percent)
74
+ @mpd.volume = percent
75
+ end
76
+
77
+
78
+
79
+ # Queue
80
+
81
+ def add_collection(col, now=false)
82
+ col.tracks.each do |t|
83
+ id = @mpd.addid t.url,
84
+ (now) ? col.tracks.index(t)+1 : nil
85
+ t.mpd_id = id
86
+ end
87
+
88
+ @history.push col
89
+
90
+ @mpd.next if now
91
+ end
92
+
93
+ def queue
94
+ # associate known Tracks with Queue items
95
+ mapped_queue = @mpd.queue.map do |mpd_song|
96
+ t = Track.retreive_from_mpd_id(mpd_song.id)
97
+ t.queue_pos = mpd_song.pos
98
+ t
99
+ end
100
+ # mapped_queue.delete_if { |t| t == current_song }
101
+ mapped_queue
102
+ end
103
+
104
+ def current_song
105
+ Track.retreive_from_mpd_id(@mpd.current_song.id) if @mpd.playing?
106
+ end
107
+
108
+ def undo
109
+ last_collection = @history.pop
110
+
111
+ last_collection.tracks.each do |t|
112
+ to_delete = @mpd.queue.select { |mpd_song| mpd_song.id == t.mpd_id }.first
113
+ @mpd.delete(to_delete.pos) if to_delete
114
+ end
115
+ last_collection
116
+ end
117
+
118
+ def clear_all
119
+ @mpd.clear
120
+ end
121
+
122
+ def clear_queue
123
+ current = @mpd.current_song
124
+ @mpd.queue.each do |t|
125
+ @mpd.delete :id => t.id unless t.id == current.id
126
+ end
127
+ end
128
+
129
+
130
+ =begin
131
+ # Deprecated add methods. Time to remove?
132
+
133
+ def add(track)
134
+ id = @mpd.addid track.url
135
+ track.mpd_id = id
136
+ end
137
+
138
+ def add_batch(tracks)
139
+ tracks.each do |t|
140
+ id = @mpd.addid t.url
141
+ t.mpd_id = id
142
+ end
143
+ end
144
+
145
+ def add_now(track)
146
+ id = @mpd.addid track.url, 1
147
+ track.mpd_id = id
148
+ @mpd.next
149
+ end
150
+
151
+ def add_now_batch(tracks)
152
+ tracks.each do |t|
153
+ id = @mpd.addid t.url, tracks.index(t)+1
154
+ t.mpd_id = id
155
+ end
156
+ @mpd.next
157
+ end
158
+ =end
159
+
160
+ # Playback commands
161
+
162
+ def play
163
+ @mpd.play
164
+ end
165
+
166
+ def pause
167
+ @mpd.pause = (@mpd.playing?) ? true : false
168
+ end
169
+
170
+ def next
171
+ @mpd.next
172
+ end
173
+
174
+ def stop
175
+ @mpd.stop
176
+ end
177
+
178
+ end
179
+
180
+ end
@@ -0,0 +1,55 @@
1
+ require 'mumble-ruby'
2
+
3
+ module Mumbletune
4
+ class MumbleClient
5
+
6
+ def initialize
7
+ opts = Mumbletune.config['mumble']
8
+ @cli = Mumble::Client.new(opts['host'], opts['port'], opts['username'], opts['password'])
9
+
10
+ @cli.on_server_sync do |message| # Once connected.
11
+ @cli.session = message.session # housekeeping for mumble-ruby
12
+ connect_to = @cli.channels.select { |key, hash| hash["name"] == opts['channel'] }.first[1][:name]
13
+ @cli.join_channel(connect_to)
14
+
15
+ @ready = true
16
+ puts ">> Connected to Mumble server at #{opts['host']}."
17
+ end
18
+
19
+ @cli.on_text_message do |data|
20
+ if data[:session].include?(@cli.me[:actor]) # if message was sent to us
21
+ # interpret the message in a separate thread
22
+ Thread.new { Message.parse(@cli, data) }
23
+ end
24
+ end
25
+ end
26
+
27
+ def connect
28
+ @ready = false
29
+ @cli.connect
30
+
31
+ @ready_wait = Thread.new do
32
+ sleep 0.1 until @ready
33
+ end
34
+ end
35
+
36
+ def stream
37
+ @ready_wait.join
38
+ input = Mumbletune.config['mpd']['fifo_out']
39
+ Thread.current.priority = 5
40
+ puts ">> Streaming to Mumble from #{input}"
41
+ @cli.stream_raw_audio(input)
42
+ end
43
+
44
+
45
+ end
46
+
47
+ end
48
+
49
+ # on_server_sync is used internally, and our callback overloads its own.
50
+ # We need access to `session` to handle the function internally
51
+ module Mumble
52
+ class Client
53
+ attr_accessor :session
54
+ end
55
+ end