mumbletune 0.1.3 → 0.2.1
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 +3 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +42 -49
- data/README.md +11 -10
- data/bin/mumbletune-ctl +23 -0
- data/conf.example.yaml +3 -4
- data/docs/commands.md +6 -3
- data/lib/mumbletune.rb +40 -35
- data/lib/mumbletune/collection.rb +25 -5
- data/lib/mumbletune/hallon_player.rb +162 -0
- data/lib/mumbletune/messages.rb +24 -19
- data/lib/mumbletune/mumble_client.rb +28 -9
- data/lib/mumbletune/resolver.rb +19 -32
- data/lib/mumbletune/spotify_resolver.rb +51 -0
- data/lib/mumbletune/{templates → template}/commands.mustache +4 -3
- data/lib/mumbletune/{templates → template}/queue.mustache +13 -11
- data/lib/mumbletune/version.rb +1 -1
- data/logs/.gitignore +4 -0
- data/mumbletune.gemspec +5 -10
- metadata +18 -82
- data/lib/mumbletune/handle_sp_error.rb +0 -23
- data/lib/mumbletune/mpd_client.rb +0 -171
- data/lib/mumbletune/sp_uri_server.rb +0 -42
- data/lib/mumbletune/spotify_track.rb +0 -90
- data/lib/mumbletune/track.rb +0 -32
data/.gitignore
CHANGED
data/Gemfile
CHANGED
@@ -2,3 +2,6 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in mumbletune.gemspec
|
4
4
|
gemspec
|
5
|
+
|
6
|
+
gem "mumble-ruby", :github => "elliottwilliams/mumble-ruby", :branch => "master"
|
7
|
+
gem "hallon-queue-output", :github => "elliottwilliams/hallon-queue-output", :branch => "master"
|
data/Gemfile.lock
CHANGED
@@ -1,74 +1,67 @@
|
|
1
|
+
GIT
|
2
|
+
remote: git://github.com/elliottwilliams/hallon-queue-output.git
|
3
|
+
revision: 19883c7b06a8b2bb7256bbef2579e715f359ab9e
|
4
|
+
branch: master
|
5
|
+
specs:
|
6
|
+
hallon-queue-output (0.0.2)
|
7
|
+
|
8
|
+
GIT
|
9
|
+
remote: git://github.com/elliottwilliams/mumble-ruby.git
|
10
|
+
revision: 8a557f55d2bace410e9ca046469e7ab7344a51de
|
11
|
+
branch: master
|
12
|
+
specs:
|
13
|
+
mumble-ruby (0.0.3)
|
14
|
+
activesupport
|
15
|
+
celt-ruby
|
16
|
+
ruby-libsamplerate
|
17
|
+
ruby_protobuf
|
18
|
+
|
1
19
|
PATH
|
2
20
|
remote: .
|
3
21
|
specs:
|
4
|
-
mumbletune (0.
|
5
|
-
|
6
|
-
|
22
|
+
mumbletune (0.2.0)
|
23
|
+
daemons
|
24
|
+
ffi (~> 1.3.0)
|
25
|
+
hallon
|
7
26
|
mumble-ruby
|
8
27
|
mustache
|
9
|
-
ruby-mpd
|
10
|
-
rubypython
|
11
|
-
sinatra
|
12
28
|
text
|
13
|
-
thin
|
14
29
|
|
15
30
|
GEM
|
16
31
|
remote: https://rubygems.org/
|
17
32
|
specs:
|
18
|
-
activesupport (3.2.
|
19
|
-
i18n (
|
33
|
+
activesupport (3.2.13)
|
34
|
+
i18n (= 0.6.1)
|
20
35
|
multi_json (~> 1.0)
|
21
|
-
blankslate (3.1.2)
|
22
36
|
celt-ruby (0.0.1)
|
23
37
|
ffi
|
24
|
-
columnize (0.3.6)
|
25
38
|
daemons (1.1.9)
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
ffi (1.4.0)
|
34
|
-
httparty (0.10.2)
|
35
|
-
multi_json (~> 1.0)
|
36
|
-
multi_xml (>= 0.5.2)
|
37
|
-
i18n (0.6.4)
|
38
|
-
meta-spotify (0.3.0)
|
39
|
-
httparty (> 0.8)
|
40
|
-
multi_json (1.6.1)
|
41
|
-
multi_xml (0.5.3)
|
42
|
-
mumble-ruby (0.0.3)
|
43
|
-
activesupport
|
44
|
-
celt-ruby
|
45
|
-
ruby_protobuf
|
39
|
+
ffi (1.3.1)
|
40
|
+
hallon (0.18.1)
|
41
|
+
spotify (~> 12.3.0)
|
42
|
+
weak_observable (~> 1.0)
|
43
|
+
i18n (0.6.1)
|
44
|
+
libspotify (12.1.51.3)
|
45
|
+
multi_json (1.7.2)
|
46
46
|
mustache (0.99.4)
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
ruby-mpd (0.2.1)
|
47
|
+
rake (10.0.4)
|
48
|
+
ref (1.0.4)
|
49
|
+
ruby-libsamplerate (0.0.1)
|
50
|
+
ffi
|
52
51
|
ruby_protobuf (0.4.11)
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
sinatra (1.4.1)
|
57
|
-
rack (~> 1.5, >= 1.5.2)
|
58
|
-
rack-protection (~> 1.4)
|
59
|
-
tilt (~> 1.3, >= 1.3.4)
|
52
|
+
spotify (12.3.0)
|
53
|
+
ffi (~> 1.0, >= 1.0.11)
|
54
|
+
libspotify (~> 12.1.51)
|
60
55
|
text (1.2.1)
|
61
|
-
|
62
|
-
|
63
|
-
eventmachine (>= 0.12.6)
|
64
|
-
rack (>= 1.0.0)
|
65
|
-
tilt (1.3.5)
|
56
|
+
weak_observable (1.0.1)
|
57
|
+
ref (~> 1.0.0)
|
66
58
|
|
67
59
|
PLATFORMS
|
68
60
|
ruby
|
69
61
|
|
70
62
|
DEPENDENCIES
|
71
63
|
bundler (~> 1.3)
|
72
|
-
|
64
|
+
hallon-queue-output!
|
65
|
+
mumble-ruby!
|
73
66
|
mumbletune!
|
74
67
|
rake
|
data/README.md
CHANGED
@@ -1,23 +1,24 @@
|
|
1
1
|
# Mumbletune
|
2
2
|
|
3
|
-
MUMBLETUNE is
|
3
|
+
MUMBLETUNE is a nice bot that connects to a mumble server and allows users to listen to Spotify together.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
7
|
-
|
7
|
+
What you need:
|
8
|
+
- A Spotify Premium account. I wish it didn't have to be this way, but for API access, Premium's required
|
9
|
+
- A Spotify App Key. Spotify will issue you one [here][1].
|
8
10
|
|
9
|
-
|
10
|
-
cd spotify-websocket-api
|
11
|
-
python setup.py build
|
12
|
-
python setup.py install
|
11
|
+
Clone this repo and install its dependencies
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
$ git clone git://github.com/elliottwilliams/mumbletune.git
|
14
|
+
cd mumbletune
|
15
|
+
bundle install
|
17
16
|
|
18
17
|
## Usage
|
19
18
|
|
20
19
|
1. Create a configuration file. See `conf.example.yaml` for help.
|
21
20
|
2. Start Mumbletune, passing your config with `-c`.
|
22
21
|
|
23
|
-
$ mumbletune -c config_file.yaml
|
22
|
+
$ mumbletune -c config_file.yaml
|
23
|
+
|
24
|
+
[1]: https://developer.spotify.com/technologies/libspotify/keys/
|
data/bin/mumbletune-ctl
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "daemons"
|
4
|
+
require "optparse"
|
5
|
+
|
6
|
+
dir = File.expand_path File.dirname(__FILE__)
|
7
|
+
log = File.expand_path File.join(dir, "../logs")
|
8
|
+
|
9
|
+
# change -c paramater to full path (survives daemon)
|
10
|
+
ARGV.each_index do |i|
|
11
|
+
if ARGV[i] == "-c"
|
12
|
+
ARGV[i+1] = File.expand_path File.join(Dir.pwd, ARGV[i+1])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
options = {
|
17
|
+
app_name: "mumbletune",
|
18
|
+
dir_mode: :normal,
|
19
|
+
dir: log,
|
20
|
+
log_output: true
|
21
|
+
}
|
22
|
+
|
23
|
+
Daemons.run(File.join(dir, "mumbletune"), options)
|
data/conf.example.yaml
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
spotify:
|
2
|
+
appkey: "/path/to/spotify_appkey.key"
|
2
3
|
username: "sp_username"
|
3
4
|
password: "sp_password"
|
4
5
|
region: "us"
|
@@ -8,8 +9,6 @@ mumble:
|
|
8
9
|
username: "mumbletune"
|
9
10
|
password: "server_password"
|
10
11
|
channel: "Music Room"
|
11
|
-
|
12
|
-
host: "localhost"
|
13
|
-
port: 6600
|
14
|
-
fifo_out: "/tmp/mumble.fifo"
|
12
|
+
player:
|
15
13
|
default_volume: 60
|
14
|
+
tracks_for_artist: 5
|
data/docs/commands.md
CHANGED
@@ -16,9 +16,12 @@
|
|
16
16
|
<br>
|
17
17
|
For example:
|
18
18
|
|
19
|
-
|
20
|
-
play nicki minaj starships
|
19
|
+
`play coldplay` plays the top 5 tracks by Coldplay.
|
21
20
|
|
22
|
-
|
21
|
+
`play nicki minaj starships` will play the (excellent) song Starships.
|
22
|
+
|
23
|
+
Mumbletune uses Spotify to find and play music. You can `play` a Spotify URL directly instead of searching, like
|
24
|
+
|
25
|
+
`play spotify:album:1HjSyGjmLNjRAKgT9t1cna`
|
23
26
|
|
24
27
|
<!-- Generate HTML with: $ pandoc -f markdown -->
|
data/lib/mumbletune.rb
CHANGED
@@ -1,81 +1,86 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
|
3
|
+
require 'mumbletune/version'
|
1
4
|
require 'mumbletune/mumble_client'
|
2
|
-
require 'mumbletune/
|
5
|
+
require 'mumbletune/hallon_player'
|
3
6
|
require 'mumbletune/messages'
|
4
|
-
require 'mumbletune/track'
|
5
7
|
require 'mumbletune/collection'
|
6
|
-
require 'mumbletune/sp_uri_server'
|
7
|
-
require 'mumbletune/spotify_track'
|
8
8
|
require 'mumbletune/resolver'
|
9
|
-
require 'mumbletune/
|
9
|
+
require 'mumbletune/spotify_resolver'
|
10
10
|
|
11
11
|
require 'optparse'
|
12
12
|
require 'yaml'
|
13
|
-
require 'eventmachine'
|
14
|
-
require 'rubypython'
|
15
13
|
|
16
14
|
module Mumbletune
|
17
15
|
class << self
|
18
|
-
attr_reader :player, :mumble, :uri_server, :config
|
16
|
+
attr_reader :player, :mumble, :uri_server, :config, :verbose
|
19
17
|
end
|
20
18
|
|
19
|
+
# defaults
|
20
|
+
@verbose = false
|
21
|
+
config_file = nil
|
22
|
+
|
21
23
|
# parse command line options
|
22
|
-
|
23
|
-
OptionParser.new do |opts|
|
24
|
+
opts = OptionParser.new do |opts|
|
24
25
|
opts.banner = "Usage: mumbletune.rb [options]"
|
25
26
|
opts.on("-c", "--config FILE", "=MANDATORY", "Path to configuration file") do |file|
|
26
27
|
config_file = file
|
27
28
|
end
|
29
|
+
opts.on("-v", "--verbose", "Verbose output") do
|
30
|
+
@verbose = true
|
31
|
+
end
|
28
32
|
opts.on("-h", "--help", "This help message") do
|
29
|
-
puts opts.help
|
33
|
+
puts opts.help
|
30
34
|
exit
|
31
35
|
end
|
32
|
-
end
|
33
|
-
raise OptionParser::MissingArgument unless config_file
|
34
|
-
|
35
|
-
# load configuration file
|
36
|
-
@config = YAML.load_file(config_file)
|
36
|
+
end
|
37
37
|
|
38
|
-
|
39
|
-
puts ">> Loading Spotify APIs..."
|
40
|
-
RubyPython.start(:python_exe => 'python2.7')
|
41
|
-
Spotify = RubyPython.import('spotify_web.friendly').Spotify
|
38
|
+
opts.parse!
|
42
39
|
|
43
|
-
|
44
|
-
|
45
|
-
|
40
|
+
unless config_file
|
41
|
+
puts opts.help
|
42
|
+
exit
|
46
43
|
end
|
47
44
|
|
45
|
+
# load configuration file
|
46
|
+
@config = YAML.load_file(config_file)
|
47
|
+
|
48
48
|
# initialize player
|
49
49
|
play_thread = Thread.new do
|
50
|
-
@player =
|
50
|
+
@player = HallonPlayer.new
|
51
|
+
@player.connect
|
52
|
+
puts ">> Connected to Spotify."
|
51
53
|
end
|
52
54
|
|
53
55
|
# connect to mumble & start streaming
|
56
|
+
sleep 0.1 until @player && @player.ready
|
54
57
|
mumble_thread = Thread.new do
|
55
58
|
@mumble = MumbleClient.new
|
56
59
|
@mumble.connect
|
60
|
+
puts ">> Connected to Mumble server at #{self.config['mumble']['host']}."
|
57
61
|
@mumble.stream
|
58
62
|
end
|
59
63
|
|
60
64
|
# shutdown code
|
61
65
|
def self.shutdown
|
66
|
+
Thread.new do
|
67
|
+
sleep 5 # timeout
|
68
|
+
puts "Timeout. Forcing exit."
|
69
|
+
exit!
|
70
|
+
end
|
71
|
+
print "\n>> Exiting... "
|
62
72
|
self.mumble.disconnect
|
73
|
+
print "Disconnected from Mumble... "
|
63
74
|
self.player.disconnect
|
64
|
-
|
75
|
+
print "Disconnected from Spotify... "
|
76
|
+
puts "\nGoodbye forever."
|
65
77
|
exit
|
66
78
|
end
|
67
79
|
|
68
80
|
# exit when Ctrl-C pressed
|
69
|
-
|
70
|
-
|
81
|
+
Signal.trap("INT") do
|
82
|
+
Mumbletune.shutdown
|
71
83
|
end
|
72
84
|
|
73
|
-
#
|
74
|
-
# sleep 3
|
75
|
-
# @player.command_test_load
|
76
|
-
# @player.command_play
|
77
|
-
|
78
|
-
Thread.stop # wake up to shut down
|
79
|
-
self.shutdown
|
80
|
-
|
85
|
+
Thread.stop # we're done here
|
81
86
|
end
|
@@ -1,17 +1,37 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
1
3
|
module Mumbletune
|
2
4
|
class Collection
|
3
|
-
|
5
|
+
include Enumerable
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
attr_accessor :type, :description, :user, :tracks, :history, :current_track
|
9
|
+
|
10
|
+
def_delegators :@tracks, :length, :first, :last, :each, :any?, :empty?
|
4
11
|
|
5
12
|
def initialize(type, tracks, description)
|
6
13
|
@type = type
|
7
14
|
@description = description
|
15
|
+
@tracks = [tracks].flatten
|
16
|
+
@history = Array.new
|
17
|
+
end
|
8
18
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
@tracks
|
19
|
+
def next
|
20
|
+
if @current_track
|
21
|
+
@history << @current_track
|
22
|
+
@tracks.delete @current_track
|
13
23
|
end
|
24
|
+
@current_track = @tracks.first
|
14
25
|
end
|
15
26
|
|
27
|
+
def empty?
|
28
|
+
without_current = @tracks.dup
|
29
|
+
without_current.delete_if { |t| t == @current_track }
|
30
|
+
without_current.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
def any?
|
34
|
+
!empty?
|
35
|
+
end
|
16
36
|
end
|
17
37
|
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require "hallon"
|
2
|
+
require "hallon-queue-output"
|
3
|
+
require "thread"
|
4
|
+
|
5
|
+
module Mumbletune
|
6
|
+
|
7
|
+
class HallonPlayer
|
8
|
+
|
9
|
+
attr_accessor :ready, :play_history, :add_history, :queue, :current_track, :audio_queue
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
conf = Mumbletune.config
|
13
|
+
|
14
|
+
@play_history = Array.new
|
15
|
+
@add_history = Array.new
|
16
|
+
@queue = Array.new
|
17
|
+
@prev_id = 0
|
18
|
+
@ready = false
|
19
|
+
@audio_queue = Queue.new
|
20
|
+
|
21
|
+
@ready = true
|
22
|
+
end
|
23
|
+
|
24
|
+
def connect
|
25
|
+
conf = Mumbletune.config
|
26
|
+
|
27
|
+
@session = Hallon::Session.initialize(IO.read(conf["spotify"]["appkey"]))
|
28
|
+
@session.login!(conf["spotify"]["username"], conf["spotify"]["password"])
|
29
|
+
|
30
|
+
@player = Hallon::Player.new(Hallon::QueueOutput) do
|
31
|
+
# Set driver options
|
32
|
+
@driver.queue = Mumbletune.player.audio_queue
|
33
|
+
@driver.verbose = true if Mumbletune.verbose
|
34
|
+
|
35
|
+
# Define callbacks
|
36
|
+
on(:end_of_track) { Mumbletune.player.next }
|
37
|
+
on(:streaming_error) { |s, e| raise "Spotify connection error: #{e}" }
|
38
|
+
on(:play_token_lost) { Mumbletune.player.play_token_lost }
|
39
|
+
end
|
40
|
+
|
41
|
+
@ready = true
|
42
|
+
|
43
|
+
start_event_loop
|
44
|
+
end
|
45
|
+
|
46
|
+
def disconnect
|
47
|
+
@logging_out = true
|
48
|
+
@event_loop_thread.kill
|
49
|
+
@session.logout!
|
50
|
+
end
|
51
|
+
|
52
|
+
# Status methods
|
53
|
+
def playing?
|
54
|
+
@player.status == :playing
|
55
|
+
end
|
56
|
+
|
57
|
+
def paused?
|
58
|
+
@player.status == :paused
|
59
|
+
end
|
60
|
+
|
61
|
+
def stopped?
|
62
|
+
@player.status == :stopped
|
63
|
+
end
|
64
|
+
|
65
|
+
def any?
|
66
|
+
cols_with_more = @queue.map { |col| col.any? }
|
67
|
+
cols_with_more.delete_if { |m| m == false }
|
68
|
+
cols_with_more.any?
|
69
|
+
end
|
70
|
+
|
71
|
+
def empty?
|
72
|
+
!any?
|
73
|
+
end
|
74
|
+
|
75
|
+
# Queue Control
|
76
|
+
def add_collection(col, now=false)
|
77
|
+
only_track = empty?
|
78
|
+
|
79
|
+
# add to the queue
|
80
|
+
if now
|
81
|
+
@queue.unshift col
|
82
|
+
else
|
83
|
+
@queue.push col
|
84
|
+
end
|
85
|
+
|
86
|
+
# record in additions history
|
87
|
+
@add_history.push col
|
88
|
+
|
89
|
+
# play it, if this is the first track or if the user specified `now`
|
90
|
+
self.next if now || only_track
|
91
|
+
end
|
92
|
+
|
93
|
+
def undo
|
94
|
+
removed = @add_history.pop
|
95
|
+
@queue.delete_if { |col| col == removed }
|
96
|
+
self.next if removed.current_track
|
97
|
+
removed
|
98
|
+
end
|
99
|
+
|
100
|
+
def clear_queue
|
101
|
+
@queue.clear
|
102
|
+
self.stop
|
103
|
+
end
|
104
|
+
|
105
|
+
# Playback Commands
|
106
|
+
def play
|
107
|
+
if stopped?
|
108
|
+
self.next
|
109
|
+
elsif paused?
|
110
|
+
@player.play
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def pause
|
115
|
+
if playing?
|
116
|
+
@player.pause
|
117
|
+
elsif paused?
|
118
|
+
@player.play
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def next
|
123
|
+
# move the collection to play_history if it has played all its tracks
|
124
|
+
@play_history << @queue.shift if @queue.first && @queue.first.empty?
|
125
|
+
|
126
|
+
return stop unless any?
|
127
|
+
|
128
|
+
track = @queue.first.next
|
129
|
+
|
130
|
+
return stop unless track
|
131
|
+
|
132
|
+
# play that shit!
|
133
|
+
@current_track = track
|
134
|
+
@player.play track
|
135
|
+
|
136
|
+
puts "\u266B #{track.name} - #{track.artist.name}" unless Mumbletune.verbose
|
137
|
+
end
|
138
|
+
|
139
|
+
def stop
|
140
|
+
@player.stop
|
141
|
+
end
|
142
|
+
|
143
|
+
# Callback Handling
|
144
|
+
def play_token_lost
|
145
|
+
Mumbletune.mumble.broadcast %w{Mumbletune was just paused because this
|
146
|
+
Spotify account is being used elsewhere. Type <code>unpause
|
147
|
+
</code> to regain control and keep playing.} * " "
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def start_event_loop
|
153
|
+
@event_loop_thread = Thread.new do
|
154
|
+
loop do
|
155
|
+
@session.process_events unless @session.disconnected?
|
156
|
+
sleep 1
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
end
|