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