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 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